@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
package/src/channel.ts DELETED
@@ -1,1879 +0,0 @@
1
- import { ChannelState } from './channel_state';
2
- import { normalizeFileName, isVideoFile, buildAttachmentPayload } from './attachment_utils';
3
- import type { VoiceRecordingMeta } from './attachment_utils';
4
- import {
5
- enrichWithUserInfo,
6
- ensureMembersUserInfoLoaded,
7
- getDirectChannelImage,
8
- getDirectChannelName,
9
- getUserInfo,
10
- logChatPromiseExecution,
11
- randomId,
12
- } from './utils';
13
- import { ErmisChat } from './client';
14
- import {
15
- APIResponse,
16
- Attachment,
17
- ChannelAPIResponse,
18
- ChannelData,
19
- ChannelQueryOptions,
20
- ChannelResponse,
21
- DefaultGenerics,
22
- Event,
23
- EventHandler,
24
- EventTypes,
25
- ExtendableGenerics,
26
- FormatMessageResponse,
27
- Message,
28
- MessageResponse,
29
- MessageSetType,
30
- ReactionAPIResponse,
31
- SendMessageAPIResponse,
32
- UpdateChannelAPIResponse,
33
- UserResponse,
34
- QueryChannelAPIResponse,
35
- AttachmentResponse,
36
- PollMessage,
37
- EditMessage,
38
- ForwardMessage,
39
- CreateTopicData,
40
- EditTopicData,
41
- } from './types';
42
- /**
43
- * Represents a Channel in the Ermis Network.
44
- * Channels handle chat sessions, livestream messages, teams, or video calls.
45
- * This class abstracts and exposes all API operations you can perform on a specific channel instance.
46
- */
47
- export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
48
- _client: ErmisChat<ErmisChatGenerics>;
49
- type: string;
50
- id: string | undefined;
51
- data: ChannelData<ErmisChatGenerics> | ChannelResponse<ErmisChatGenerics> | undefined;
52
- _data: ChannelData<ErmisChatGenerics> | ChannelResponse<ErmisChatGenerics>;
53
- cid: string;
54
- listeners: { [key: string]: (string | EventHandler<ErmisChatGenerics>)[] };
55
- state: ChannelState<ErmisChatGenerics>;
56
- initialized: boolean;
57
- offlineMode: boolean;
58
- lastKeyStroke?: Date;
59
- lastTypingEvent: Date | null;
60
- isTyping: boolean;
61
- disconnected: boolean;
62
-
63
- /**
64
- * Initializes a new Channel class instance.
65
- * Normally you should not call this directly; use `client.channel(type, id)` instead.
66
- *
67
- * @param client - The shared ErmisChat client instance initializing this channel.
68
- * @param type - The type of channel (`messaging`, `team`, `livestream`, etc.).
69
- * @param id - The unique ID of the channel.
70
- * @param data - Initial arbitrary metadata stored within this channel.
71
- */
72
- constructor(
73
- client: ErmisChat<ErmisChatGenerics>,
74
- type: string,
75
- id: string | undefined,
76
- data: ChannelData<ErmisChatGenerics>,
77
- ) {
78
- const validTypeRe = /^[\w_-]+$/;
79
- const validIDRe = /^[\w!:_-]+$/;
80
-
81
- if (!validTypeRe.test(type)) {
82
- throw new Error(`Invalid chat type ${type}, letters, numbers and "_-" are allowed`);
83
- }
84
- if (typeof id === 'string' && !validIDRe.test(id)) {
85
- throw new Error(`Invalid chat id ${id}, letters, numbers and "!-_" are allowed`);
86
- }
87
-
88
- this._client = client;
89
- this.type = type;
90
- this.id = id;
91
- this.data = data;
92
- this._data = { ...data };
93
- this.cid = `${type}:${id}`;
94
- this.listeners = {};
95
- this.state = new ChannelState<ErmisChatGenerics>(this);
96
- this.initialized = false;
97
- this.offlineMode = false;
98
- this.lastTypingEvent = null;
99
- this.isTyping = false;
100
- this.disconnected = false;
101
- }
102
-
103
- getClient(): ErmisChat<ErmisChatGenerics> {
104
- return this._client;
105
- }
106
-
107
- /**
108
- * Sends a message to this channel.
109
- * By default, it pushes the message eagerly (optimistically) to the local UI state before the server replies.
110
- *
111
- * @param message - The constructed text/attachment object payload representing the message.
112
- * @returns A Promise resolving to the exact API response encompassing message details.
113
- */
114
- async sendMessage(message: Message<ErmisChatGenerics>) {
115
- // 1. Generate ID upfront
116
- if (!message.id) {
117
- message = { ...message, id: randomId() };
118
- }
119
- const messageId = message.id!;
120
-
121
- // 2. Build optimistic (fake) message and push into state immediately
122
- const optimisticMessage = {
123
- ...message,
124
- id: messageId,
125
- status: 'sending',
126
- created_at: new Date().toISOString(),
127
- updated_at: new Date().toISOString(),
128
- user: this.getClient().user,
129
- user_id: this.getClient().userID,
130
- type: 'regular',
131
- } as unknown as MessageResponse<ErmisChatGenerics>;
132
-
133
- this.state.addMessageSorted(optimisticMessage);
134
-
135
- // 3. Call API — don't update status on success (WS message.new will handle it)
136
- try {
137
- return await this.getClient().post<SendMessageAPIResponse<ErmisChatGenerics>>(this._channelURL() + '/message', {
138
- message: { ...message },
139
- });
140
- } catch (error: any) {
141
- // 4. On error: check if it's an offline/network error
142
- const isOfflineError =
143
- !error.response ||
144
- error.code === 'ERR_NETWORK' ||
145
- error.isWSFailure ||
146
- !this.getClient().wsConnection?.isHealthy;
147
- const statusToSet = isOfflineError ? 'failed_offline' : 'error';
148
- this.state.updateMessageStatus(messageId, statusToSet);
149
- throw error;
150
- }
151
- }
152
-
153
- async retryMessage(messageId: string) {
154
- const stateMsg = this.state.messages.find((m) => m.id === messageId);
155
- if (!stateMsg) throw new Error(`Message ${messageId} not found in state`);
156
-
157
- this.state.updateMessageStatus(messageId, 'sending');
158
-
159
- const messagePayload: any = {
160
- id: stateMsg.id,
161
- text: stateMsg.text,
162
- attachments: stateMsg.attachments,
163
- mentioned_users: stateMsg.mentioned_users,
164
- parent_id: stateMsg.parent_id,
165
- quoted_message_id: stateMsg.quoted_message_id,
166
- sticker_url: (stateMsg as any).sticker_url,
167
- };
168
-
169
- if (stateMsg.show_in_channel !== undefined) {
170
- messagePayload.show_in_channel = stateMsg.show_in_channel;
171
- }
172
-
173
- try {
174
- return await this.getClient().post<SendMessageAPIResponse<ErmisChatGenerics>>(this._channelURL() + '/message', {
175
- message: messagePayload,
176
- });
177
- } catch (error: any) {
178
- const isOfflineError =
179
- !error.response ||
180
- error.code === 'ERR_NETWORK' ||
181
- error.isWSFailure ||
182
- !this.getClient().wsConnection?.isHealthy;
183
- this.state.updateMessageStatus(messageId, isOfflineError ? 'failed_offline' : 'error');
184
- throw error;
185
- }
186
- }
187
-
188
- async createPoll(pollMessage: PollMessage) {
189
- const id = randomId();
190
- pollMessage = { ...pollMessage, id };
191
-
192
- return await this.getClient().post<SendMessageAPIResponse<ErmisChatGenerics>>(this._channelURL() + '/message', {
193
- message: { ...pollMessage },
194
- });
195
- }
196
-
197
- async votePoll(messageID: string, pollChoice: string) {
198
- if (!messageID) {
199
- throw Error(`Message id is missing`);
200
- }
201
- return await this.getClient().post<APIResponse>(
202
- this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/poll/${pollChoice}`,
203
- );
204
- }
205
-
206
- async forwardMessage(message: ForwardMessage<ErmisChatGenerics>, channel: { type: string; channelID: string }) {
207
- if (!message.id) {
208
- message = { ...message, id: randomId() };
209
- }
210
-
211
- return await this.getClient().post<SendMessageAPIResponse<ErmisChatGenerics>>(
212
- `${this.getClient().baseURL}/channels/${channel.type}/${channel.channelID}` + '/message',
213
- {
214
- message: { ...message },
215
- },
216
- );
217
- }
218
-
219
- async pinMessage(messageID: string) {
220
- return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/pin`);
221
- }
222
-
223
- async unpinMessage(messageID: string) {
224
- return await this.getClient().post(
225
- this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/unpin`,
226
- );
227
- }
228
-
229
- async pin() {
230
- if (this.data) this.data.is_pinned = true;
231
- this.getClient().dispatchEvent({
232
- type: 'channel.pinned',
233
- cid: this.cid,
234
- channel: this.data,
235
- } as Event<ErmisChatGenerics>);
236
-
237
- try {
238
- return await this.getClient().pinChannel(this.type, this.id as string);
239
- } catch (e) {
240
- if (this.data) this.data.is_pinned = false;
241
- this.getClient().dispatchEvent({
242
- type: 'channel.unpinned',
243
- cid: this.cid,
244
- channel: this.data,
245
- } as Event<ErmisChatGenerics>);
246
- throw e;
247
- }
248
- }
249
-
250
- async unpin() {
251
- if (this.data) this.data.is_pinned = false;
252
- this.getClient().dispatchEvent({
253
- type: 'channel.unpinned',
254
- cid: this.cid,
255
- channel: this.data,
256
- } as Event<ErmisChatGenerics>);
257
-
258
- try {
259
- return await this.getClient().unpinChannel(this.type, this.id as string);
260
- } catch (e) {
261
- if (this.data) this.data.is_pinned = true;
262
- this.getClient().dispatchEvent({
263
- type: 'channel.pinned',
264
- cid: this.cid,
265
- channel: this.data,
266
- } as Event<ErmisChatGenerics>);
267
- throw e;
268
- }
269
- }
270
-
271
- async editMessage(oldMessageID: string, message: EditMessage) {
272
- return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
273
- message,
274
- });
275
- }
276
-
277
- sendFile(
278
- uri: string | NodeJS.ReadableStream | Buffer | File,
279
- name?: string,
280
- contentType?: string,
281
- user?: UserResponse<ErmisChatGenerics>,
282
- ) {
283
- return this.getClient().sendFile(`${this._channelURL()}/file`, uri, name, contentType, user);
284
- }
285
- /**
286
- * Pre-process files (normalize names), upload them in parallel,
287
- * generate video thumbnails, and build attachment payloads.
288
- *
289
- * @param files - Array of File objects to upload
290
- * @param options - Optional voice recording metadata
291
- * @returns `attachments` ready for sendMessage, and `failedFiles` for error display
292
- */
293
- async uploadAndPrepareAttachments(
294
- files: File[],
295
- options?: {
296
- /** Map from file index → voice recording metadata */
297
- voiceMetadata?: Map<number, VoiceRecordingMeta>;
298
- },
299
- ): Promise<{
300
- attachments: Attachment[];
301
- failedFiles: Array<{ file: File; error: Error }>;
302
- }> {
303
- const failedFiles: Array<{ file: File; error: Error }> = [];
304
-
305
- // 1. Pre-process: normalize file names
306
- const processedFiles = files.map((file) => {
307
- const newName = normalizeFileName(file.name);
308
- if (newName !== file.name) {
309
- return new File([file], newName, { type: file.type, lastModified: file.lastModified });
310
- }
311
- return file;
312
- });
313
-
314
- // 2. Upload all files in parallel
315
- const uploadResults = await Promise.allSettled(
316
- processedFiles.map((file) => this.sendFile(file, file.name, file.type)),
317
- );
318
-
319
- // 3. For successful video uploads, generate and upload thumbnails
320
- const thumbUrls = new Map<number, string>();
321
- const thumbPromises: Promise<void>[] = [];
322
-
323
- for (let i = 0; i < processedFiles.length; i++) {
324
- const result = uploadResults[i];
325
- if (result.status === 'fulfilled' && isVideoFile(processedFiles[i])) {
326
- thumbPromises.push(
327
- (async () => {
328
- try {
329
- const thumbBlob = await this.getThumbBlobVideo(files[i]);
330
- if (thumbBlob) {
331
- const thumbFile = new File([thumbBlob], `thumb_${processedFiles[i].name}.jpg`, { type: 'image/jpeg' });
332
- const thumbResp = await this.sendFile(thumbFile, thumbFile.name, 'image/jpeg');
333
- thumbUrls.set(i, thumbResp.file);
334
- }
335
- } catch {
336
- // Thumbnail failure is non-critical
337
- }
338
- })(),
339
- );
340
- }
341
- }
342
-
343
- await Promise.allSettled(thumbPromises);
344
-
345
- // 4. Build attachment payloads from successful uploads
346
- const attachments: Attachment[] = [];
347
- for (let i = 0; i < processedFiles.length; i++) {
348
- const result = uploadResults[i];
349
- if (result.status === 'fulfilled') {
350
- const uploadedUrl = result.value.file;
351
- const thumbUrl = thumbUrls.get(i);
352
- const voiceMeta = options?.voiceMetadata?.get(i);
353
- attachments.push(buildAttachmentPayload(processedFiles[i], uploadedUrl, thumbUrl, voiceMeta));
354
- } else {
355
- failedFiles.push({
356
- file: files[i],
357
- error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)),
358
- });
359
- }
360
- }
361
-
362
- return { attachments, failedFiles };
363
- }
364
-
365
- async sendEvent(event: Event<ErmisChatGenerics>) {
366
- // this._checkInitialized();
367
- return await this.getClient().post(this._channelURL() + '/event', {
368
- event,
369
- });
370
- }
371
-
372
- async sendReaction(messageID: string, reactionType: string) {
373
- if (!messageID) {
374
- throw Error(`Message id is missing`);
375
- }
376
- return await this.getClient().post<ReactionAPIResponse<ErmisChatGenerics>>(
377
- this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/reaction/${reactionType}`,
378
- );
379
- }
380
-
381
- deleteReaction(messageID: string, reactionType: string) {
382
- // this._checkInitialized();
383
- if (!reactionType || !messageID) {
384
- throw Error('Deleting a reaction requires specifying both the message and reaction type');
385
- }
386
-
387
- const url = this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/reaction/${reactionType}`;
388
- //provided when server side request
389
- // if (user_id) {
390
- // return this.getClient().delete<ReactionAPIResponse<ErmisChatGenerics>>(url, { user_id });
391
- // }
392
-
393
- return this.getClient().delete<ReactionAPIResponse<ErmisChatGenerics>>(url, {});
394
- }
395
-
396
- async update(
397
- channelData: Partial<ChannelData<ErmisChatGenerics>> | Partial<ChannelResponse<ErmisChatGenerics>> = {},
398
- updateMessage?: Message<ErmisChatGenerics>,
399
- ) {
400
- // Strip out reserved names that will result in API errors.
401
- const reserved = [
402
- 'config',
403
- 'cid',
404
- 'created_by',
405
- 'id',
406
- 'member_count',
407
- 'type',
408
- 'created_at',
409
- 'updated_at',
410
- 'last_message_at',
411
- 'own_capabilities',
412
- ];
413
- reserved.forEach((key) => {
414
- delete channelData[key];
415
- });
416
-
417
- return await this._update({
418
- message: updateMessage,
419
- data: channelData,
420
- });
421
- }
422
-
423
- async delete() {
424
- return await this.getClient().delete(this._channelURL());
425
- }
426
-
427
- async truncate() {
428
- return await this.getClient().delete(this._channelURL() + '/truncate');
429
- }
430
-
431
- async blockUser() {
432
- return await this.getClient().post(this._channelURL(), { action: 'block' });
433
- }
434
-
435
- async unblockUser() {
436
- return await this.getClient().post(this._channelURL(), { action: 'unblock' });
437
- }
438
-
439
- async acceptInvite(action: string) {
440
- // const url = this.getClient().baseURL + `/invites/${this.type}/${this.id}/accept`;
441
- const channel_id = this.id;
442
-
443
- const url = this.getClient().userBaseURL + `/token_gate/join_channel/${this.type}`;
444
- return this.getClient().post<APIResponse>(url, {}, { channel_id, action });
445
- }
446
-
447
- async rejectInvite() {
448
- const url = this.getClient().baseURL + `/invites/${this.type}/${this.id}/reject`;
449
- return this.getClient().post<APIResponse>(url);
450
- }
451
-
452
- async skipInvite() {
453
- const url = this.getClient().baseURL + `/invites/${this.type}/${this.id}/skip`;
454
- return this.getClient().post<APIResponse>(url);
455
- }
456
-
457
- /**
458
- * Directly invites or adds registered users into this channel.
459
- *
460
- * @param members - Array of user IDs explicitly selected to be added.
461
- */
462
- async addMembers(members: string[]) {
463
- return await this._update({ add_members: members });
464
- }
465
-
466
- async addModerators(members: string[]) {
467
- return await this._update({ promote_members: members });
468
- }
469
-
470
- async banMembers(members: string[]) {
471
- return await this._update({ ban_members: members });
472
- }
473
-
474
- async unbanMembers(members: string[]) {
475
- return await this._update({ unban_members: members });
476
- }
477
-
478
- async updateCapabilities(capabilities: string[]) {
479
- return await this._update({ capabilities });
480
- }
481
-
482
- /**
483
- * Set slow mode (message cooldown) for the channel.
484
- * Only applicable to team channels. Prevents members from sending
485
- * messages faster than the specified cooldown interval.
486
- *
487
- * @param cooldown - Cooldown duration in milliseconds.
488
- * Allowed values: 0 (off), 10000 (10s), 30000 (30s),
489
- * 60000 (1min), 300000 (5min), 900000 (15min), 3600000 (1h).
490
- */
491
- async setSlowMode(cooldown: 0 | 10000 | 30000 | 60000 | 300000 | 900000 | 3600000) {
492
- const allowedValues = [0, 10000, 30000, 60000, 300000, 900000, 3600000];
493
- if (!allowedValues.includes(cooldown)) {
494
- throw new Error(
495
- `Invalid cooldown value: ${cooldown}. Allowed values are: ${allowedValues.join(', ')} (milliseconds).`,
496
- );
497
- }
498
- return await this.update({ member_message_cooldown: cooldown } as any);
499
- }
500
-
501
- async queryAttachmentMessages() {
502
- const response = await this.getClient().post<AttachmentResponse<ErmisChatGenerics>>(
503
- this.getClient().baseURL + `/channels/${this.type}/${this.id}/attachment`,
504
- {
505
- attachment_types: ['image', 'video', 'file', 'voiceRecording', 'linkPreview'],
506
- },
507
- );
508
-
509
- // Sort newest first
510
- if (response.attachments) {
511
- response.attachments.sort(
512
- (a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
513
- );
514
- }
515
-
516
- return response;
517
- }
518
-
519
- async searchMessage(search_term: string, offset: number) {
520
- const response: any = await this.getClient().post(this.getClient().baseURL + `/channels/search`, {
521
- cid: this.cid,
522
- search_term,
523
- offset,
524
- limit: 25,
525
- });
526
-
527
- if (!response || response?.search_result?.messages.length === 0) {
528
- return null;
529
- }
530
-
531
- return {
532
- ...response?.search_result,
533
- messages: response?.search_result?.messages.map((message: any) => {
534
- const user = getUserInfo(message.user_id, Object.values(this.getClient().state.users)) || message.user;
535
- return { ...message, user };
536
- }),
537
- };
538
- }
539
-
540
- /**
541
- * Expels specified currently participating users out of the channel.
542
- *
543
- * @param members - Array of user IDs to strictly remove from this chat.
544
- */
545
- async removeMembers(members: string[]) {
546
- return await this._update({ remove_members: members });
547
- }
548
-
549
- async demoteModerators(members: string[]) {
550
- return await this._update({ demote_members: members });
551
- }
552
-
553
- async _update(payload: Object) {
554
- const data = await this.getClient().post<UpdateChannelAPIResponse<ErmisChatGenerics>>(this._channelURL(), payload);
555
- this.data = { ...this.data, ...data.channel };
556
- return data;
557
- }
558
-
559
- _processTopics(topicsFromApi: any, users: any[]) {
560
- const topics = topicsFromApi.map((topic: any) => {
561
- // Enrich topic members with user info
562
- if (topic.channel && topic.channel.members) {
563
- topic.channel.members = enrichWithUserInfo(topic.channel.members, users);
564
- }
565
- // Enrich topic messages with user info
566
- if (topic.messages) {
567
- topic.messages = enrichWithUserInfo(topic.messages, users);
568
- }
569
- // Enrich topic pinned messages with user info
570
- if (topic.pinned_messages) {
571
- topic.pinned_messages = enrichWithUserInfo(topic.pinned_messages, users);
572
- }
573
- // Enrich topic read with user info
574
- if (topic.read) {
575
- topic.read = enrichWithUserInfo(topic.read, users);
576
- }
577
- return topic;
578
- });
579
-
580
- const { channels } = this.getClient().hydrateChannels(topics, {});
581
-
582
- // Store topics in channel state
583
- this.state.topics = channels;
584
- }
585
-
586
- async muteNotification(duration: number | null) {
587
- return await this.getClient().post<AttachmentResponse<ErmisChatGenerics>>(
588
- this.getClient().baseURL + `/channels/${this.type}/${this.id}/muted`,
589
- { mute: true, duration },
590
- );
591
- }
592
-
593
- async unMuteNotification() {
594
- return await this.getClient().post<AttachmentResponse<ErmisChatGenerics>>(
595
- this.getClient().baseURL + `/channels/${this.type}/${this.id}/muted`,
596
- { mute: false },
597
- );
598
- }
599
-
600
- async keystroke(parent_id?: string, options?: { user_id: string }) {
601
- const now = new Date();
602
- const diff = this.lastTypingEvent && now.getTime() - this.lastTypingEvent.getTime();
603
- this.lastKeyStroke = now;
604
- this.isTyping = true;
605
- // send a typing.start every 2 seconds
606
- if (diff === null || diff > 2000) {
607
- this.lastTypingEvent = new Date();
608
- await this.sendEvent({
609
- type: 'typing.start',
610
- parent_id,
611
- ...(options || {}),
612
- } as Event<ErmisChatGenerics>);
613
- }
614
- }
615
-
616
- async stopTyping(parent_id?: string, options?: { user_id: string }) {
617
- if (!this.isTyping) return;
618
- this.lastTypingEvent = null;
619
- this.isTyping = false;
620
- await this.sendEvent({
621
- type: 'typing.stop',
622
- parent_id,
623
- ...(options || {}),
624
- } as Event<ErmisChatGenerics>);
625
- }
626
-
627
- _isTypingIndicatorsEnabled(): boolean {
628
- return true;
629
- }
630
-
631
- lastMessage() {
632
- let min = this.state.latestMessages.length - 5;
633
- if (min < 0) {
634
- min = 0;
635
- }
636
- const max = this.state.latestMessages.length + 1;
637
- const messageSlice = this.state.latestMessages.slice(min, max);
638
-
639
- // sort by pk desc
640
- messageSlice.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
641
-
642
- return messageSlice[0];
643
- }
644
-
645
- /**
646
- * Emits a mark-read event, updating the backend that the authenticated user has viewed up to the latest known message.
647
- * @returns Successful acknowledgement from the server.
648
- */
649
- async markRead() {
650
- return await this.getClient().post(this._channelURL() + '/read');
651
- }
652
-
653
- clean() {
654
- if (this.lastKeyStroke) {
655
- const now = new Date();
656
- const diff = now.getTime() - this.lastKeyStroke.getTime();
657
- if (diff > 1000 && this.isTyping) {
658
- logChatPromiseExecution(this.stopTyping(), 'stop typing event');
659
- }
660
- }
661
-
662
- this.state.clean();
663
- }
664
-
665
- /**
666
- * Subscribes to realtime events (WebSocket) for this channel, grabs the latest available metadata,
667
- * loads the most recent messages, and initializes the local state.
668
- *
669
- * @param options - Pagination limits like `{ watch: true, presence: true, state: true }`.
670
- * @returns The synchronized comprehensive channel state.
671
- */
672
- async watch(options?: ChannelQueryOptions) {
673
- // Make sure we wait for the connect promise if there is a pending one
674
- await this.getClient().wsPromise;
675
-
676
- const combined = { ...options };
677
- const state = await this.query(combined, 'latest');
678
- this.initialized = true;
679
- // Ensure all members' user info are loaded in state.users
680
- await ensureMembersUserInfoLoaded(this.getClient(), state.channel.members);
681
-
682
- // Get the latest users after updating
683
- const users = Object.values(this.getClient().state.users);
684
- state.channel.members = enrichWithUserInfo(state.channel.members, users);
685
- state.channel.name =
686
- state.channel.type === 'messaging'
687
- ? getDirectChannelName(state.channel.members, this.getClient().userID || '')
688
- : state.channel.name;
689
- state.channel.image =
690
- state.channel.type === 'messaging'
691
- ? getDirectChannelImage(state.channel.members, this.getClient().userID || '')
692
- : state.channel.image;
693
- state.messages = enrichWithUserInfo(state.messages, users);
694
- state.pinned_messages = state.pinned_messages ? enrichWithUserInfo(state.pinned_messages, users) : [];
695
- state.read = enrichWithUserInfo(state.read || [], users);
696
-
697
- // Process topics for team channels (already handled in query, but ensuring consistency)
698
- if (this.type === 'team' && state.channel.topics_enabled) {
699
- const payload = {
700
- filter_conditions: { type: ['topic'], parent_cid: this.cid, project_id: this.getClient().projectId },
701
- sort: [],
702
- message_limit: 25,
703
- };
704
- const topicsFromApi: any = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(
705
- this.getClient().baseURL + '/channels',
706
- payload,
707
- );
708
-
709
- this._processTopics(topicsFromApi.channels || [], users);
710
- }
711
-
712
- this.data = state.channel;
713
-
714
- this._client.logger('info', `channel:watch() - started watching channel ${this.cid}`, {
715
- tags: ['channel'],
716
- channel: this,
717
- });
718
- return state;
719
- }
720
-
721
- lastRead() {
722
- const { userID } = this.getClient();
723
- if (userID) {
724
- return this.state.read[userID] ? this.state.read[userID].last_read : null;
725
- }
726
- }
727
- // TODO: KhoaKheu Add mute Users later, confict here
728
- _countMessageAsUnread(message: FormatMessageResponse<ErmisChatGenerics> | MessageResponse<ErmisChatGenerics>) {
729
- if (message.parent_id && !message.show_in_channel) return false;
730
- if (message.user?.id === this.getClient().userID) return false;
731
- if (message.type === 'system') return false;
732
-
733
- // Return false if channel doesn't allow read events.
734
- if (Array.isArray(this.data?.own_capabilities) && !this.data?.own_capabilities.includes('read-events'))
735
- return false;
736
-
737
- return true;
738
- }
739
-
740
- countUnread(lastRead?: Date | null) {
741
- if (!lastRead) return this.state.unreadCount;
742
-
743
- let count = 0;
744
- for (let i = 0; i < this.state.latestMessages.length; i += 1) {
745
- const message = this.state.latestMessages[i];
746
- if (message.created_at > lastRead && this._countMessageAsUnread(message)) {
747
- count++;
748
- }
749
- }
750
- return count;
751
- }
752
-
753
- getUnreadMemberCount() {
754
- if (!this.state.read) return [];
755
-
756
- return Object.values(this.state.read);
757
- }
758
-
759
- getCapabilitiesMember() {
760
- if (!this.data) return [];
761
-
762
- return this.data.member_capabilities;
763
- }
764
-
765
- create = async () => {
766
- if (this.type === 'messaging') {
767
- return await this.createDirectChannel('latest');
768
- } else {
769
- return await this.query({}, 'latest');
770
- }
771
- };
772
-
773
- async createTopic(data: CreateTopicData) {
774
- const project_id = this._client.projectId;
775
- const uuid = randomId();
776
- const topicID = `${project_id}:${uuid}`;
777
-
778
- const queryURL = `${this.getClient().baseURL}/channels/topic/${topicID}`;
779
- const payload: any = {
780
- project_id,
781
- parent_cid: this.cid,
782
- data: { ...data },
783
- };
784
-
785
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', payload);
786
-
787
- return state;
788
- }
789
-
790
- async query(options: ChannelQueryOptions, messageSetToAddToIfDoesNotExist: MessageSetType = 'current') {
791
- // Make sure we wait for the connect promise if there is a pending one
792
- await this.getClient().wsPromise;
793
-
794
- let project_id = this._client.projectId;
795
- let update_options = { ...options, project_id };
796
-
797
- let queryURL = `${this.getClient().baseURL}/channels/${this.type}`;
798
- if (this.id) {
799
- queryURL += `/${this.id}`;
800
- } else {
801
- if (this.type === 'team' || this.type === 'meeting') {
802
- const uuid = randomId();
803
- this.id = `${project_id}:${uuid}`;
804
- queryURL += `/${this.id}`;
805
- }
806
- }
807
-
808
- const payload: any = {
809
- state: true,
810
- ...update_options,
811
- };
812
-
813
- if (this._data && Object.keys(this._data).length > 0) {
814
- payload.data = this._data;
815
- }
816
-
817
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', payload);
818
- // Ensure all members' user info are loaded in state.users
819
- await ensureMembersUserInfoLoaded(this.getClient(), state.channel.members);
820
- const users = Object.values(this.getClient().state.users);
821
- state.channel.members = enrichWithUserInfo(state.channel.members, users);
822
- state.channel.name =
823
- state.channel.type === 'messaging'
824
- ? getDirectChannelName(state.channel.members, this.getClient().userID || '')
825
- : state.channel.name;
826
- state.channel.image =
827
- state.channel.type === 'messaging'
828
- ? getDirectChannelImage(state.channel.members, this.getClient().userID || '')
829
- : state.channel.image;
830
- state.messages = enrichWithUserInfo(state.messages, users);
831
- state.pinned_messages = state.pinned_messages ? enrichWithUserInfo(state.pinned_messages, users) : [];
832
- state.read = enrichWithUserInfo(state.read || [], users);
833
- state.channel.is_pinned = state.is_pinned || false;
834
-
835
- // Process topics for team channels
836
- // if (this.type === 'team' && state.channel.topics_enabled && state.topics) {
837
- // this._processTopics(state.topics, users);
838
- // }
839
-
840
- // update the channel id if it was missing
841
-
842
- // update the channel id if it was missing or temporary
843
- const oldCid = this.cid;
844
- if (oldCid !== state.channel.cid) {
845
- this.id = state.channel.id;
846
- this.cid = state.channel.cid;
847
-
848
- if (oldCid in this.getClient().activeChannels) {
849
- delete this.getClient().activeChannels[oldCid];
850
- }
851
-
852
- // set the channel as active...
853
- const membersStr = state.channel.members
854
- .map((member) => member.user_id || member.user?.id)
855
- .sort()
856
- .join(',');
857
- const tempChannelCid = `${this.type}:!members-${membersStr}`;
858
-
859
- if (tempChannelCid in this.getClient().activeChannels) {
860
- // This gets set in `client.channel()` function, when channel is created
861
- // using members, not id.
862
- delete this.getClient().activeChannels[tempChannelCid];
863
- }
864
-
865
- if (!(this.cid in this.getClient().activeChannels)) {
866
- this.getClient().activeChannels[this.cid] = this;
867
- }
868
- } else if (!(this.cid in this.getClient().activeChannels)) {
869
- this.getClient().activeChannels[this.cid] = this;
870
- }
871
-
872
- // add any messages to our channel state
873
- const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
874
-
875
- const areCapabilitiesChanged =
876
- [...(state.channel.own_capabilities || [])].sort().join() !==
877
- [...(Array.isArray(this.data?.own_capabilities) ? (this.data?.own_capabilities as string[]) : [])].sort().join();
878
- this.data = state.channel;
879
- this.offlineMode = false;
880
-
881
- if (areCapabilitiesChanged) {
882
- this.getClient().dispatchEvent({
883
- type: 'capabilities.changed',
884
- cid: this.cid,
885
- own_capabilities: state.channel.own_capabilities,
886
- });
887
- }
888
-
889
- return state;
890
- }
891
-
892
- async createDirectChannel(messageSetToAddToIfDoesNotExist: MessageSetType = 'current') {
893
- // Make sure we wait for the connect promise if there is a pending one
894
- await this.getClient().wsPromise;
895
-
896
- const project_id = this._client.projectId;
897
-
898
- const queryURL = `${this.getClient().baseURL}/channels/${this.type}`;
899
-
900
- const payload: any = {
901
- project_id,
902
- };
903
-
904
- if (this._data && Object.keys(this._data).length > 0) {
905
- payload.data = this._data;
906
- }
907
-
908
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', payload);
909
-
910
- const users = Object.values(this.getClient().state.users);
911
- state.channel.members = enrichWithUserInfo(state.channel.members, users);
912
- state.channel.name =
913
- state.channel.type === 'messaging'
914
- ? getDirectChannelName(state.channel.members, this.getClient().userID || '')
915
- : state.channel.name;
916
- state.messages = enrichWithUserInfo(state.messages, users);
917
- state.pinned_messages = state.pinned_messages ? enrichWithUserInfo(state.pinned_messages, users) : [];
918
- state.read = enrichWithUserInfo(state.read || [], users);
919
-
920
- // add any messages to our channel state
921
- const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
922
-
923
- const areCapabilitiesChanged =
924
- [...(state.channel.own_capabilities || [])].sort().join() !==
925
- [...(Array.isArray(this.data?.own_capabilities) ? (this.data?.own_capabilities as string[]) : [])].sort().join();
926
- this.data = state.channel;
927
- this.offlineMode = false;
928
-
929
- if (areCapabilitiesChanged) {
930
- this.getClient().dispatchEvent({
931
- type: 'capabilities.changed',
932
- cid: state.channel.cid,
933
- own_capabilities: state.channel.own_capabilities,
934
- });
935
- }
936
-
937
- return state;
938
- }
939
-
940
- async queryMessagesLessThanId(message_id: string, limit: number = 25) {
941
- await this.getClient().wsPromise;
942
-
943
- let project_id = this._client.projectId;
944
- let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
945
-
946
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
947
- // data: this._data,
948
- state: true,
949
- project_id,
950
- messages: { limit, id_lt: message_id },
951
- });
952
-
953
- const users = Object.values(this.getClient().state.users);
954
- state.messages = enrichWithUserInfo(state.messages, users);
955
- if (state.messages && state.messages.length > 0) {
956
- for (const msg of state.messages) {
957
- if (!msg.pinned) {
958
- const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
959
- if (pm) {
960
- msg.pinned = true;
961
- const pmDate = pm.pinned_at || new Date();
962
- msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
963
- }
964
- }
965
- }
966
- this.state.addMessagesSorted(state.messages, false, true, true, 'current');
967
- }
968
- return state.messages;
969
- }
970
-
971
- async queryMessagesGreaterThanId(message_id: string, limit: number = 25) {
972
- await this.getClient().wsPromise;
973
-
974
- let project_id = this._client.projectId;
975
- let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
976
-
977
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
978
- // data: this._data,
979
- state: true,
980
- project_id,
981
- messages: { limit, id_gt: message_id },
982
- });
983
-
984
- const users = Object.values(this.getClient().state.users);
985
- state.messages = enrichWithUserInfo(state.messages, users);
986
- if (state.messages && state.messages.length > 0) {
987
- for (const msg of state.messages) {
988
- if (!msg.pinned) {
989
- const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
990
- if (pm) {
991
- msg.pinned = true;
992
- const pmDate = pm.pinned_at || new Date();
993
- msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
994
- }
995
- }
996
- }
997
- this.state.addMessagesSorted(state.messages, false, true, true, 'current');
998
- }
999
- return state.messages;
1000
- }
1001
-
1002
- async queryMessagesAroundId(message_id: string, limit: number = 25) {
1003
- await this.getClient().wsPromise;
1004
-
1005
- let project_id = this._client.projectId;
1006
- let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
1007
-
1008
- const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
1009
- // data: this._data,
1010
- state: true,
1011
- project_id,
1012
- messages: { limit, id_around: message_id },
1013
- });
1014
-
1015
- const users = Object.values(this.getClient().state.users);
1016
- state.messages = enrichWithUserInfo(state.messages, users);
1017
- if (state.messages && state.messages.length > 0) {
1018
- for (const msg of state.messages) {
1019
- if (!msg.pinned) {
1020
- const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
1021
- if (pm) {
1022
- msg.pinned = true;
1023
- const pmDate = pm.pinned_at || new Date();
1024
- msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
1025
- }
1026
- }
1027
- }
1028
- this.state.addMessagesSorted(state.messages, false, true, true, 'current');
1029
- }
1030
- return state.messages;
1031
- }
1032
-
1033
- async deleteMessage(messageId: string) {
1034
- return await this.getClient().delete<APIResponse & { message: MessageResponse<ErmisChatGenerics> }>(
1035
- this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageId}`,
1036
- );
1037
- }
1038
-
1039
- async deleteMessageForMe(messageId: string) {
1040
- return await this.getClient().delete<APIResponse & { message: MessageResponse<ErmisChatGenerics> }>(
1041
- this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageId}`,
1042
- { for_me: true },
1043
- );
1044
- }
1045
-
1046
- async getThumbBlobVideo(file: File): Promise<Blob | null> {
1047
- return new Promise((resolve) => {
1048
- const videoPlayer = document.createElement('video');
1049
- videoPlayer.src = URL.createObjectURL(file);
1050
- videoPlayer.crossOrigin = 'anonymous';
1051
- videoPlayer.muted = true; // Đảm bảo không phát tiếng nếu browser tự phát
1052
- videoPlayer.load();
1053
-
1054
- let attempts = 0;
1055
- const maxAttempts = 5;
1056
- const seekInterval = 1.0; // Nhảy mỗi lần 1 giây nếu gặp ảnh đen
1057
-
1058
- const cleanup = () => {
1059
- URL.revokeObjectURL(videoPlayer.src);
1060
- videoPlayer.remove();
1061
- };
1062
-
1063
- videoPlayer.addEventListener('error', () => {
1064
- console.error('Error when loading video file.');
1065
- cleanup();
1066
- resolve(null);
1067
- });
1068
-
1069
- const captureFrame = () => {
1070
- try {
1071
- const canvas = document.createElement('canvas');
1072
- canvas.width = videoPlayer.videoWidth;
1073
- canvas.height = videoPlayer.videoHeight;
1074
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
1075
-
1076
- if (!ctx) {
1077
- console.error('Failed to create canvas context.');
1078
- cleanup();
1079
- resolve(null);
1080
- return;
1081
- }
1082
-
1083
- ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height);
1084
-
1085
- // Kiểm tra xem có phải khung hình đen không
1086
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1087
- const data = imageData.data;
1088
- let totalLuminance = 0;
1089
- const sampleStep = 40; // Lấy mẫu để tối ưu hiệu năng
1090
- let samples = 0;
1091
-
1092
- for (let i = 0; i < data.length; i += sampleStep * 4) {
1093
- const r = data[i];
1094
- const g = data[i + 1];
1095
- const b = data[i + 2];
1096
- // Công thức tính độ sáng tiêu chuẩn (ITU-R BT.709)
1097
- totalLuminance += 0.2126 * r + 0.7152 * g + 0.0722 * b;
1098
- samples++;
1099
- }
1100
-
1101
- const avgLuminance = totalLuminance / samples;
1102
-
1103
- // Nếu ảnh quá tối (đen) và vẫn còn lượt thử, nhảy tiếp
1104
- if (
1105
- avgLuminance < 15 &&
1106
- attempts < maxAttempts &&
1107
- videoPlayer.currentTime + seekInterval < videoPlayer.duration
1108
- ) {
1109
- attempts++;
1110
- videoPlayer.currentTime += seekInterval;
1111
- return; // Đợi sự kiện 'seeked' tiếp theo
1112
- }
1113
-
1114
- // Xuất kết quả nếu ảnh ok hoặc đã hết lượt thử
1115
- canvas.toBlob(
1116
- (blob) => {
1117
- cleanup();
1118
- if (!blob) {
1119
- console.error('Failed to generate thumbnail.');
1120
- resolve(null);
1121
- return;
1122
- }
1123
- resolve(blob);
1124
- },
1125
- 'image/jpeg',
1126
- 0.75,
1127
- );
1128
- } catch (error) {
1129
- console.error('Error while extracting thumbnail:', error);
1130
- cleanup();
1131
- resolve(null);
1132
- }
1133
- };
1134
-
1135
- videoPlayer.addEventListener('loadedmetadata', () => {
1136
- // Bắt đầu từ giây thứ 0.5 để tránh đoạn khởi đầu thường bị lỗi encoder
1137
- videoPlayer.currentTime = Math.min(0.5, videoPlayer.duration);
1138
- });
1139
-
1140
- videoPlayer.addEventListener('seeked', captureFrame);
1141
- });
1142
- }
1143
-
1144
- async enableTopics() {
1145
- return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/enable`, {
1146
- project_id: this.getClient().projectId,
1147
- messages: { limit: 25 },
1148
- });
1149
- }
1150
-
1151
- async disableTopics() {
1152
- return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/disable`, {
1153
- project_id: this.getClient().projectId,
1154
- });
1155
- }
1156
-
1157
- async closeTopic(topicCID: string) {
1158
- return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/close`, {
1159
- project_id: this.getClient().projectId,
1160
- topic_cid: topicCID,
1161
- });
1162
- }
1163
-
1164
- async reopenTopic(topicCID: string) {
1165
- return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/reopen`, {
1166
- project_id: this.getClient().projectId,
1167
- topic_cid: topicCID,
1168
- });
1169
- }
1170
-
1171
- async editTopic(topicCID: string, data: EditTopicData) {
1172
- const response: any = await this.getClient().post(
1173
- this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics`,
1174
- {
1175
- project_id: this.getClient().projectId,
1176
- topic_cid: topicCID,
1177
- data,
1178
- },
1179
- );
1180
-
1181
- if (response) {
1182
- const activeTopic = this.getClient().activeChannels[topicCID];
1183
-
1184
- if (activeTopic) {
1185
- activeTopic.data = response.channel;
1186
- return activeTopic.data;
1187
- } else {
1188
- return response.channel;
1189
- }
1190
- }
1191
- }
1192
-
1193
- on(eventType: EventTypes, callback: EventHandler<ErmisChatGenerics>): { unsubscribe: () => void };
1194
- on(callback: EventHandler<ErmisChatGenerics>): { unsubscribe: () => void };
1195
- on(
1196
- callbackOrString: EventHandler<ErmisChatGenerics> | EventTypes,
1197
- callbackOrNothing?: EventHandler<ErmisChatGenerics>,
1198
- ): { unsubscribe: () => void } {
1199
- const key = callbackOrNothing ? (callbackOrString as string) : 'all';
1200
- const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
1201
- if (!(key in this.listeners)) {
1202
- this.listeners[key] = [];
1203
- }
1204
- this._client.logger('info', `Attaching listener for ${key} event on channel ${this.cid}`, {
1205
- tags: ['event', 'channel'],
1206
- channel: this,
1207
- });
1208
-
1209
- this.listeners[key].push(callback);
1210
-
1211
- return {
1212
- unsubscribe: () => {
1213
- this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, {
1214
- tags: ['event', 'channel'],
1215
- channel: this,
1216
- });
1217
-
1218
- this.listeners[key] = this.listeners[key].filter((el) => el !== callback);
1219
- },
1220
- };
1221
- }
1222
-
1223
- off(eventType: EventTypes, callback: EventHandler<ErmisChatGenerics>): void;
1224
- off(callback: EventHandler<ErmisChatGenerics>): void;
1225
- off(
1226
- callbackOrString: EventHandler<ErmisChatGenerics> | EventTypes,
1227
- callbackOrNothing?: EventHandler<ErmisChatGenerics>,
1228
- ): void {
1229
- const key = callbackOrNothing ? (callbackOrString as string) : 'all';
1230
- const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
1231
- if (!(key in this.listeners)) {
1232
- this.listeners[key] = [];
1233
- }
1234
-
1235
- this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, {
1236
- tags: ['event', 'channel'],
1237
- channel: this,
1238
- });
1239
- this.listeners[key] = this.listeners[key].filter((value) => value !== callback);
1240
- }
1241
-
1242
- // eslint-disable-next-line sonarjs/cognitive-complexity
1243
- async _handleChannelEvent(event: Event<ErmisChatGenerics>) {
1244
- const channel = this;
1245
- this._client.logger(
1246
- 'info',
1247
- `channel:_handleChannelEvent - Received event of type { ${event.type} } on ${this.cid}`,
1248
- {
1249
- tags: ['event', 'channel'],
1250
- channel: this,
1251
- },
1252
- );
1253
-
1254
- const channelState = channel.state;
1255
- const users = Object.values(this.getClient().state.users);
1256
- switch (event.type) {
1257
- case 'typing.start':
1258
- if (event.user?.id) {
1259
- const user = getUserInfo(event.user.id || '', users);
1260
- event.user = user;
1261
- channelState.typing[event.user.id] = event;
1262
- }
1263
- break;
1264
- case 'typing.stop':
1265
- if (event.user?.id) {
1266
- delete channelState.typing[event.user.id];
1267
- }
1268
- break;
1269
- case 'message.read':
1270
- if (event.user?.id && event.created_at) {
1271
- const user = getUserInfo(event.user.id || '', users);
1272
- event.user = user;
1273
- channelState.read[event.user.id] = {
1274
- last_read: new Date(event.created_at),
1275
- last_read_message_id: event.last_read_message_id,
1276
- user,
1277
- unread_messages: 0,
1278
- };
1279
-
1280
- if (event.user?.id === this.getClient().user?.id) {
1281
- channelState.unreadCount = 0;
1282
- }
1283
- }
1284
- break;
1285
- case 'user.watching.start':
1286
- if (event.user?.id) {
1287
- channelState.watchers[event.user.id] = event.user;
1288
- }
1289
- break;
1290
- case 'user.watching.stop':
1291
- if (event.user?.id) {
1292
- delete channelState.watchers[event.user.id];
1293
- }
1294
- break;
1295
- case 'message.deleted':
1296
- if (event.message) {
1297
- this._extendEventWithOwnReactions(event);
1298
- channelState.removeMessage(event.message);
1299
-
1300
- if (channelState.latestMessages.length === 0) {
1301
- this.query({ messages: { limit: 1 } })
1302
- .then(() => {
1303
- this._callChannelListeners({
1304
- type: 'channel.updated',
1305
- cid: this.cid,
1306
- channel: this.data,
1307
- } as any);
1308
- })
1309
- .catch((err) => {
1310
- this._client.logger('error', 'Failed to query for new last message after deletion', { err });
1311
- });
1312
- }
1313
-
1314
- channelState.removeQuotedMessageReferences(event.message);
1315
-
1316
- if ([...channelState.pinnedMessages].some((msg) => msg.id === event.message?.id)) {
1317
- channelState.removePinnedMessage(event.message);
1318
- }
1319
-
1320
- const msgTime = event.message.created_at ? new Date(event.message.created_at) : null;
1321
-
1322
- for (const userId in channelState.read) {
1323
- if (userId !== event.user?.id && event.message.id === channelState.read[userId].last_read_message_id) {
1324
- // Clear last_read_message_id if the deleted message is the last_read_message_id
1325
- channelState.read[userId] = { ...channelState.read[userId], last_read_message_id: undefined };
1326
- }
1327
-
1328
- // Decrement unread_messages if the deleted message was unread for this user
1329
- const userRead = channelState.read[userId];
1330
- const lastRead = userRead.last_read ? new Date(userRead.last_read) : new Date(0);
1331
-
1332
- if (msgTime && msgTime > lastRead) {
1333
- // Ensure we don't decrement if the message was sent by the user being checked
1334
- const wasSentByUser = event.message.user?.id === userId || event.message.user_id === userId;
1335
- const isSystem = event.message.type === 'system';
1336
-
1337
- if (!wasSentByUser && !isSystem) {
1338
- userRead.unread_messages = Math.max(0, userRead.unread_messages - 1);
1339
- if (userId === this.getClient().userID) {
1340
- channelState.unreadCount = Math.max(0, channelState.unreadCount - 1);
1341
- }
1342
- }
1343
- }
1344
- }
1345
- }
1346
- break;
1347
- case 'message.deleted_for_me':
1348
- if (event.message) {
1349
- // Xoá thông tin user trong event này vì nó là thông tin người thực hiện xoá (chính mình),
1350
- // tránh việc ghi đè lên tác giả gốc của tin nhắn dẫn đến sai lệch layout.
1351
- delete event.message.user;
1352
- delete (event.message as any).user_id;
1353
-
1354
- event.message.display_type = 'deleted';
1355
- event.message.pinned_at = null;
1356
- (event.message as any).updated_at = null;
1357
- event.message.status = 'received';
1358
- channelState.addMessageSorted(event.message);
1359
- channelState.removeQuotedMessageReferences(event.message);
1360
-
1361
- if ([...channelState.pinnedMessages].some((msg) => msg.id === event.message?.id)) {
1362
- channelState.removePinnedMessage(event.message);
1363
- }
1364
- }
1365
- break;
1366
- case 'message.new':
1367
- if (event.message) {
1368
- /* if message belongs to current user, always assume timestamp is changed to filter it out and add again to avoid duplication */
1369
- const ownMessage = event.user?.id === this.getClient().user?.id;
1370
- const isThreadMessage = !!event.message.parent_id;
1371
-
1372
- const existUser = users.find((user) => user.id === event.user?.id);
1373
- if (!existUser) {
1374
- if (event.user?.id) {
1375
- const resUser = await this.getClient().queryUser(event.user.id);
1376
- users.push(resUser);
1377
- }
1378
- }
1379
-
1380
- const userInfo = getUserInfo(event.user?.id || '', users);
1381
- event.message.user = userInfo;
1382
- if (event.message?.quoted_message) {
1383
- const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
1384
- event.message.quoted_message.user = quotedUser;
1385
- }
1386
- event.user = userInfo;
1387
-
1388
- if (this.state.isUpToDate || isThreadMessage) {
1389
- channelState.addMessageSorted(event.message, ownMessage);
1390
- }
1391
- // if (event.message.pinned) {
1392
- // channelState.addPinnedMessage(event.message);
1393
- // }
1394
-
1395
- // do not increase the unread count - the back-end does not increase the count neither in the following cases:
1396
- // 1. the message is mine
1397
- // 2. the message is a thread reply from any user
1398
- const preventUnreadCountUpdate = ownMessage || isThreadMessage;
1399
- if (preventUnreadCountUpdate) break;
1400
-
1401
- if (event.user?.id) {
1402
- for (const userId in channelState.read) {
1403
- if (userId === event.user.id) {
1404
- channelState.read[event.user.id] = {
1405
- last_read: new Date(event.created_at as string),
1406
- user: event.user,
1407
- unread_messages: 0,
1408
- };
1409
- } else {
1410
- channelState.read[userId].unread_messages += 1;
1411
- }
1412
- }
1413
- }
1414
-
1415
- if (this._countMessageAsUnread(event.message)) {
1416
- channelState.unreadCount = channelState.unreadCount + 1;
1417
- }
1418
- }
1419
- break;
1420
- case 'message.updated':
1421
- if (event.message) {
1422
- const userEvent = getUserInfo(event.user?.id || '', users);
1423
- const userMsg = getUserInfo(event.message.user?.id || '', users);
1424
- event.user = userEvent;
1425
- event.message.user = userMsg;
1426
-
1427
- if (event.message?.quoted_message) {
1428
- const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
1429
- event.message.quoted_message.user = quotedUser;
1430
- }
1431
-
1432
- if (event.message?.latest_reactions) {
1433
- event.message.latest_reactions = enrichWithUserInfo(event.message.latest_reactions || [], users);
1434
- }
1435
-
1436
- this._extendEventWithOwnReactions(event);
1437
- channelState.addMessageSorted(event.message, false, false);
1438
- if (event.message.pinned) {
1439
- channelState.addPinnedMessage(event.message);
1440
- } else {
1441
- channelState.removePinnedMessage(event.message);
1442
- }
1443
- }
1444
- break;
1445
- case 'message.pinned':
1446
- if (event.message) {
1447
- const user = getUserInfo(event.message.user?.id || '', users);
1448
- event.message.user = user;
1449
- channelState.addPinnedMessage(event.message);
1450
- channelState.addMessageSorted(event.message, false, false);
1451
- }
1452
- break;
1453
- case 'message.unpinned':
1454
- if (event.message) {
1455
- const user = getUserInfo(event.message.user?.id || '', users);
1456
- event.message.user = user;
1457
- channelState.removePinnedMessage(event.message);
1458
- channelState.addMessageSorted(event.message, false, false);
1459
- }
1460
- break;
1461
- case 'channel.truncate': {
1462
- const truncateDate = event.channel?.created_at || event.created_at;
1463
- if (truncateDate) {
1464
- const truncatedAt = +new Date(truncateDate);
1465
-
1466
- channelState.messageSets.forEach((messageSet, messageSetIndex) => {
1467
- messageSet.messages.forEach(({ created_at: createdAt, id }) => {
1468
- if (truncatedAt > +createdAt) channelState.removeMessage({ id, messageSetIndex });
1469
- });
1470
- });
1471
-
1472
- channelState.pinnedMessages.forEach(({ id, created_at: createdAt }) => {
1473
- if (truncatedAt > +createdAt)
1474
- channelState.removePinnedMessage({ id } as MessageResponse<ErmisChatGenerics>);
1475
- });
1476
- } else {
1477
- channelState.clearMessages();
1478
- }
1479
-
1480
-
1481
- channelState.unreadCount = 0;
1482
- // system messages don't increment unread counts
1483
- if (event.message) {
1484
- channelState.addMessageSorted(event.message);
1485
- if (event.message.pinned) {
1486
- channelState.addPinnedMessage(event.message);
1487
- }
1488
- }
1489
- break;
1490
- }
1491
- case 'member.added':
1492
- if (event.member?.user_id) {
1493
- const user = getUserInfo(event.member.user_id, users);
1494
- event.member.user = user;
1495
-
1496
- channelState.members[event.member.user_id] = event.member;
1497
-
1498
- if (event.member.user?.id === this.getClient().user?.id) {
1499
- channelState.membership = event.member;
1500
- }
1501
- }
1502
- break;
1503
- case 'member.updated':
1504
- if (event.member?.user_id) {
1505
- const user = getUserInfo(event.member.user_id, users);
1506
- event.member.user = user;
1507
- channelState.members[event.member.user_id] = event.member;
1508
- channelState.membership = event.member;
1509
- }
1510
- break;
1511
- case 'member.removed':
1512
- if (event.member?.user_id) {
1513
- delete channelState.members[event.member.user_id];
1514
- } else if (event.user?.id) {
1515
- // fallback just in case some legacy payload uses event.user for the removed user
1516
- delete channelState.members[event.user.id];
1517
- }
1518
- break;
1519
- case 'channel.topic.enabled':
1520
- if (channel.data) {
1521
- channel.data.topics_enabled = true;
1522
- }
1523
- channelState.topics = channelState.topics || [];
1524
- event.user = getUserInfo(event.user?.id || '', users);
1525
- break;
1526
- case 'channel.topic.disabled':
1527
- if (channel.data) {
1528
- channel.data.topics_enabled = false;
1529
- }
1530
- channelState.topics = [];
1531
- event.user = getUserInfo(event.user?.id || '', users);
1532
- break;
1533
- case 'channel.updated':
1534
- if (event.channel) {
1535
- channel.data = {
1536
- ...channel.data,
1537
- ...event.channel,
1538
- own_capabilities: event.channel?.own_capabilities ?? channel.data?.own_capabilities,
1539
- };
1540
- }
1541
- break;
1542
- case 'pollchoice.new':
1543
- if (event.message) {
1544
- const user = getUserInfo(event.message.user?.id || '', users);
1545
- event.message.user = user;
1546
- channelState.addMessageSorted(event.message, false, false);
1547
- }
1548
- break;
1549
- case 'reaction.new':
1550
- if (event.message && event.reaction) {
1551
- const userMsg = getUserInfo(event.message.user?.id || '', users);
1552
- const userReaction = getUserInfo(event.reaction.user?.id || '', users);
1553
- event.message.user = userMsg;
1554
- event.message.latest_reactions = enrichWithUserInfo(event.message.latest_reactions || [], users);
1555
- event.reaction.user = userReaction;
1556
- if (event.message?.quoted_message) {
1557
- const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
1558
- event.message.quoted_message.user = quotedUser;
1559
- }
1560
- event.message = channelState.addReaction(event.reaction, event.message);
1561
- }
1562
- break;
1563
- case 'reaction.deleted':
1564
- event.user = getUserInfo(event.user?.id || '', users);
1565
- if (event.message) {
1566
- if (event.message?.quoted_message) {
1567
- const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
1568
- event.message.quoted_message.user = quotedUser;
1569
- }
1570
- event.message.user = getUserInfo(event.message.user?.id || '', users);
1571
- event.message.latest_reactions?.map((item) => {
1572
- item.user = getUserInfo(item.user?.id || '', users);
1573
- return item;
1574
- });
1575
- }
1576
-
1577
- if (event.reaction) {
1578
- event.reaction.user = getUserInfo(event.reaction.user?.id || '', users);
1579
- event.message = channelState.removeReaction(event.reaction, event.message);
1580
- }
1581
- break;
1582
- case 'member.joined':
1583
- case 'notification.invite_accepted':
1584
- if (event.member?.user_id) {
1585
- const existUser = users.find((user) => user.id === event.member?.user_id);
1586
-
1587
- if (!existUser) {
1588
- const resUser = await this.getClient().queryUser(event.member?.user_id);
1589
- users.push(resUser);
1590
- }
1591
-
1592
- const user = getUserInfo(event.member.user_id, users);
1593
- event.member.user = user;
1594
-
1595
- if (event.member.user_id === this.getClient().user?.id) {
1596
- channelState.membership = event.member;
1597
- this.state.membership = event.member;
1598
- }
1599
-
1600
- channelState.members[event.member.user_id] = event.member;
1601
- channel.data = {
1602
- ...channel.data,
1603
- member_count: Number(channel.data?.member_count) + 1,
1604
- members: channel.data?.members ? [...channel.data.members, event.member] : [event.member],
1605
- } as ChannelAPIResponse<ErmisChatGenerics>['channel'];
1606
- this.offlineMode = true;
1607
- this.initialized = true;
1608
- }
1609
- break;
1610
- case 'notification.invite_rejected':
1611
- if (event.member?.user_id) {
1612
- delete channelState.members[event.member.user_id];
1613
-
1614
- // channel.data = {
1615
- // ...channel.data,
1616
- // member_count: Number(channel.data?.member_count) - 1,
1617
- // members: channel.data?.members?.filter((m: any) => m.user_id !== event.member?.user_id) || [],
1618
- // } as ChannelAPIResponse<ErmisChatGenerics>['channel'];
1619
- }
1620
- break;
1621
- case 'notification.invite_messaging_skipped':
1622
- if (event.member?.user_id) {
1623
- const user = getUserInfo(event.member.user_id, users);
1624
- event.member.user = user;
1625
-
1626
- if (event.member.user_id === this.getClient().user?.id) {
1627
- channelState.membership = event.member;
1628
- this.state.membership = event.member;
1629
- }
1630
-
1631
- channelState.members[event.member.user_id] = event.member;
1632
-
1633
- // this.offlineMode = true;
1634
- // this.initialized = true;
1635
- }
1636
- break;
1637
- case 'member.promoted':
1638
- case 'member.demoted':
1639
- case 'member.banned':
1640
- case 'member.unbanned':
1641
- case 'member.blocked':
1642
- case 'member.unblocked':
1643
- if (event.member?.user_id) {
1644
- const user = getUserInfo(event.member.user_id, users);
1645
- event.member.user = user;
1646
- channelState.members[event.member.user_id] = event.member;
1647
- if (event.member.user_id === this.getClient().user?.id) {
1648
- channelState.membership = event.member;
1649
- this.state.membership = event.member;
1650
- }
1651
- }
1652
- break;
1653
- case 'channel.pinned':
1654
- if (channel.data) {
1655
- channel.data.is_pinned = true;
1656
- }
1657
- break;
1658
- case 'channel.unpinned':
1659
- if (channel.data) {
1660
- channel.data.is_pinned = false;
1661
- }
1662
- break;
1663
-
1664
- case 'channel.topic.created':
1665
- const members = event.channel?.members || [];
1666
- const enrichedMembers = enrichWithUserInfo(members, users);
1667
-
1668
- const topicState: any = {
1669
- channel: event.channel,
1670
- members: enrichedMembers,
1671
- messages: [],
1672
- pinned_messages: [],
1673
- };
1674
- const topic = this.getClient().channel(event.channel_type || '', event.channel_id || '');
1675
- topic.data = event.channel;
1676
- topic._initializeState(topicState, 'latest');
1677
-
1678
- if (!channelState.topics) {
1679
- channelState.topics = [];
1680
- }
1681
- if (!channelState.topics.some((t) => t.cid === topic.cid)) {
1682
- channelState.topics.push(topic);
1683
- }
1684
- break;
1685
- case 'channel.topic.closed':
1686
- if (channel.data) {
1687
- channel.data.is_closed_topic = true;
1688
- }
1689
- event.user = getUserInfo(event.user?.id || '', users);
1690
- break;
1691
- case 'channel.topic.reopen':
1692
- if (channel.data) {
1693
- channel.data.is_closed_topic = false;
1694
- }
1695
- event.user = getUserInfo(event.user?.id || '', users);
1696
- break;
1697
- case 'channel.topic.updated':
1698
- if (channel.data) {
1699
- channel.data.name = event.channel?.name;
1700
- channel.data.image = event.channel?.image;
1701
- channel.data.description = event.channel?.description;
1702
- }
1703
-
1704
- event.user = getUserInfo(event.user?.id || '', users);
1705
- break;
1706
- default:
1707
- }
1708
-
1709
- // any event can send over the online count
1710
- if (event.watcher_count !== undefined) {
1711
- channel.state.watcher_count = event.watcher_count;
1712
- }
1713
- }
1714
-
1715
- _callChannelListeners = (event: Event<ErmisChatGenerics>) => {
1716
- const channel = this;
1717
- // gather and call the listeners
1718
- const listeners = [];
1719
- if (channel.listeners.all) {
1720
- listeners.push(...channel.listeners.all);
1721
- }
1722
- if (channel.listeners[event.type]) {
1723
- listeners.push(...channel.listeners[event.type]);
1724
- }
1725
-
1726
- // call the event and send it to the listeners
1727
- for (const listener of listeners) {
1728
- if (typeof listener !== 'string') {
1729
- listener(event);
1730
- }
1731
- }
1732
- };
1733
-
1734
- _channelURL = () => {
1735
- if (!this.id) {
1736
- throw new Error('channel id is not defined');
1737
- }
1738
- return `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
1739
- };
1740
-
1741
- _checkInitialized() {
1742
- if (!this.initialized && !this.offlineMode) {
1743
- throw Error(
1744
- `Channel ${this.cid} hasn't been initialized yet. Make sure to call .watch() and wait for it to resolve`,
1745
- );
1746
- }
1747
- }
1748
-
1749
- // eslint-disable-next-line sonarjs/cognitive-complexity
1750
- _initializeState(
1751
- state: ChannelAPIResponse<ErmisChatGenerics>,
1752
- messageSetToAddToIfDoesNotExist: MessageSetType = 'latest',
1753
- updateUserIds?: (id: string) => void,
1754
- ) {
1755
- const { state: clientState, user, userID } = this.getClient();
1756
- // add the Users
1757
- if (state.channel.members) {
1758
- for (const member of state.channel.members) {
1759
- if (member.user) {
1760
- if (updateUserIds) {
1761
- updateUserIds(member.user.id);
1762
- }
1763
- clientState.updateUserReference(member.user, this.cid);
1764
- }
1765
- }
1766
- }
1767
-
1768
- this.state.membership = state.membership || {};
1769
-
1770
- // Remove duplicate messages by ID
1771
- const map = new Map();
1772
- const uniqueMessages = [];
1773
-
1774
- if (!state.messages) {
1775
- state.messages = [];
1776
- }
1777
- for (const msg of state.messages) {
1778
- if (!map.has(msg.id)) {
1779
- map.set(msg.id, true);
1780
- uniqueMessages.push(msg);
1781
- }
1782
- }
1783
-
1784
- if (this.state.pinnedMessages) {
1785
- this.state.pinnedMessages = [];
1786
- }
1787
- this.state.addPinnedMessages(state.pinned_messages || []);
1788
-
1789
- const messages = uniqueMessages || [];
1790
- if (!this.state.messages) {
1791
- this.state.initMessages();
1792
- }
1793
- const { messageSet } = this.state.addMessagesSorted(messages, false, true, true, messageSetToAddToIfDoesNotExist);
1794
-
1795
- if (state.watcher_count !== undefined) {
1796
- this.state.watcher_count = state.watcher_count;
1797
- }
1798
- // NOTE: we don't send the watchers with the channel data anymore
1799
- // // convert the arrays into objects for easier syncing...
1800
- if (state.watchers) {
1801
- for (const watcher of state.watchers) {
1802
- if (watcher) {
1803
- clientState.updateUserReference(watcher, this.cid);
1804
- this.state.watchers[watcher.id] = watcher;
1805
- }
1806
- }
1807
- }
1808
-
1809
- // initialize read state to last message or current time if the channel is empty
1810
- // if the user is a member, this value will be overwritten later on otherwise this ensures
1811
- // that everything up to this point is not marked as unread
1812
- if (userID != null) {
1813
- const last_read = this.state.last_message_at || new Date();
1814
- if (user) {
1815
- this.state.read[user.id] = {
1816
- user,
1817
- last_read,
1818
- unread_messages: 0,
1819
- };
1820
- }
1821
- }
1822
-
1823
- // apply read state if part of the state
1824
- if (state.read) {
1825
- for (const read of state.read) {
1826
- this.state.read[read.user.id] = {
1827
- last_read: new Date(read.last_read),
1828
- last_read_message_id: read.last_read_message_id,
1829
- unread_messages: read.unread_messages ?? 0,
1830
- user: read.user,
1831
- last_send: read.last_send,
1832
- };
1833
-
1834
- if (read.user.id === user?.id) {
1835
- this.state.unreadCount = this.state.read[read.user.id].unread_messages;
1836
- }
1837
- }
1838
- }
1839
-
1840
- if (state.channel.members) {
1841
- this.state.members = state.channel.members.reduce((acc, member) => {
1842
- if (member.user) {
1843
- acc[member.user.id] = member;
1844
- }
1845
- return acc;
1846
- }, {} as ChannelState<ErmisChatGenerics>['members']);
1847
- }
1848
-
1849
- // Process topics for team channels
1850
- if (state.channel.type === 'team' && state.channel.topics_enabled && state.topics) {
1851
- const users = Object.values(this.getClient().state.users);
1852
- this._processTopics(state.topics, users);
1853
- }
1854
-
1855
- return {
1856
- messageSet,
1857
- };
1858
- }
1859
-
1860
- _extendEventWithOwnReactions(event: Event<ErmisChatGenerics>) {
1861
- if (!event.message) {
1862
- return;
1863
- }
1864
- const message = this.state.findMessage(event.message.id, event.message.parent_id);
1865
- if (message) {
1866
- event.message.own_reactions = message.own_reactions;
1867
- }
1868
- }
1869
-
1870
- _disconnect() {
1871
- this._client.logger('info', `channel:disconnect() - Disconnecting the channel ${this.cid}`, {
1872
- tags: ['connection', 'channel'],
1873
- channel: this,
1874
- });
1875
-
1876
- this.disconnected = true;
1877
- this.state.setIsUpToDate(false);
1878
- }
1879
- }