@ermis-network/ermis-chat-sdk 1.0.9 → 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.
- package/README.md +330 -0
- package/bin/init-call.js +9 -0
- package/dist/encryption/index.browser.cjs +13045 -0
- package/dist/encryption/index.browser.cjs.map +1 -0
- package/dist/encryption/index.browser.mjs +12959 -0
- package/dist/encryption/index.browser.mjs.map +1 -0
- package/dist/encryption/index.cjs +13045 -0
- package/dist/encryption/index.cjs.map +1 -0
- package/dist/encryption/index.d.mts +3 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.mjs +12959 -0
- package/dist/encryption/index.mjs.map +1 -0
- package/dist/index-CcvHIY5q.d.mts +4988 -0
- package/dist/index-CcvHIY5q.d.ts +4988 -0
- package/dist/index.browser.cjs +20399 -6823
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.full-bundle.min.js +20 -18
- package/dist/index.browser.full-bundle.min.js.map +1 -1
- package/dist/index.browser.mjs +20315 -6790
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +20400 -6824
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +167 -1356
- package/dist/index.d.ts +167 -1356
- package/dist/index.mjs +20312 -6787
- package/dist/index.mjs.map +1 -1
- package/dist/wasm_worker.worker.mjs +1600 -0
- package/dist/wasm_worker.worker.mjs.map +1 -0
- package/package.json +22 -7
- package/public/e2ee-media-stream-worker.js +627 -0
- package/public/ermis_call_node_wasm_bg.wasm +0 -0
- package/public/openmls_wasm_bg.wasm +0 -0
- package/src/attachment_utils.ts +0 -148
- package/src/auth.ts +0 -352
- package/src/channel.ts +0 -1806
- package/src/channel_state.ts +0 -607
- package/src/client.ts +0 -1617
- package/src/client_state.ts +0 -55
- package/src/connection.ts +0 -587
- package/src/ermis_call_node.ts +0 -978
- package/src/errors.ts +0 -60
- package/src/events.ts +0 -46
- package/src/hevc_decoder_config.ts +0 -305
- package/src/index.ts +0 -16
- package/src/media_stream_receiver.ts +0 -525
- package/src/media_stream_sender.ts +0 -400
- package/src/shims/empty.ts +0 -1
- package/src/signal_message.ts +0 -146
- package/src/system_message.ts +0 -117
- package/src/token_manager.ts +0 -48
- package/src/types.ts +0 -581
- package/src/utils.ts +0 -534
- package/src/wasm/ermis_call_node_wasm.d.ts +0 -154
- package/src/wasm/ermis_call_node_wasm.js +0 -1498
package/src/channel.ts
DELETED
|
@@ -1,1806 +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
|
-
if (!this.id) {
|
|
843
|
-
this.id = state.channel.id;
|
|
844
|
-
this.cid = state.channel.cid;
|
|
845
|
-
|
|
846
|
-
// set the channel as active...
|
|
847
|
-
const membersStr = state.channel.members
|
|
848
|
-
.map((member) => member.user_id || member.user?.id)
|
|
849
|
-
.sort()
|
|
850
|
-
.join(',');
|
|
851
|
-
const tempChannelCid = `${this.type}:!members-${membersStr}`;
|
|
852
|
-
|
|
853
|
-
if (tempChannelCid in this.getClient().activeChannels) {
|
|
854
|
-
// This gets set in `client.channel()` function, when channel is created
|
|
855
|
-
// using members, not id.
|
|
856
|
-
delete this.getClient().activeChannels[tempChannelCid];
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (!(this.cid in this.getClient().activeChannels)) {
|
|
860
|
-
this.getClient().activeChannels[this.cid] = this;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// add any messages to our channel state
|
|
865
|
-
const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
|
|
866
|
-
|
|
867
|
-
const areCapabilitiesChanged =
|
|
868
|
-
[...(state.channel.own_capabilities || [])].sort().join() !==
|
|
869
|
-
[...(Array.isArray(this.data?.own_capabilities) ? (this.data?.own_capabilities as string[]) : [])].sort().join();
|
|
870
|
-
this.data = state.channel;
|
|
871
|
-
this.offlineMode = false;
|
|
872
|
-
|
|
873
|
-
if (areCapabilitiesChanged) {
|
|
874
|
-
this.getClient().dispatchEvent({
|
|
875
|
-
type: 'capabilities.changed',
|
|
876
|
-
cid: this.cid,
|
|
877
|
-
own_capabilities: state.channel.own_capabilities,
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
return state;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
async createDirectChannel(messageSetToAddToIfDoesNotExist: MessageSetType = 'current') {
|
|
885
|
-
// Make sure we wait for the connect promise if there is a pending one
|
|
886
|
-
await this.getClient().wsPromise;
|
|
887
|
-
|
|
888
|
-
const project_id = this._client.projectId;
|
|
889
|
-
|
|
890
|
-
const queryURL = `${this.getClient().baseURL}/channels/${this.type}`;
|
|
891
|
-
|
|
892
|
-
const payload: any = {
|
|
893
|
-
project_id,
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
if (this._data && Object.keys(this._data).length > 0) {
|
|
897
|
-
payload.data = this._data;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', payload);
|
|
901
|
-
|
|
902
|
-
const users = Object.values(this.getClient().state.users);
|
|
903
|
-
state.channel.members = enrichWithUserInfo(state.channel.members, users);
|
|
904
|
-
state.channel.name =
|
|
905
|
-
state.channel.type === 'messaging'
|
|
906
|
-
? getDirectChannelName(state.channel.members, this.getClient().userID || '')
|
|
907
|
-
: state.channel.name;
|
|
908
|
-
state.messages = enrichWithUserInfo(state.messages, users);
|
|
909
|
-
state.pinned_messages = state.pinned_messages ? enrichWithUserInfo(state.pinned_messages, users) : [];
|
|
910
|
-
state.read = enrichWithUserInfo(state.read || [], users);
|
|
911
|
-
|
|
912
|
-
// add any messages to our channel state
|
|
913
|
-
const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
|
|
914
|
-
|
|
915
|
-
const areCapabilitiesChanged =
|
|
916
|
-
[...(state.channel.own_capabilities || [])].sort().join() !==
|
|
917
|
-
[...(Array.isArray(this.data?.own_capabilities) ? (this.data?.own_capabilities as string[]) : [])].sort().join();
|
|
918
|
-
this.data = state.channel;
|
|
919
|
-
this.offlineMode = false;
|
|
920
|
-
|
|
921
|
-
if (areCapabilitiesChanged) {
|
|
922
|
-
this.getClient().dispatchEvent({
|
|
923
|
-
type: 'capabilities.changed',
|
|
924
|
-
cid: state.channel.cid,
|
|
925
|
-
own_capabilities: state.channel.own_capabilities,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
return state;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
async queryMessagesLessThanId(message_id: string, limit: number = 25) {
|
|
933
|
-
await this.getClient().wsPromise;
|
|
934
|
-
|
|
935
|
-
let project_id = this._client.projectId;
|
|
936
|
-
let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
|
|
937
|
-
|
|
938
|
-
const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
|
|
939
|
-
// data: this._data,
|
|
940
|
-
state: true,
|
|
941
|
-
project_id,
|
|
942
|
-
messages: { limit, id_lt: message_id },
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
const users = Object.values(this.getClient().state.users);
|
|
946
|
-
state.messages = enrichWithUserInfo(state.messages, users);
|
|
947
|
-
if (state.messages && state.messages.length > 0) {
|
|
948
|
-
for (const msg of state.messages) {
|
|
949
|
-
if (!msg.pinned) {
|
|
950
|
-
const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
|
|
951
|
-
if (pm) {
|
|
952
|
-
msg.pinned = true;
|
|
953
|
-
const pmDate = pm.pinned_at || new Date();
|
|
954
|
-
msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
this.state.addMessagesSorted(state.messages, false, true, true, 'current');
|
|
959
|
-
}
|
|
960
|
-
return state.messages;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
async queryMessagesGreaterThanId(message_id: string, limit: number = 25) {
|
|
964
|
-
await this.getClient().wsPromise;
|
|
965
|
-
|
|
966
|
-
let project_id = this._client.projectId;
|
|
967
|
-
let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
|
|
968
|
-
|
|
969
|
-
const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
|
|
970
|
-
// data: this._data,
|
|
971
|
-
state: true,
|
|
972
|
-
project_id,
|
|
973
|
-
messages: { limit, id_gt: message_id },
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
const users = Object.values(this.getClient().state.users);
|
|
977
|
-
state.messages = enrichWithUserInfo(state.messages, users);
|
|
978
|
-
if (state.messages && state.messages.length > 0) {
|
|
979
|
-
for (const msg of state.messages) {
|
|
980
|
-
if (!msg.pinned) {
|
|
981
|
-
const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
|
|
982
|
-
if (pm) {
|
|
983
|
-
msg.pinned = true;
|
|
984
|
-
const pmDate = pm.pinned_at || new Date();
|
|
985
|
-
msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
this.state.addMessagesSorted(state.messages, false, true, true, 'current');
|
|
990
|
-
}
|
|
991
|
-
return state.messages;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
async queryMessagesAroundId(message_id: string, limit: number = 25) {
|
|
995
|
-
await this.getClient().wsPromise;
|
|
996
|
-
|
|
997
|
-
let project_id = this._client.projectId;
|
|
998
|
-
let queryURL = `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
|
|
999
|
-
|
|
1000
|
-
const state = await this.getClient().post<QueryChannelAPIResponse<ErmisChatGenerics>>(queryURL + '/query', {
|
|
1001
|
-
// data: this._data,
|
|
1002
|
-
state: true,
|
|
1003
|
-
project_id,
|
|
1004
|
-
messages: { limit, id_around: message_id },
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
const users = Object.values(this.getClient().state.users);
|
|
1008
|
-
state.messages = enrichWithUserInfo(state.messages, users);
|
|
1009
|
-
if (state.messages && state.messages.length > 0) {
|
|
1010
|
-
for (const msg of state.messages) {
|
|
1011
|
-
if (!msg.pinned) {
|
|
1012
|
-
const pm = this.state.pinnedMessages?.find((p) => p.id === msg.id);
|
|
1013
|
-
if (pm) {
|
|
1014
|
-
msg.pinned = true;
|
|
1015
|
-
const pmDate = pm.pinned_at || new Date();
|
|
1016
|
-
msg.pinned_at = typeof pmDate === 'string' ? pmDate : pmDate.toISOString();
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
this.state.addMessagesSorted(state.messages, false, true, true, 'current');
|
|
1021
|
-
}
|
|
1022
|
-
return state.messages;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
async deleteMessage(messageId: string) {
|
|
1026
|
-
return await this.getClient().delete<APIResponse & { message: MessageResponse<ErmisChatGenerics> }>(
|
|
1027
|
-
this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageId}`,
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async deleteMessageForMe(messageId: string) {
|
|
1032
|
-
return await this.getClient().delete<APIResponse & { message: MessageResponse<ErmisChatGenerics> }>(
|
|
1033
|
-
this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageId}`,
|
|
1034
|
-
{ for_me: true },
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
async getThumbBlobVideo(file: File): Promise<Blob | null> {
|
|
1039
|
-
return new Promise((resolve) => {
|
|
1040
|
-
const seekTo = 0.1;
|
|
1041
|
-
const videoPlayer = document.createElement('video');
|
|
1042
|
-
videoPlayer.src = URL.createObjectURL(file);
|
|
1043
|
-
videoPlayer.crossOrigin = 'anonymous'; // Tránh lỗi CORS nếu cần
|
|
1044
|
-
videoPlayer.load();
|
|
1045
|
-
|
|
1046
|
-
videoPlayer.addEventListener('error', () => {
|
|
1047
|
-
console.error('Error when loading video file.');
|
|
1048
|
-
resolve(null);
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
videoPlayer.addEventListener('loadedmetadata', () => {
|
|
1052
|
-
if (videoPlayer.duration < seekTo) {
|
|
1053
|
-
console.error('Video is too short.');
|
|
1054
|
-
resolve(null);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
setTimeout(() => {
|
|
1059
|
-
videoPlayer.currentTime = seekTo;
|
|
1060
|
-
}, 200);
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
videoPlayer.addEventListener('seeked', () => {
|
|
1064
|
-
try {
|
|
1065
|
-
const canvas = document.createElement('canvas');
|
|
1066
|
-
canvas.width = videoPlayer.videoWidth;
|
|
1067
|
-
canvas.height = videoPlayer.videoHeight;
|
|
1068
|
-
const ctx = canvas.getContext('2d');
|
|
1069
|
-
|
|
1070
|
-
if (!ctx) {
|
|
1071
|
-
console.error('Failed to create canvas context.');
|
|
1072
|
-
resolve(null);
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height);
|
|
1077
|
-
|
|
1078
|
-
ctx.canvas.toBlob(
|
|
1079
|
-
(blob) => {
|
|
1080
|
-
if (!blob) {
|
|
1081
|
-
console.error('Failed to generate thumbnail.');
|
|
1082
|
-
resolve(null);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
resolve(blob);
|
|
1086
|
-
URL.revokeObjectURL(videoPlayer.src); // Giải phóng bộ nhớ
|
|
1087
|
-
},
|
|
1088
|
-
'image/jpeg',
|
|
1089
|
-
0.75,
|
|
1090
|
-
);
|
|
1091
|
-
} catch (error) {
|
|
1092
|
-
console.error('Error while extracting thumbnail:', error);
|
|
1093
|
-
resolve(null);
|
|
1094
|
-
}
|
|
1095
|
-
});
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
async enableTopics() {
|
|
1100
|
-
return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/enable`, {
|
|
1101
|
-
project_id: this.getClient().projectId,
|
|
1102
|
-
messages: { limit: 25 },
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
async disableTopics() {
|
|
1107
|
-
return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/disable`, {
|
|
1108
|
-
project_id: this.getClient().projectId,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async closeTopic(topicCID: string) {
|
|
1113
|
-
return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/close`, {
|
|
1114
|
-
project_id: this.getClient().projectId,
|
|
1115
|
-
topic_cid: topicCID,
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
async reopenTopic(topicCID: string) {
|
|
1120
|
-
return await this.getClient().post(this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics/reopen`, {
|
|
1121
|
-
project_id: this.getClient().projectId,
|
|
1122
|
-
topic_cid: topicCID,
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
async editTopic(topicCID: string, data: EditTopicData) {
|
|
1127
|
-
const response: any = await this.getClient().post(
|
|
1128
|
-
this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics`,
|
|
1129
|
-
{
|
|
1130
|
-
project_id: this.getClient().projectId,
|
|
1131
|
-
topic_cid: topicCID,
|
|
1132
|
-
data,
|
|
1133
|
-
},
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
if (response) {
|
|
1137
|
-
const activeTopic = this.getClient().activeChannels[topicCID];
|
|
1138
|
-
|
|
1139
|
-
if (activeTopic) {
|
|
1140
|
-
activeTopic.data = response.channel;
|
|
1141
|
-
return activeTopic.data;
|
|
1142
|
-
} else {
|
|
1143
|
-
return response.channel;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
on(eventType: EventTypes, callback: EventHandler<ErmisChatGenerics>): { unsubscribe: () => void };
|
|
1149
|
-
on(callback: EventHandler<ErmisChatGenerics>): { unsubscribe: () => void };
|
|
1150
|
-
on(
|
|
1151
|
-
callbackOrString: EventHandler<ErmisChatGenerics> | EventTypes,
|
|
1152
|
-
callbackOrNothing?: EventHandler<ErmisChatGenerics>,
|
|
1153
|
-
): { unsubscribe: () => void } {
|
|
1154
|
-
const key = callbackOrNothing ? (callbackOrString as string) : 'all';
|
|
1155
|
-
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
1156
|
-
if (!(key in this.listeners)) {
|
|
1157
|
-
this.listeners[key] = [];
|
|
1158
|
-
}
|
|
1159
|
-
this._client.logger('info', `Attaching listener for ${key} event on channel ${this.cid}`, {
|
|
1160
|
-
tags: ['event', 'channel'],
|
|
1161
|
-
channel: this,
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
this.listeners[key].push(callback);
|
|
1165
|
-
|
|
1166
|
-
return {
|
|
1167
|
-
unsubscribe: () => {
|
|
1168
|
-
this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, {
|
|
1169
|
-
tags: ['event', 'channel'],
|
|
1170
|
-
channel: this,
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
this.listeners[key] = this.listeners[key].filter((el) => el !== callback);
|
|
1174
|
-
},
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
off(eventType: EventTypes, callback: EventHandler<ErmisChatGenerics>): void;
|
|
1179
|
-
off(callback: EventHandler<ErmisChatGenerics>): void;
|
|
1180
|
-
off(
|
|
1181
|
-
callbackOrString: EventHandler<ErmisChatGenerics> | EventTypes,
|
|
1182
|
-
callbackOrNothing?: EventHandler<ErmisChatGenerics>,
|
|
1183
|
-
): void {
|
|
1184
|
-
const key = callbackOrNothing ? (callbackOrString as string) : 'all';
|
|
1185
|
-
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
1186
|
-
if (!(key in this.listeners)) {
|
|
1187
|
-
this.listeners[key] = [];
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
this._client.logger('info', `Removing listener for ${key} event from channel ${this.cid}`, {
|
|
1191
|
-
tags: ['event', 'channel'],
|
|
1192
|
-
channel: this,
|
|
1193
|
-
});
|
|
1194
|
-
this.listeners[key] = this.listeners[key].filter((value) => value !== callback);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
1198
|
-
async _handleChannelEvent(event: Event<ErmisChatGenerics>) {
|
|
1199
|
-
const channel = this;
|
|
1200
|
-
this._client.logger(
|
|
1201
|
-
'info',
|
|
1202
|
-
`channel:_handleChannelEvent - Received event of type { ${event.type} } on ${this.cid}`,
|
|
1203
|
-
{
|
|
1204
|
-
tags: ['event', 'channel'],
|
|
1205
|
-
channel: this,
|
|
1206
|
-
},
|
|
1207
|
-
);
|
|
1208
|
-
|
|
1209
|
-
const channelState = channel.state;
|
|
1210
|
-
const users = Object.values(this.getClient().state.users);
|
|
1211
|
-
switch (event.type) {
|
|
1212
|
-
case 'typing.start':
|
|
1213
|
-
if (event.user?.id) {
|
|
1214
|
-
const user = getUserInfo(event.user.id || '', users);
|
|
1215
|
-
event.user = user;
|
|
1216
|
-
channelState.typing[event.user.id] = event;
|
|
1217
|
-
}
|
|
1218
|
-
break;
|
|
1219
|
-
case 'typing.stop':
|
|
1220
|
-
if (event.user?.id) {
|
|
1221
|
-
delete channelState.typing[event.user.id];
|
|
1222
|
-
}
|
|
1223
|
-
break;
|
|
1224
|
-
case 'message.read':
|
|
1225
|
-
if (event.user?.id && event.created_at) {
|
|
1226
|
-
const user = getUserInfo(event.user.id || '', users);
|
|
1227
|
-
event.user = user;
|
|
1228
|
-
channelState.read[event.user.id] = {
|
|
1229
|
-
last_read: new Date(event.created_at),
|
|
1230
|
-
last_read_message_id: event.last_read_message_id,
|
|
1231
|
-
user,
|
|
1232
|
-
unread_messages: 0,
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
if (event.user?.id === this.getClient().user?.id) {
|
|
1236
|
-
channelState.unreadCount = 0;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
break;
|
|
1240
|
-
case 'user.watching.start':
|
|
1241
|
-
if (event.user?.id) {
|
|
1242
|
-
channelState.watchers[event.user.id] = event.user;
|
|
1243
|
-
}
|
|
1244
|
-
break;
|
|
1245
|
-
case 'user.watching.stop':
|
|
1246
|
-
if (event.user?.id) {
|
|
1247
|
-
delete channelState.watchers[event.user.id];
|
|
1248
|
-
}
|
|
1249
|
-
break;
|
|
1250
|
-
case 'message.deleted':
|
|
1251
|
-
if (event.message) {
|
|
1252
|
-
this._extendEventWithOwnReactions(event);
|
|
1253
|
-
//! NOTE: check lai o day
|
|
1254
|
-
channelState.removeMessage(event.message);
|
|
1255
|
-
channelState.addMessageSorted(event.message, false, false);
|
|
1256
|
-
// if (event.hard_delete) channelState.removeMessage(event.message);
|
|
1257
|
-
// else channelState.addMessageSorted(event.message, false, false);
|
|
1258
|
-
|
|
1259
|
-
channelState.removeQuotedMessageReferences(event.message);
|
|
1260
|
-
|
|
1261
|
-
// if (event.message.pinned) {
|
|
1262
|
-
// channelState.removePinnedMessage(event.message);
|
|
1263
|
-
// }
|
|
1264
|
-
|
|
1265
|
-
if ([...channelState.pinnedMessages].some((msg) => msg.id === event.message?.id)) {
|
|
1266
|
-
channelState.removePinnedMessage(event.message);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
for (const userId in channelState.read) {
|
|
1270
|
-
if (userId !== event.user?.id && event.message.id === channelState.read[userId].last_read_message_id) {
|
|
1271
|
-
// Clear last_read_message_id if the deleted message is the last_read_message_id
|
|
1272
|
-
channelState.read[userId] = { ...channelState.read[userId], last_read_message_id: undefined };
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
break;
|
|
1277
|
-
case 'message.deleted_for_me':
|
|
1278
|
-
if (event.message) {
|
|
1279
|
-
channelState.removeMessage(event.message);
|
|
1280
|
-
channelState.removeQuotedMessageReferences(event.message);
|
|
1281
|
-
|
|
1282
|
-
if ([...channelState.pinnedMessages].some((msg) => msg.id === event.message?.id)) {
|
|
1283
|
-
channelState.removePinnedMessage(event.message);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
break;
|
|
1287
|
-
case 'message.new':
|
|
1288
|
-
if (event.message) {
|
|
1289
|
-
/* if message belongs to current user, always assume timestamp is changed to filter it out and add again to avoid duplication */
|
|
1290
|
-
const ownMessage = event.user?.id === this.getClient().user?.id;
|
|
1291
|
-
const isThreadMessage = !!event.message.parent_id;
|
|
1292
|
-
|
|
1293
|
-
const existUser = users.find((user) => user.id === event.user?.id);
|
|
1294
|
-
if (!existUser) {
|
|
1295
|
-
if (event.user?.id) {
|
|
1296
|
-
const resUser = await this.getClient().queryUser(event.user.id);
|
|
1297
|
-
users.push(resUser);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
const userInfo = getUserInfo(event.user?.id || '', users);
|
|
1302
|
-
event.message.user = userInfo;
|
|
1303
|
-
if (event.message?.quoted_message) {
|
|
1304
|
-
const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
|
|
1305
|
-
event.message.quoted_message.user = quotedUser;
|
|
1306
|
-
}
|
|
1307
|
-
event.user = userInfo;
|
|
1308
|
-
|
|
1309
|
-
if (this.state.isUpToDate || isThreadMessage) {
|
|
1310
|
-
channelState.addMessageSorted(event.message, ownMessage);
|
|
1311
|
-
}
|
|
1312
|
-
// if (event.message.pinned) {
|
|
1313
|
-
// channelState.addPinnedMessage(event.message);
|
|
1314
|
-
// }
|
|
1315
|
-
|
|
1316
|
-
// do not increase the unread count - the back-end does not increase the count neither in the following cases:
|
|
1317
|
-
// 1. the message is mine
|
|
1318
|
-
// 2. the message is a thread reply from any user
|
|
1319
|
-
const preventUnreadCountUpdate = ownMessage || isThreadMessage;
|
|
1320
|
-
if (preventUnreadCountUpdate) break;
|
|
1321
|
-
|
|
1322
|
-
if (event.user?.id) {
|
|
1323
|
-
for (const userId in channelState.read) {
|
|
1324
|
-
if (userId === event.user.id) {
|
|
1325
|
-
channelState.read[event.user.id] = {
|
|
1326
|
-
last_read: new Date(event.created_at as string),
|
|
1327
|
-
user: event.user,
|
|
1328
|
-
unread_messages: 0,
|
|
1329
|
-
};
|
|
1330
|
-
} else {
|
|
1331
|
-
channelState.read[userId].unread_messages += 1;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (this._countMessageAsUnread(event.message)) {
|
|
1337
|
-
channelState.unreadCount = channelState.unreadCount + 1;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
break;
|
|
1341
|
-
case 'message.updated':
|
|
1342
|
-
if (event.message) {
|
|
1343
|
-
const userEvent = getUserInfo(event.user?.id || '', users);
|
|
1344
|
-
const userMsg = getUserInfo(event.message.user?.id || '', users);
|
|
1345
|
-
event.user = userEvent;
|
|
1346
|
-
event.message.user = userMsg;
|
|
1347
|
-
|
|
1348
|
-
if (event.message?.quoted_message) {
|
|
1349
|
-
const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
|
|
1350
|
-
event.message.quoted_message.user = quotedUser;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (event.message?.latest_reactions) {
|
|
1354
|
-
event.message.latest_reactions = enrichWithUserInfo(event.message.latest_reactions || [], users);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
this._extendEventWithOwnReactions(event);
|
|
1358
|
-
channelState.addMessageSorted(event.message, false, false);
|
|
1359
|
-
if (event.message.pinned) {
|
|
1360
|
-
channelState.addPinnedMessage(event.message);
|
|
1361
|
-
} else {
|
|
1362
|
-
channelState.removePinnedMessage(event.message);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
break;
|
|
1366
|
-
case 'message.pinned':
|
|
1367
|
-
if (event.message) {
|
|
1368
|
-
const user = getUserInfo(event.message.user?.id || '', users);
|
|
1369
|
-
event.message.user = user;
|
|
1370
|
-
channelState.addPinnedMessage(event.message);
|
|
1371
|
-
channelState.addMessageSorted(event.message, false, false);
|
|
1372
|
-
}
|
|
1373
|
-
break;
|
|
1374
|
-
case 'message.unpinned':
|
|
1375
|
-
if (event.message) {
|
|
1376
|
-
const user = getUserInfo(event.message.user?.id || '', users);
|
|
1377
|
-
event.message.user = user;
|
|
1378
|
-
channelState.removePinnedMessage(event.message);
|
|
1379
|
-
channelState.addMessageSorted(event.message, false, false);
|
|
1380
|
-
}
|
|
1381
|
-
break;
|
|
1382
|
-
case 'channel.truncate':
|
|
1383
|
-
if (event.channel?.created_at) {
|
|
1384
|
-
const truncatedAt = +new Date(event.channel.created_at);
|
|
1385
|
-
|
|
1386
|
-
channelState.messageSets.forEach((messageSet, messageSetIndex) => {
|
|
1387
|
-
messageSet.messages.forEach(({ created_at: createdAt, id }) => {
|
|
1388
|
-
if (truncatedAt > +createdAt) channelState.removeMessage({ id, messageSetIndex });
|
|
1389
|
-
});
|
|
1390
|
-
});
|
|
1391
|
-
|
|
1392
|
-
channelState.pinnedMessages.forEach(({ id, created_at: createdAt }) => {
|
|
1393
|
-
if (truncatedAt > +createdAt)
|
|
1394
|
-
channelState.removePinnedMessage({ id } as MessageResponse<ErmisChatGenerics>);
|
|
1395
|
-
});
|
|
1396
|
-
} else {
|
|
1397
|
-
channelState.clearMessages();
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
channelState.unreadCount = 0;
|
|
1401
|
-
// system messages don't increment unread counts
|
|
1402
|
-
if (event.message) {
|
|
1403
|
-
channelState.addMessageSorted(event.message);
|
|
1404
|
-
if (event.message.pinned) {
|
|
1405
|
-
channelState.addPinnedMessage(event.message);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
break;
|
|
1409
|
-
case 'member.added':
|
|
1410
|
-
if (event.member?.user_id) {
|
|
1411
|
-
const user = getUserInfo(event.member.user_id, users);
|
|
1412
|
-
event.member.user = user;
|
|
1413
|
-
|
|
1414
|
-
channelState.members[event.member.user_id] = event.member;
|
|
1415
|
-
|
|
1416
|
-
if (event.member.user?.id === this.getClient().user?.id) {
|
|
1417
|
-
channelState.membership = event.member;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
break;
|
|
1421
|
-
case 'member.updated':
|
|
1422
|
-
if (event.member?.user_id) {
|
|
1423
|
-
const user = getUserInfo(event.member.user_id, users);
|
|
1424
|
-
event.member.user = user;
|
|
1425
|
-
channelState.members[event.member.user_id] = event.member;
|
|
1426
|
-
channelState.membership = event.member;
|
|
1427
|
-
}
|
|
1428
|
-
break;
|
|
1429
|
-
case 'member.removed':
|
|
1430
|
-
if (event.member?.user_id) {
|
|
1431
|
-
delete channelState.members[event.member.user_id];
|
|
1432
|
-
} else if (event.user?.id) {
|
|
1433
|
-
// fallback just in case some legacy payload uses event.user for the removed user
|
|
1434
|
-
delete channelState.members[event.user.id];
|
|
1435
|
-
}
|
|
1436
|
-
break;
|
|
1437
|
-
case 'channel.topic.enabled':
|
|
1438
|
-
if (channel.data) {
|
|
1439
|
-
channel.data.topics_enabled = true;
|
|
1440
|
-
}
|
|
1441
|
-
channelState.topics = channelState.topics || [];
|
|
1442
|
-
break;
|
|
1443
|
-
case 'channel.topic.disabled':
|
|
1444
|
-
if (channel.data) {
|
|
1445
|
-
channel.data.topics_enabled = false;
|
|
1446
|
-
}
|
|
1447
|
-
channelState.topics = [];
|
|
1448
|
-
break;
|
|
1449
|
-
case 'channel.updated':
|
|
1450
|
-
if (event.channel) {
|
|
1451
|
-
channel.data = {
|
|
1452
|
-
...channel.data,
|
|
1453
|
-
...event.channel,
|
|
1454
|
-
own_capabilities: event.channel?.own_capabilities ?? channel.data?.own_capabilities,
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
break;
|
|
1458
|
-
case 'pollchoice.new':
|
|
1459
|
-
if (event.message) {
|
|
1460
|
-
const user = getUserInfo(event.message.user?.id || '', users);
|
|
1461
|
-
event.message.user = user;
|
|
1462
|
-
channelState.addMessageSorted(event.message, false, false);
|
|
1463
|
-
}
|
|
1464
|
-
break;
|
|
1465
|
-
case 'reaction.new':
|
|
1466
|
-
if (event.message && event.reaction) {
|
|
1467
|
-
const userMsg = getUserInfo(event.message.user?.id || '', users);
|
|
1468
|
-
const userReaction = getUserInfo(event.reaction.user?.id || '', users);
|
|
1469
|
-
event.message.user = userMsg;
|
|
1470
|
-
event.message.latest_reactions = enrichWithUserInfo(event.message.latest_reactions || [], users);
|
|
1471
|
-
event.reaction.user = userReaction;
|
|
1472
|
-
if (event.message?.quoted_message) {
|
|
1473
|
-
const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
|
|
1474
|
-
event.message.quoted_message.user = quotedUser;
|
|
1475
|
-
}
|
|
1476
|
-
event.message = channelState.addReaction(event.reaction, event.message);
|
|
1477
|
-
}
|
|
1478
|
-
break;
|
|
1479
|
-
case 'reaction.deleted':
|
|
1480
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1481
|
-
if (event.message) {
|
|
1482
|
-
if (event.message?.quoted_message) {
|
|
1483
|
-
const quotedUser = getUserInfo(event.message.quoted_message.user?.id || '', users);
|
|
1484
|
-
event.message.quoted_message.user = quotedUser;
|
|
1485
|
-
}
|
|
1486
|
-
event.message.user = getUserInfo(event.message.user?.id || '', users);
|
|
1487
|
-
event.message.latest_reactions?.map((item) => {
|
|
1488
|
-
item.user = getUserInfo(item.user?.id || '', users);
|
|
1489
|
-
return item;
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
if (event.reaction) {
|
|
1494
|
-
event.reaction.user = getUserInfo(event.reaction.user?.id || '', users);
|
|
1495
|
-
event.message = channelState.removeReaction(event.reaction, event.message);
|
|
1496
|
-
}
|
|
1497
|
-
break;
|
|
1498
|
-
case 'member.joined':
|
|
1499
|
-
case 'notification.invite_accepted':
|
|
1500
|
-
if (event.member?.user_id) {
|
|
1501
|
-
const existUser = users.find((user) => user.id === event.member?.user_id);
|
|
1502
|
-
|
|
1503
|
-
if (!existUser) {
|
|
1504
|
-
const resUser = await this.getClient().queryUser(event.member?.user_id);
|
|
1505
|
-
users.push(resUser);
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
const user = getUserInfo(event.member.user_id, users);
|
|
1509
|
-
event.member.user = user;
|
|
1510
|
-
|
|
1511
|
-
if (event.member.user_id === this.getClient().user?.id) {
|
|
1512
|
-
channelState.membership = event.member;
|
|
1513
|
-
this.state.membership = event.member;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
channelState.members[event.member.user_id] = event.member;
|
|
1517
|
-
channel.data = {
|
|
1518
|
-
...channel.data,
|
|
1519
|
-
member_count: Number(channel.data?.member_count) + 1,
|
|
1520
|
-
members: channel.data?.members ? [...channel.data.members, event.member] : [event.member],
|
|
1521
|
-
} as ChannelAPIResponse<ErmisChatGenerics>['channel'];
|
|
1522
|
-
this.offlineMode = true;
|
|
1523
|
-
this.initialized = true;
|
|
1524
|
-
}
|
|
1525
|
-
break;
|
|
1526
|
-
case 'notification.invite_rejected':
|
|
1527
|
-
if (event.member?.user_id) {
|
|
1528
|
-
delete channelState.members[event.member.user_id];
|
|
1529
|
-
|
|
1530
|
-
// channel.data = {
|
|
1531
|
-
// ...channel.data,
|
|
1532
|
-
// member_count: Number(channel.data?.member_count) - 1,
|
|
1533
|
-
// members: channel.data?.members?.filter((m: any) => m.user_id !== event.member?.user_id) || [],
|
|
1534
|
-
// } as ChannelAPIResponse<ErmisChatGenerics>['channel'];
|
|
1535
|
-
}
|
|
1536
|
-
break;
|
|
1537
|
-
case 'notification.invite_messaging_skipped':
|
|
1538
|
-
if (event.member?.user_id) {
|
|
1539
|
-
const user = getUserInfo(event.member.user_id, users);
|
|
1540
|
-
event.member.user = user;
|
|
1541
|
-
|
|
1542
|
-
if (event.member.user_id === this.getClient().user?.id) {
|
|
1543
|
-
channelState.membership = event.member;
|
|
1544
|
-
this.state.membership = event.member;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
channelState.members[event.member.user_id] = event.member;
|
|
1548
|
-
|
|
1549
|
-
// this.offlineMode = true;
|
|
1550
|
-
// this.initialized = true;
|
|
1551
|
-
}
|
|
1552
|
-
break;
|
|
1553
|
-
case 'member.promoted':
|
|
1554
|
-
case 'member.demoted':
|
|
1555
|
-
case 'member.banned':
|
|
1556
|
-
case 'member.unbanned':
|
|
1557
|
-
case 'member.blocked':
|
|
1558
|
-
case 'member.unblocked':
|
|
1559
|
-
if (event.member?.user_id) {
|
|
1560
|
-
const user = getUserInfo(event.member.user_id, users);
|
|
1561
|
-
event.member.user = user;
|
|
1562
|
-
channelState.members[event.member.user_id] = event.member;
|
|
1563
|
-
if (event.member.user_id === this.getClient().user?.id) {
|
|
1564
|
-
channelState.membership = event.member;
|
|
1565
|
-
this.state.membership = event.member;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
break;
|
|
1569
|
-
case 'channel.pinned':
|
|
1570
|
-
if (channel.data) {
|
|
1571
|
-
channel.data.is_pinned = true;
|
|
1572
|
-
}
|
|
1573
|
-
break;
|
|
1574
|
-
case 'channel.unpinned':
|
|
1575
|
-
if (channel.data) {
|
|
1576
|
-
channel.data.is_pinned = false;
|
|
1577
|
-
}
|
|
1578
|
-
break;
|
|
1579
|
-
case 'channel.topic.disabled':
|
|
1580
|
-
if (channel.data) {
|
|
1581
|
-
channel.data.topics_enabled = false;
|
|
1582
|
-
}
|
|
1583
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1584
|
-
break;
|
|
1585
|
-
case 'channel.topic.enabled':
|
|
1586
|
-
if (channel.data) {
|
|
1587
|
-
channel.data.topics_enabled = true;
|
|
1588
|
-
}
|
|
1589
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1590
|
-
break;
|
|
1591
|
-
case 'channel.topic.created':
|
|
1592
|
-
const members = event.channel?.members || [];
|
|
1593
|
-
const enrichedMembers = enrichWithUserInfo(members, users);
|
|
1594
|
-
|
|
1595
|
-
const topicState: any = {
|
|
1596
|
-
channel: event.channel,
|
|
1597
|
-
members: enrichedMembers,
|
|
1598
|
-
messages: [],
|
|
1599
|
-
pinned_messages: [],
|
|
1600
|
-
};
|
|
1601
|
-
const topic = this.getClient().channel(event.channel_type || '', event.channel_id || '');
|
|
1602
|
-
topic.data = event.channel;
|
|
1603
|
-
topic._initializeState(topicState, 'latest');
|
|
1604
|
-
|
|
1605
|
-
if (!channelState.topics) {
|
|
1606
|
-
channelState.topics = [];
|
|
1607
|
-
}
|
|
1608
|
-
if (!channelState.topics.some((t) => t.cid === topic.cid)) {
|
|
1609
|
-
channelState.topics.push(topic);
|
|
1610
|
-
}
|
|
1611
|
-
break;
|
|
1612
|
-
case 'channel.topic.closed':
|
|
1613
|
-
if (channel.data) {
|
|
1614
|
-
channel.data.is_closed_topic = true;
|
|
1615
|
-
}
|
|
1616
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1617
|
-
break;
|
|
1618
|
-
case 'channel.topic.reopen':
|
|
1619
|
-
if (channel.data) {
|
|
1620
|
-
channel.data.is_closed_topic = false;
|
|
1621
|
-
}
|
|
1622
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1623
|
-
break;
|
|
1624
|
-
case 'channel.topic.updated':
|
|
1625
|
-
if (channel.data) {
|
|
1626
|
-
channel.data.name = event.channel?.name;
|
|
1627
|
-
channel.data.image = event.channel?.image;
|
|
1628
|
-
channel.data.description = event.channel?.description;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
event.user = getUserInfo(event.user?.id || '', users);
|
|
1632
|
-
break;
|
|
1633
|
-
default:
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// any event can send over the online count
|
|
1637
|
-
if (event.watcher_count !== undefined) {
|
|
1638
|
-
channel.state.watcher_count = event.watcher_count;
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
_callChannelListeners = (event: Event<ErmisChatGenerics>) => {
|
|
1643
|
-
const channel = this;
|
|
1644
|
-
// gather and call the listeners
|
|
1645
|
-
const listeners = [];
|
|
1646
|
-
if (channel.listeners.all) {
|
|
1647
|
-
listeners.push(...channel.listeners.all);
|
|
1648
|
-
}
|
|
1649
|
-
if (channel.listeners[event.type]) {
|
|
1650
|
-
listeners.push(...channel.listeners[event.type]);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
// call the event and send it to the listeners
|
|
1654
|
-
for (const listener of listeners) {
|
|
1655
|
-
if (typeof listener !== 'string') {
|
|
1656
|
-
listener(event);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
|
|
1661
|
-
_channelURL = () => {
|
|
1662
|
-
if (!this.id) {
|
|
1663
|
-
throw new Error('channel id is not defined');
|
|
1664
|
-
}
|
|
1665
|
-
return `${this.getClient().baseURL}/channels/${this.type}/${this.id}`;
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
|
-
_checkInitialized() {
|
|
1669
|
-
if (!this.initialized && !this.offlineMode) {
|
|
1670
|
-
throw Error(
|
|
1671
|
-
`Channel ${this.cid} hasn't been initialized yet. Make sure to call .watch() and wait for it to resolve`,
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
1677
|
-
_initializeState(
|
|
1678
|
-
state: ChannelAPIResponse<ErmisChatGenerics>,
|
|
1679
|
-
messageSetToAddToIfDoesNotExist: MessageSetType = 'latest',
|
|
1680
|
-
updateUserIds?: (id: string) => void,
|
|
1681
|
-
) {
|
|
1682
|
-
const { state: clientState, user, userID } = this.getClient();
|
|
1683
|
-
// add the Users
|
|
1684
|
-
if (state.channel.members) {
|
|
1685
|
-
for (const member of state.channel.members) {
|
|
1686
|
-
if (member.user) {
|
|
1687
|
-
if (updateUserIds) {
|
|
1688
|
-
updateUserIds(member.user.id);
|
|
1689
|
-
}
|
|
1690
|
-
clientState.updateUserReference(member.user, this.cid);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
this.state.membership = state.membership || {};
|
|
1696
|
-
|
|
1697
|
-
// Remove duplicate messages by ID
|
|
1698
|
-
const map = new Map();
|
|
1699
|
-
const uniqueMessages = [];
|
|
1700
|
-
|
|
1701
|
-
if (!state.messages) {
|
|
1702
|
-
state.messages = [];
|
|
1703
|
-
}
|
|
1704
|
-
for (const msg of state.messages) {
|
|
1705
|
-
if (!map.has(msg.id)) {
|
|
1706
|
-
map.set(msg.id, true);
|
|
1707
|
-
uniqueMessages.push(msg);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
if (this.state.pinnedMessages) {
|
|
1712
|
-
this.state.pinnedMessages = [];
|
|
1713
|
-
}
|
|
1714
|
-
this.state.addPinnedMessages(state.pinned_messages || []);
|
|
1715
|
-
|
|
1716
|
-
const messages = uniqueMessages || [];
|
|
1717
|
-
if (!this.state.messages) {
|
|
1718
|
-
this.state.initMessages();
|
|
1719
|
-
}
|
|
1720
|
-
const { messageSet } = this.state.addMessagesSorted(messages, false, true, true, messageSetToAddToIfDoesNotExist);
|
|
1721
|
-
|
|
1722
|
-
if (state.watcher_count !== undefined) {
|
|
1723
|
-
this.state.watcher_count = state.watcher_count;
|
|
1724
|
-
}
|
|
1725
|
-
// NOTE: we don't send the watchers with the channel data anymore
|
|
1726
|
-
// // convert the arrays into objects for easier syncing...
|
|
1727
|
-
if (state.watchers) {
|
|
1728
|
-
for (const watcher of state.watchers) {
|
|
1729
|
-
if (watcher) {
|
|
1730
|
-
clientState.updateUserReference(watcher, this.cid);
|
|
1731
|
-
this.state.watchers[watcher.id] = watcher;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// initialize read state to last message or current time if the channel is empty
|
|
1737
|
-
// if the user is a member, this value will be overwritten later on otherwise this ensures
|
|
1738
|
-
// that everything up to this point is not marked as unread
|
|
1739
|
-
if (userID != null) {
|
|
1740
|
-
const last_read = this.state.last_message_at || new Date();
|
|
1741
|
-
if (user) {
|
|
1742
|
-
this.state.read[user.id] = {
|
|
1743
|
-
user,
|
|
1744
|
-
last_read,
|
|
1745
|
-
unread_messages: 0,
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
// apply read state if part of the state
|
|
1751
|
-
if (state.read) {
|
|
1752
|
-
for (const read of state.read) {
|
|
1753
|
-
this.state.read[read.user.id] = {
|
|
1754
|
-
last_read: new Date(read.last_read),
|
|
1755
|
-
last_read_message_id: read.last_read_message_id,
|
|
1756
|
-
unread_messages: read.unread_messages ?? 0,
|
|
1757
|
-
user: read.user,
|
|
1758
|
-
last_send: read.last_send,
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
if (read.user.id === user?.id) {
|
|
1762
|
-
this.state.unreadCount = this.state.read[read.user.id].unread_messages;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
if (state.channel.members) {
|
|
1768
|
-
this.state.members = state.channel.members.reduce((acc, member) => {
|
|
1769
|
-
if (member.user) {
|
|
1770
|
-
acc[member.user.id] = member;
|
|
1771
|
-
}
|
|
1772
|
-
return acc;
|
|
1773
|
-
}, {} as ChannelState<ErmisChatGenerics>['members']);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// Process topics for team channels
|
|
1777
|
-
if (state.channel.type === 'team' && state.channel.topics_enabled && state.topics) {
|
|
1778
|
-
const users = Object.values(this.getClient().state.users);
|
|
1779
|
-
this._processTopics(state.topics, users);
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
return {
|
|
1783
|
-
messageSet,
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
_extendEventWithOwnReactions(event: Event<ErmisChatGenerics>) {
|
|
1788
|
-
if (!event.message) {
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
const message = this.state.findMessage(event.message.id, event.message.parent_id);
|
|
1792
|
-
if (message) {
|
|
1793
|
-
event.message.own_reactions = message.own_reactions;
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
_disconnect() {
|
|
1798
|
-
this._client.logger('info', `channel:disconnect() - Disconnecting the channel ${this.cid}`, {
|
|
1799
|
-
tags: ['connection', 'channel'],
|
|
1800
|
-
channel: this,
|
|
1801
|
-
});
|
|
1802
|
-
|
|
1803
|
-
this.disconnected = true;
|
|
1804
|
-
this.state.setIsUpToDate(false);
|
|
1805
|
-
}
|
|
1806
|
-
}
|