@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.
Files changed (54) hide show
  1. package/README.md +330 -0
  2. package/bin/init-call.js +9 -0
  3. package/dist/encryption/index.browser.cjs +13045 -0
  4. package/dist/encryption/index.browser.cjs.map +1 -0
  5. package/dist/encryption/index.browser.mjs +12959 -0
  6. package/dist/encryption/index.browser.mjs.map +1 -0
  7. package/dist/encryption/index.cjs +13045 -0
  8. package/dist/encryption/index.cjs.map +1 -0
  9. package/dist/encryption/index.d.mts +3 -0
  10. package/dist/encryption/index.d.ts +3 -0
  11. package/dist/encryption/index.mjs +12959 -0
  12. package/dist/encryption/index.mjs.map +1 -0
  13. package/dist/index-CcvHIY5q.d.mts +4988 -0
  14. package/dist/index-CcvHIY5q.d.ts +4988 -0
  15. package/dist/index.browser.cjs +20399 -6823
  16. package/dist/index.browser.cjs.map +1 -1
  17. package/dist/index.browser.full-bundle.min.js +20 -18
  18. package/dist/index.browser.full-bundle.min.js.map +1 -1
  19. package/dist/index.browser.mjs +20315 -6790
  20. package/dist/index.browser.mjs.map +1 -1
  21. package/dist/index.cjs +20400 -6824
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.mts +167 -1356
  24. package/dist/index.d.ts +167 -1356
  25. package/dist/index.mjs +20312 -6787
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/wasm_worker.worker.mjs +1600 -0
  28. package/dist/wasm_worker.worker.mjs.map +1 -0
  29. package/package.json +22 -7
  30. package/public/e2ee-media-stream-worker.js +627 -0
  31. package/public/ermis_call_node_wasm_bg.wasm +0 -0
  32. package/public/openmls_wasm_bg.wasm +0 -0
  33. package/src/attachment_utils.ts +0 -148
  34. package/src/auth.ts +0 -352
  35. package/src/channel.ts +0 -1806
  36. package/src/channel_state.ts +0 -607
  37. package/src/client.ts +0 -1617
  38. package/src/client_state.ts +0 -55
  39. package/src/connection.ts +0 -587
  40. package/src/ermis_call_node.ts +0 -978
  41. package/src/errors.ts +0 -60
  42. package/src/events.ts +0 -46
  43. package/src/hevc_decoder_config.ts +0 -305
  44. package/src/index.ts +0 -16
  45. package/src/media_stream_receiver.ts +0 -525
  46. package/src/media_stream_sender.ts +0 -400
  47. package/src/shims/empty.ts +0 -1
  48. package/src/signal_message.ts +0 -146
  49. package/src/system_message.ts +0 -117
  50. package/src/token_manager.ts +0 -48
  51. package/src/types.ts +0 -581
  52. package/src/utils.ts +0 -534
  53. package/src/wasm/ermis_call_node_wasm.d.ts +0 -154
  54. package/src/wasm/ermis_call_node_wasm.js +0 -1498
@@ -1,400 +0,0 @@
1
- import { base64Encode, createPacketWithHeader } from './utils';
2
- import { AudioConfig, INodeCall, TransceiverState, VideoConfig } from './types';
3
-
4
- export class MediaStreamSender {
5
- private videoEncoder: VideoEncoder | null = null;
6
- private audioEncoder: AudioEncoder | null = null;
7
- private videoReader: ReadableStreamDefaultReader<VideoFrame> | null = null;
8
-
9
- private localStream: MediaStream | null = null;
10
-
11
- private videoConfig: VideoConfig | null = null;
12
- private audioConfig: AudioConfig | null = null;
13
-
14
- private videoConfigSent: boolean = false;
15
- private audioConfigSent: boolean = false;
16
-
17
- private hasVideo: boolean = false;
18
- private hasAudio: boolean = false;
19
-
20
- private forceKeyFrame: boolean = false;
21
-
22
- private nodeCall: INodeCall;
23
-
24
- constructor(nodeCall: INodeCall) {
25
- this.nodeCall = nodeCall;
26
- }
27
-
28
- /**
29
- * Bắt đầu xử lý MediaStream
30
- */
31
- public async connect(address: string): Promise<void> {
32
- try {
33
- await this.nodeCall.connect(address);
34
- await this.sendConnected();
35
- await this.sendConfigs();
36
- } catch (error) {
37
- console.error('Error starting MediaStreamSender:', error);
38
- }
39
- }
40
-
41
- public async sendConfigs(): Promise<void> {
42
- try {
43
- await this.sendTransceiverState(this.hasAudio, this.hasVideo);
44
- await this.sendAudioConfig();
45
-
46
- const videoTrack = this.localStream?.getVideoTracks()[0];
47
- if (videoTrack) {
48
- await this.sendVideoConfig();
49
- }
50
- } catch (error) {
51
- console.error('Error sending configs:', error);
52
- }
53
- }
54
-
55
- /**
56
- * Dừng và reset encoders
57
- */
58
- public stop = (): void => {
59
- if (this.videoReader) {
60
- try {
61
- this.videoReader.cancel('Stream stopped').catch(() => {});
62
- } catch (e) {}
63
- this.videoReader = null;
64
- }
65
-
66
- if (this.videoEncoder) {
67
- try {
68
- if (this.videoEncoder.state !== 'closed') {
69
- this.videoEncoder.reset(); // Xả frame
70
- this.videoEncoder.close();
71
- }
72
- } catch (e) {}
73
- this.videoEncoder = null;
74
- }
75
-
76
- // Reset and close audio encoder
77
- if (this.audioEncoder) {
78
- try {
79
- if (this.audioEncoder.state !== 'closed') {
80
- this.audioEncoder.reset();
81
- this.audioEncoder.close();
82
- }
83
- } catch (e) {}
84
- this.audioEncoder = null;
85
- }
86
-
87
- // Reset configs and flags
88
- this.videoConfig = null;
89
- this.audioConfig = null;
90
-
91
- this.videoConfigSent = false;
92
- this.audioConfigSent = false;
93
- this.hasVideo = false;
94
- this.hasAudio = false;
95
-
96
- if (this.localStream) {
97
- this.localStream.getTracks().forEach((track) => track.stop());
98
- this.localStream = null;
99
- }
100
- };
101
-
102
- public initAudioEncoder = (audioTrack: MediaStreamTrack): void => {
103
- this.localStream = new MediaStream([audioTrack]);
104
- this.audioConfigSent = false;
105
- this.hasAudio = !!audioTrack;
106
-
107
- const audioEncoder = new AudioEncoder({
108
- output: (chunk, metadata) => {
109
- if (metadata?.decoderConfig && !this.audioConfigSent) {
110
- let description: string | undefined = undefined;
111
- if (metadata.decoderConfig.description) {
112
- description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
113
- }
114
-
115
- this.audioConfig = {
116
- codec: metadata.decoderConfig.codec ?? 'opus',
117
- sampleRate: metadata.decoderConfig.sampleRate ?? 48000,
118
- numberOfChannels: metadata.decoderConfig.numberOfChannels ?? 1,
119
- ...(description && { description }),
120
- };
121
- }
122
-
123
- if (chunk && this.isReadyToSendData('audio')) {
124
- const data = new ArrayBuffer(chunk.byteLength);
125
- chunk.copyTo(data);
126
- // const timestamp = Math.floor(chunk.timestamp / 1000);
127
- const timestamp = chunk.timestamp;
128
-
129
- const packet = createPacketWithHeader(data, timestamp, 'audio', null);
130
- this.sendPacketOrQueue(packet, 'audio', null);
131
- }
132
- },
133
- error: (e) => console.error('AudioEncoder error:', e),
134
- });
135
-
136
- audioEncoder.configure({
137
- codec: 'mp4a.40.2',
138
- sampleRate: 48000,
139
- numberOfChannels: 1,
140
- bitrate: 128000,
141
- });
142
-
143
- this.audioEncoder = audioEncoder;
144
- this.processAudioFrames(audioTrack);
145
- };
146
-
147
- public initVideoEncoder(videoTrack: MediaStreamTrack): void {
148
- if (this.localStream) {
149
- this.localStream.addTrack(videoTrack);
150
- }
151
-
152
- this.videoConfigSent = false;
153
- this.hasVideo = !!videoTrack;
154
-
155
- const settings = videoTrack.getSettings();
156
- const videoWidth = settings.width || 1280;
157
- const videoHeight = settings.height || 720;
158
-
159
- const videoEncoder = new VideoEncoder({
160
- output: async (chunk, metadata) => {
161
- if (metadata?.decoderConfig && !this.videoConfigSent) {
162
- let description: string | undefined = undefined;
163
- if (metadata.decoderConfig.description) {
164
- description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
165
- }
166
-
167
- this.videoConfig = {
168
- codec: metadata.decoderConfig.codec ?? 'hev1.1.6.L93.B0',
169
- codedWidth: metadata.decoderConfig.codedWidth ?? videoWidth,
170
- codedHeight: metadata.decoderConfig.codedHeight ?? videoHeight,
171
- frameRate: 30.0,
172
- orientation: 0,
173
- ...(description && { description }),
174
- };
175
-
176
- await this.sendVideoConfig();
177
- }
178
-
179
- if (chunk && this.isReadyToSendData('video')) {
180
- const data = new ArrayBuffer(chunk.byteLength);
181
- chunk.copyTo(data);
182
- const frameType = chunk.type === 'key' ? 'video-key' : 'video-delta';
183
- // const timestamp = Math.floor(chunk.timestamp / 1000);
184
- const timestamp = chunk.timestamp;
185
-
186
- const packet = createPacketWithHeader(data, timestamp, frameType, null);
187
- this.sendPacketOrQueue(packet, 'video', frameType);
188
- }
189
- },
190
- error: (e) => console.error('VideoEncoder error:', e),
191
- });
192
-
193
- videoEncoder.configure({
194
- codec: 'hev1.1.6.L93.B0',
195
- width: videoWidth,
196
- height: videoHeight,
197
- bitrate: 500_000,
198
- framerate: 30,
199
- latencyMode: 'realtime',
200
- hardwareAcceleration: 'prefer-hardware',
201
- });
202
-
203
- this.videoEncoder = videoEncoder;
204
- this.processVideoFrames(videoTrack);
205
- }
206
-
207
- public initEncoders = (stream: MediaStream): void => {
208
- const videoTrack = stream.getVideoTracks()[0];
209
- const audioTrack = stream.getAudioTracks()[0];
210
-
211
- if (audioTrack) {
212
- this.initAudioEncoder(audioTrack);
213
- }
214
-
215
- if (videoTrack) {
216
- this.initVideoEncoder(videoTrack);
217
- }
218
- };
219
-
220
- public sendTransceiverState = async (audioEnable: boolean, videoEnable: boolean) => {
221
- const state: TransceiverState = {
222
- audio_enable: !!audioEnable,
223
- video_enable: !!videoEnable,
224
- };
225
- const configPacket = createPacketWithHeader(null, null, 'transciverState', state);
226
- await this.nodeCall.sendControlFrame(configPacket);
227
- };
228
-
229
- public async replaceVideoTrack(track: MediaStreamTrack): Promise<void> {
230
- // 1. Dừng reader của track cũ (quan trọng)
231
- if (this.videoReader) {
232
- try {
233
- // Việc gọi cancel sẽ làm Promise tại dòng await read() bên dưới throw lỗi hoặc trả về done
234
- await this.videoReader.cancel('Replacing track');
235
- } catch (e) {
236
- // Bỏ qua lỗi khi cancel
237
- }
238
- this.videoReader = null;
239
- }
240
-
241
- if (track) {
242
- this.processVideoFrames(track);
243
- }
244
- }
245
-
246
- public async replaceAudioTrack(track: MediaStreamTrack): Promise<void> {
247
- if (track) {
248
- this.processAudioFrames(track);
249
- }
250
- }
251
-
252
- /**
253
- * Yêu cầu gửi keyframe ngay lập tức (được gọi khi nhận REQUEST_KEY_FRAME từ receiver)
254
- */
255
- public requestKeyFrame = (): void => {
256
- console.log('📥 KeyFrame requested');
257
- this.forceKeyFrame = true;
258
- };
259
-
260
- // ================= PRIVATE METHODS =================
261
-
262
- private processVideoFrames = async (videoTrack: MediaStreamTrack) => {
263
- try {
264
- // @ts-ignore: MediaStreamTrackProcessor is explicitly defined in WebCodecs types
265
- const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
266
- this.videoReader = videoProcessor.readable.getReader();
267
-
268
- let frameCounter = 0;
269
- while (true) {
270
- if (!this.videoReader) break;
271
-
272
- const { done, value: frame } = await this.videoReader.read();
273
- if (done) break;
274
-
275
- if (!this.videoEncoder) {
276
- frame?.close();
277
- break;
278
- }
279
-
280
- if (frame) {
281
- frameCounter += 1;
282
- const keyFrame = frameCounter % 60 === 0 || this.forceKeyFrame;
283
- if (this.forceKeyFrame) {
284
- console.log('📤 Sending forced KeyFrame');
285
- this.forceKeyFrame = false;
286
- }
287
- try {
288
- this.videoEncoder.encode(frame, { keyFrame });
289
- } catch (err) {
290
- console.error('Encode error:', err);
291
- } finally {
292
- frame.close();
293
- }
294
- }
295
- }
296
- } catch (error: any) {
297
- console.error(`Error processing video frames: ${error.message}`);
298
- } finally {
299
- if (this.videoReader) {
300
- try {
301
- this.videoReader.releaseLock();
302
- } catch (e) {}
303
- // this.videoReader = null;
304
- }
305
- }
306
- };
307
-
308
- private processAudioFrames = async (audioTrack: MediaStreamTrack) => {
309
- // @ts-ignore
310
- const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
311
- const audioReader = audioProcessor.readable.getReader();
312
-
313
- try {
314
- while (true) {
315
- const { done, value: frame } = await audioReader.read();
316
- if (done) break;
317
-
318
- if (!this.audioEncoder) {
319
- frame?.close();
320
- break;
321
- }
322
-
323
- if (frame) {
324
- try {
325
- this.audioEncoder.encode(frame);
326
- } catch (err) {
327
- console.error('Audio Encoding error:', err);
328
- } finally {
329
- frame.close();
330
- }
331
- }
332
- }
333
- } catch (error: any) {
334
- console.error(`Error processing audio frames: ${error.message}`);
335
- }
336
- };
337
-
338
- private isReadyToSendData = (type: 'video' | 'audio'): boolean => {
339
- const videoReady = !this.hasVideo || this.videoConfigSent;
340
- const audioReady = !this.hasAudio || this.audioConfigSent;
341
- const allConfigsSent = videoReady && audioReady;
342
-
343
- if (type === 'video') {
344
- return allConfigsSent && this.videoConfigSent;
345
- } else if (type === 'audio') {
346
- return allConfigsSent && this.audioConfigSent;
347
- }
348
-
349
- return false;
350
- };
351
-
352
- private sendVideoConfig = async () => {
353
- if (this.videoConfig && !this.videoConfigSent) {
354
- try {
355
- const configPacket = createPacketWithHeader(null, null, 'videoConfig', this.videoConfig);
356
- await this.nodeCall.sendControlFrame(configPacket);
357
- this.videoConfigSent = true;
358
- } catch (error) {
359
- console.error('Error sending video config:', error);
360
- }
361
- }
362
- };
363
-
364
- private sendAudioConfig = async () => {
365
- if (this.audioConfig && !this.audioConfigSent) {
366
- try {
367
- const configPacket = createPacketWithHeader(null, null, 'audioConfig', this.audioConfig);
368
- await this.nodeCall.sendControlFrame(configPacket);
369
- this.audioConfigSent = true;
370
- } catch (error) {
371
- console.error('Error sending audio config:', error);
372
- }
373
- }
374
- };
375
-
376
- public sendConnected = async () => {
377
- const configPacket = createPacketWithHeader(null, null, 'connected', null);
378
- await this.nodeCall.sendControlFrame(configPacket);
379
- };
380
-
381
- private sendPacketOrQueue = async (
382
- packet: Uint8Array,
383
- type: 'video' | 'audio',
384
- frameType: 'video-key' | 'video-delta' | null,
385
- ) => {
386
- if (!this.isReadyToSendData(type)) {
387
- return;
388
- }
389
-
390
- if (type === 'audio') {
391
- await this.nodeCall.sendAudioFrame(packet);
392
- } else if (type === 'video') {
393
- if (frameType === 'video-key') {
394
- await this.nodeCall.beginWithGop(packet);
395
- } else if (frameType === 'video-delta') {
396
- await this.nodeCall.sendFrame(packet);
397
- }
398
- }
399
- };
400
- }
@@ -1 +0,0 @@
1
- export default null;
@@ -1,146 +0,0 @@
1
- /**
2
- * Call type constants for signal messages.
3
- */
4
- export const CallType = {
5
- AUDIO: 'audio',
6
- VIDEO: 'video',
7
- } as const;
8
-
9
- export type CallTypeValue = (typeof CallType)[keyof typeof CallType];
10
-
11
- /**
12
- * Result of parsing a signal message.
13
- */
14
- export interface SignalMessageResult {
15
- text: string;
16
- duration: string;
17
- callType: CallTypeValue | '';
18
- color: string;
19
- }
20
-
21
- /**
22
- * Format duration from milliseconds to "X min, Y sec" format.
23
- */
24
- function formatDuration(durationMs: string): string {
25
- if (!durationMs) return '';
26
- const ms = parseInt(durationMs, 10);
27
- if (isNaN(ms) || ms <= 0) return '';
28
- const totalSeconds = Math.floor(ms / 1000);
29
- const minutes = Math.floor(totalSeconds / 60);
30
- const seconds = totalSeconds % 60;
31
- return `${minutes} min, ${seconds} sec`;
32
- }
33
-
34
- /**
35
- * Parse a raw signal message string into a structured object
36
- * containing text, duration, call type, and color.
37
- *
38
- * Signal messages represent call events. The raw format is:
39
- * `"<formatId> <callerId> [<enderId> <duration>]"`
40
- *
41
- * @param value - Raw signal message string from the server
42
- * @param myUserId - The current user's ID (from client.userID)
43
- * @returns Parsed signal message object, or null if input is empty
44
- */
45
- export function parseSignalMessage(
46
- value: string,
47
- myUserId: string,
48
- ): SignalMessageResult | null {
49
- if (!value || typeof value !== 'string') return null;
50
-
51
- const trimmed = value.trim();
52
- if (!trimmed) return null;
53
-
54
- const parts = trimmed.split(' ');
55
- const number = parseInt(parts[0], 10);
56
- const callerId = parts[1] ?? '';
57
- const isMe = myUserId === callerId;
58
-
59
- let enderId = '';
60
- let duration = '';
61
- let callType: CallTypeValue | '' = '';
62
- let color = '';
63
-
64
- if (number === 3 || number === 6) {
65
- enderId = parts[2] ?? '';
66
- duration = parts[3] === '0' ? '' : (parts[3] ?? '');
67
- }
68
-
69
- let text: string;
70
- switch (number) {
71
- case 1: // AudioCallStarted
72
- text = isMe ? 'Calling...' : 'Incoming audio call...';
73
- callType = CallType.AUDIO;
74
- color = '#54D62C';
75
- break;
76
- case 2: // AudioCallMissed
77
- text = isMe ? 'Outgoing audio call' : 'You missed audio call';
78
- callType = CallType.AUDIO;
79
- color = '#FF4842';
80
- break;
81
- case 3: // AudioCallEnded
82
- if (duration) {
83
- text = isMe ? 'Outgoing audio call' : 'Incoming audio call';
84
- color = '#54D62C';
85
- } else {
86
- if (enderId === myUserId) {
87
- text = 'You cancel audio call';
88
- } else {
89
- text = 'You missed audio call';
90
- }
91
- color = '#FF4842';
92
- }
93
- callType = CallType.AUDIO;
94
- break;
95
- case 4: // VideoCallStarted
96
- text = isMe ? 'Calling...' : 'Incoming video call...';
97
- callType = CallType.VIDEO;
98
- color = '#54D62C';
99
- break;
100
- case 5: // VideoCallMissed
101
- text = isMe ? 'Outgoing video call' : 'You missed video call';
102
- callType = CallType.VIDEO;
103
- color = '#FF4842';
104
- break;
105
- case 6: // VideoCallEnded
106
- if (duration) {
107
- text = isMe ? 'Outgoing video call' : 'Incoming video call';
108
- color = '#54D62C';
109
- } else {
110
- if (enderId === myUserId) {
111
- text = 'You cancel video call';
112
- } else {
113
- text = 'You missed video call';
114
- }
115
- color = '#FF4842';
116
- }
117
- callType = CallType.VIDEO;
118
- break;
119
- case 7: // AudioCallRejected
120
- text = isMe ? 'Recipient rejected audio call' : 'You rejected audio call';
121
- callType = CallType.AUDIO;
122
- color = '#FF4842';
123
- break;
124
- case 8: // VideoCallRejected
125
- text = isMe ? 'Recipient rejected video call' : 'You rejected video call';
126
- callType = CallType.VIDEO;
127
- color = '#FF4842';
128
- break;
129
- case 9: // AudioCallBusy
130
- text = isMe ? 'Recipient was busy' : 'You missed audio call';
131
- callType = CallType.AUDIO;
132
- color = '#FF4842';
133
- break;
134
- case 10: // VideoCallBusy
135
- text = isMe ? 'Recipient was busy' : 'You missed video call';
136
- callType = CallType.VIDEO;
137
- color = '#FF4842';
138
- break;
139
- default:
140
- text = trimmed;
141
- callType = '';
142
- color = '';
143
- }
144
-
145
- return { text, duration: formatDuration(duration), callType, color };
146
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * Duration lookup: milliseconds → human-readable text.
3
- */
4
- const DURATION_MAP: Record<string, string> = {
5
- '10000': '10 seconds',
6
- '30000': '30 seconds',
7
- '60000': '1 minute',
8
- '300000': '5 minutes',
9
- '900000': '15 minutes',
10
- '3600000': '60 minutes',
11
- };
12
-
13
- /**
14
- * Resolve a user ID to a display name using the provided map.
15
- * Falls back to the raw userId if no entry is found.
16
- */
17
- function resolveUser(userId: string, userMap: Record<string, string>): string {
18
- return userMap[userId] ?? userId;
19
- }
20
-
21
- /**
22
- * Parse a raw system message string into a human-readable English sentence.
23
- *
24
- * The raw format is: `"<formatId> <userID> [<param1> <param2> ...]"`
25
- *
26
- * @param value - Raw system message string from the server
27
- * @param userMap - Mapping of user IDs → display names
28
- * @returns Parsed English text, or the original string if the format is unknown
29
- */
30
- export function parseSystemMessage(
31
- value: string,
32
- userMap: Record<string, string>,
33
- ): string {
34
- if (!value || typeof value !== 'string') return value ?? '';
35
-
36
- const trimmed = value.trim();
37
- if (!trimmed) return '';
38
-
39
- const parts = trimmed.split(' ');
40
- const formatId = parts[0];
41
- const userId = parts[1] ?? '';
42
- const userName = userId ? resolveUser(userId, userMap) : 'User';
43
-
44
- switch (formatId) {
45
- // 1: userName changed the channel name to channelName (may contain spaces)
46
- case '1': {
47
- const channelName = parts.slice(2).join(' ');
48
- return `${userName} changed the channel name to ${channelName}.`;
49
- }
50
-
51
- // 2–13, 16–17: single-user actions
52
- case '2':
53
- return `${userName} changed the channel avatar.`;
54
- case '3':
55
- return `${userName} changed the channel description.`;
56
- case '4':
57
- return `${userName} was removed from the channel.`;
58
- case '5':
59
- return `${userName} was banned.`;
60
- case '6':
61
- return `${userName} was unbanned.`;
62
- case '7':
63
- return `${userName} was promoted to moderator.`;
64
- case '8':
65
- return `${userName} was demoted from moderator.`;
66
- case '9':
67
- return `${userName}'s permissions were updated.`;
68
- case '10':
69
- return `${userName} joined the channel.`;
70
- case '11':
71
- return `${userName} declined the channel invitation.`;
72
- case '12':
73
- return `${userName} left the channel.`;
74
- case '13':
75
- return `${userName} cleared the chat history.`;
76
-
77
- // 14: channel type change (true = public, false = private)
78
- case '14': {
79
- const rawType = parts[2] ?? '';
80
- const channelType = rawType === 'true' ? 'public' : 'private';
81
- return `${userName} changed the channel to ${channelType}.`;
82
- }
83
-
84
- // 15: cooldown toggle / duration
85
- case '15': {
86
- const duration = parts[2] ?? '0';
87
- if (duration === '0') {
88
- return `${userName} disabled cooldown.`;
89
- }
90
- const durationText = DURATION_MAP[duration] ?? `${duration}ms`;
91
- return `${userName} enabled cooldown for ${durationText}.`;
92
- }
93
-
94
- case '16':
95
- return `${userName} updated the banned words.`;
96
- case '17':
97
- return `${userName} was added to the channel.`;
98
-
99
- // 18: admin transfer (two user IDs)
100
- case '18': {
101
- const oldUserId = parts[1] ?? '';
102
- const newUserId = parts[2] ?? '';
103
- const oldUserName = oldUserId ? resolveUser(oldUserId, userMap) : 'User';
104
- const newUserName = newUserId ? resolveUser(newUserId, userMap) : 'User';
105
- return `Admin ${oldUserName} left and assigned ${newUserName} as the new admin.`;
106
- }
107
-
108
- // 19–20: pin / unpin (userId + msgID)
109
- case '19':
110
- return `${userName} pinned a message.`;
111
- case '20':
112
- return `${userName} unpinned a message.`;
113
-
114
- default:
115
- return trimmed;
116
- }
117
- }
@@ -1,48 +0,0 @@
1
- import { ExtendableGenerics, DefaultGenerics, UserResponse } from './types';
2
-
3
- /**
4
- * TokenManager
5
- *
6
- * Manages token storage and retrieval for the chat client.
7
- */
8
- export class TokenManager<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
9
- loadTokenPromise: Promise<string> | null;
10
- token?: string;
11
- user?: UserResponse<ErmisChatGenerics>;
12
-
13
- constructor() {
14
- this.loadTokenPromise = null;
15
- }
16
-
17
- /**
18
- * Set the static string token.
19
- */
20
- setTokenOrProvider = async (tokenOrProvider: string | null, user: UserResponse<ErmisChatGenerics>) => {
21
- this.user = user;
22
-
23
- if (typeof tokenOrProvider === 'string') {
24
- this.token = tokenOrProvider;
25
- }
26
-
27
- this.loadTokenPromise = Promise.resolve(this.token as string);
28
- };
29
-
30
- /**
31
- * Resets the token manager.
32
- */
33
- reset = () => {
34
- this.token = undefined;
35
- this.user = undefined;
36
- this.loadTokenPromise = null;
37
- };
38
-
39
- /**
40
- * Resolves when token is ready.
41
- */
42
- tokenReady = () => this.loadTokenPromise;
43
-
44
- /** Returns the current token */
45
- getToken = () => {
46
- return this.token;
47
- };
48
- }