@ermis-network/ermis-chat-sdk 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ermis-network/ermis-chat-sdk",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Ermis Chat SDK",
5
5
  "author": "Ermis",
6
6
  "homepage": "https://ermis.network/",
package/src/channel.ts CHANGED
@@ -36,6 +36,8 @@ import {
36
36
  PollMessage,
37
37
  EditMessage,
38
38
  ForwardMessage,
39
+ CreateTopicData,
40
+ EditTopicData,
39
41
  } from './types';
40
42
  /**
41
43
  * Represents a Channel in the Ermis Network.
@@ -224,6 +226,48 @@ export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGener
224
226
  );
225
227
  }
226
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
+
227
271
  async editMessage(oldMessageID: string, message: EditMessage) {
228
272
  return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
229
273
  message,
@@ -726,7 +770,7 @@ export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGener
726
770
  }
727
771
  };
728
772
 
729
- async createTopic(data: any) {
773
+ async createTopic(data: CreateTopicData) {
730
774
  const project_id = this._client.projectId;
731
775
  const uuid = randomId();
732
776
  const topicID = `${project_id}:${uuid}`;
@@ -1079,7 +1123,7 @@ export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGener
1079
1123
  });
1080
1124
  }
1081
1125
 
1082
- async editTopic(topicCID: string, data: any) {
1126
+ async editTopic(topicCID: string, data: EditTopicData) {
1083
1127
  const response: any = await this.getClient().post(
1084
1128
  this.getClient().baseURL + `/channels/${this.type}/${this.id}/topics`,
1085
1129
  {
@@ -1390,6 +1434,18 @@ export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGener
1390
1434
  delete channelState.members[event.user.id];
1391
1435
  }
1392
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;
1393
1449
  case 'channel.updated':
1394
1450
  if (event.channel) {
1395
1451
  channel.data = {
@@ -1545,7 +1601,13 @@ export class Channel<ErmisChatGenerics extends ExtendableGenerics = DefaultGener
1545
1601
  const topic = this.getClient().channel(event.channel_type || '', event.channel_id || '');
1546
1602
  topic.data = event.channel;
1547
1603
  topic._initializeState(topicState, 'latest');
1548
- channelState.topics?.unshift(topic);
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
+ }
1549
1611
  break;
1550
1612
  case 'channel.topic.closed':
1551
1613
  if (channel.data) {
package/src/client.ts CHANGED
@@ -205,7 +205,6 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
205
205
  params.avatar = user.avatar;
206
206
  }
207
207
  const url = this.userBaseURL + '/get_token/external_auth';
208
- const query = new URLSearchParams(params).toString();
209
208
  const headers: Record<string, string> = {
210
209
  'Content-Type': 'application/json',
211
210
  };
@@ -213,21 +212,21 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
213
212
  const tokenStr = typeof token === 'string' && token.startsWith('Bearer ') ? token : `Bearer ${token}`;
214
213
  headers['Authorization'] = tokenStr;
215
214
  }
216
- const response = await fetch(`${url}?${query}`, {
217
- method: 'GET',
218
- headers,
219
- });
220
- if (!response.ok) {
221
- let errorMsg = '';
222
- try {
223
- const errorData = await response.json();
224
- errorMsg = errorData.message || JSON.stringify(errorData);
225
- } catch {
226
- errorMsg = await response.text();
215
+ try {
216
+ const response = await this.axiosInstance.get(url, {
217
+ params,
218
+ headers,
219
+ });
220
+ return response.data;
221
+ } catch (error: any) {
222
+ let errorMsg = 'Failed to fetch external auth token';
223
+ if (error.response && error.response.data) {
224
+ errorMsg = error.response.data.message || JSON.stringify(error.response.data);
225
+ } else if (error.message) {
226
+ errorMsg = error.message;
227
227
  }
228
228
  throw new Error(errorMsg);
229
229
  }
230
- return await response.json();
231
230
  }
232
231
 
233
232
  /**
@@ -242,7 +241,7 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
242
241
  connectUser = async (
243
242
  user: UserResponse<ErmisChatGenerics>,
244
243
  userTokenOrProvider: string | null,
245
- extenal_auth?: boolean, // pass true if you are using external auth
244
+ external_auth?: boolean, // pass true if you are using external auth
246
245
  ) => {
247
246
  this.logger('info', 'client:connectUser() - started', {
248
247
  tags: ['connection', 'client'],
@@ -251,19 +250,25 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
251
250
  throw new Error('The "id" field on the user is missing');
252
251
  }
253
252
 
253
+ let connectionUser = user;
254
+ let connectionToken = userTokenOrProvider;
255
+
254
256
  // If external auth is enabled, get the token from the server
255
- if (extenal_auth) {
257
+ if (external_auth) {
256
258
  const external_auth_token = await this.getExternalAuthToken(user, userTokenOrProvider);
257
259
 
258
- userTokenOrProvider = external_auth_token.token;
259
- user.id = external_auth_token.user_id;
260
+ connectionToken = external_auth_token.token;
261
+ connectionUser = {
262
+ ...user,
263
+ id: external_auth_token.user_id,
264
+ };
260
265
  }
261
266
 
262
267
  /**
263
268
  * Calling connectUser multiple times is potentially the result of a bad integration, however,
264
269
  * If the user id remains the same we don't throw error
265
270
  */
266
- if (this.userID === user.id && this.setUserPromise) {
271
+ if (this.userID === connectionUser.id && this.setUserPromise) {
267
272
  console.warn(
268
273
  'Consecutive calls to connectUser is detected, ideally you should only call this function once in your app.',
269
274
  );
@@ -283,11 +288,11 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
283
288
  }
284
289
 
285
290
  // we generate the client id client side
286
- this.userID = user.id;
291
+ this.userID = connectionUser.id;
287
292
 
288
- const setTokenPromise = this._setToken(user, userTokenOrProvider);
289
- this._setUser(user);
290
- this.state.updateUser({ id: user.id, name: user?.name || user.id, avatar: user?.avatar || '' });
293
+ const setTokenPromise = this._setToken(connectionUser, connectionToken);
294
+ this._setUser(connectionUser);
295
+ this.state.updateUser({ id: connectionUser.id, name: connectionUser?.name || connectionUser.id, avatar: connectionUser?.avatar || '' });
291
296
 
292
297
  const wsPromise = this.openConnection();
293
298
 
@@ -609,8 +614,8 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
609
614
  dispatchEvent = (event: Event<ErmisChatGenerics>) => {
610
615
  if (!event.received_at) event.received_at = new Date();
611
616
 
612
- // If the event is channel.created, handle it asynchronously
613
- if (event.type === 'channel.created') {
617
+ // If the event is channel.created or channel.topic.created, handle it asynchronously
618
+ if (event.type === 'channel.created' || event.type === 'channel.topic.created') {
614
619
  this._handleChannelCreatedEvent(event).then(() => {
615
620
  this._afterDispatchEvent(event);
616
621
  });
@@ -789,6 +794,85 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
789
794
  }
790
795
  }
791
796
 
797
+ if (event.type === 'message.new' && event.channel_type === 'topic') {
798
+ postListenerCallbacks.push(() => {
799
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
800
+ if (parentCid && this.activeChannels[parentCid]) {
801
+ const parentChannel = this.activeChannels[parentCid];
802
+ if (parentChannel.state.topics) {
803
+ parentChannel.state.topics.sort((a, b) => {
804
+ const aLatest = a.state?.latestMessages?.[a.state.latestMessages.length - 1]?.created_at;
805
+ const bLatest = b.state?.latestMessages?.[b.state.latestMessages.length - 1]?.created_at;
806
+ const aTime = aLatest ? new Date(aLatest).getTime() : 0;
807
+ const bTime = bLatest ? new Date(bLatest).getTime() : 0;
808
+ return bTime - aTime;
809
+ });
810
+ parentChannel._callChannelListeners({ ...event, type: 'channel.updated', channel: parentChannel.data } as any);
811
+ }
812
+ }
813
+ });
814
+ }
815
+ if (event.type === 'channel.topic.updated') {
816
+ postListenerCallbacks.push(() => {
817
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
818
+ if (parentCid && this.activeChannels[parentCid]) {
819
+ const parentChannel = this.activeChannels[parentCid];
820
+ if (parentChannel.state?.topics && event.channel) {
821
+ const topicIndex = parentChannel.state.topics.findIndex((t: any) => t.cid === event.cid || t.channel?.cid === event.cid);
822
+ if (topicIndex !== -1) {
823
+ const t = parentChannel.state.topics[topicIndex] as any;
824
+ if (t.data) {
825
+ t.data = { ...t.data, ...event.channel };
826
+ } else if (t.channel) {
827
+ t.channel = { ...t.channel, ...event.channel };
828
+ } else {
829
+ Object.assign(t, event.channel);
830
+ }
831
+ }
832
+ parentChannel._callChannelListeners({ ...event, type: 'channel.updated', channel: parentChannel.data } as any);
833
+ }
834
+ }
835
+
836
+ if (event.cid && this.activeChannels[event.cid]) {
837
+ const topicChannel = this.activeChannels[event.cid];
838
+ if (event.channel) {
839
+ topicChannel.data = { ...topicChannel.data, ...event.channel };
840
+ topicChannel._callChannelListeners({ ...event, type: 'channel.updated', channel: topicChannel.data } as any);
841
+ }
842
+ }
843
+ });
844
+ }
845
+
846
+ if (event.type === 'channel.topic.closed' || event.type === 'channel.topic.reopen') {
847
+ postListenerCallbacks.push(() => {
848
+ const isClosed = event.type === 'channel.topic.closed';
849
+ const parentCid = event.parent_cid;
850
+ if (parentCid && this.activeChannels[parentCid]) {
851
+ const parentChannel = this.activeChannels[parentCid];
852
+ if (parentChannel.state?.topics) {
853
+ const topicIndex = parentChannel.state.topics.findIndex((t: any) => t.cid === event.cid || t.channel?.cid === event.cid);
854
+ if (topicIndex !== -1) {
855
+ const t = parentChannel.state.topics[topicIndex] as any;
856
+ if (t.data) t.data.is_closed_topic = isClosed;
857
+ else if (t.channel) t.channel.is_closed_topic = isClosed;
858
+ else t.is_closed_topic = isClosed;
859
+ }
860
+ parentChannel._callChannelListeners({ ...event, type: 'channel.updated', channel: parentChannel.data } as any);
861
+ }
862
+ }
863
+
864
+ if (event.cid && this.activeChannels[event.cid]) {
865
+ const topicChannel = this.activeChannels[event.cid];
866
+ if (topicChannel.data) {
867
+ topicChannel.data.is_closed_topic = isClosed;
868
+ }
869
+ topicChannel._callChannelListeners({ ...event, type: 'channel.updated', channel: topicChannel.data } as any);
870
+ }
871
+ });
872
+ }
873
+
874
+
875
+
792
876
  if (event.type === 'connection.recovered') {
793
877
  postListenerCallbacks.push(() => {
794
878
  // Auto-resend offline failed messages
@@ -1086,7 +1170,14 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
1086
1170
  this.projectId = project_id;
1087
1171
  }
1088
1172
 
1089
- async uploadFile(file: File) {
1173
+ /**
1174
+ * Uploads a new avatar image for the current user.
1175
+ * The user's avatar URL is automatically updated in both the client and the local state.
1176
+ *
1177
+ * @param file - The image file to upload.
1178
+ * @returns The response containing the new avatar URL.
1179
+ */
1180
+ async uploadAvatar(file: File) {
1090
1181
  const formData = new FormData();
1091
1182
  formData.append('avatar', file);
1092
1183
  let response = await this.post<{ avatar: string }>(this.userBaseURL + '/users/upload', formData, {
@@ -1102,12 +1193,8 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
1102
1193
 
1103
1194
  return response;
1104
1195
  }
1105
- async updateProfile(name: string, about_me: string) {
1106
- let body = {
1107
- name,
1108
- about_me,
1109
- };
1110
- let response = await this.patch<UserResponse<ErmisChatGenerics>>(this.userBaseURL + '/users/update', body);
1196
+ async updateProfile(updates: Partial<UserResponse<ErmisChatGenerics>>) {
1197
+ let response = await this.patch<UserResponse<ErmisChatGenerics>>(this.userBaseURL + '/users/update', updates);
1111
1198
  this.user = response;
1112
1199
  this.state.updateUser(response);
1113
1200
  return response;
@@ -1359,53 +1446,60 @@ export class ErmisChat<ErmisChatGenerics extends ExtendableGenerics = DefaultGen
1359
1446
  };
1360
1447
 
1361
1448
  /**
1362
- * Creates an interactive `meeting` Channel locally, and immediately creates it on the server.
1363
- * Consumers can customize the `name` field. `members` and `public` fields are constrained.
1449
+ * Creates a quick channel and immediately registers it on the server.
1450
+ * Quick channels are public group channels that anyone can join without an invitation.
1451
+ * The creator is added as the first member automatically.
1364
1452
  *
1365
- * @param name - The custom name for the meeting channel.
1453
+ * @param name - An optional display name for the channel.
1366
1454
  * @returns A promise that resolves to the created `Channel` object.
1367
1455
  */
1368
- async createMeetingChannel(name?: string): Promise<Channel<ErmisChatGenerics>> {
1456
+ async createQuickChannel(name?: string): Promise<Channel<ErmisChatGenerics>> {
1369
1457
  if (!this.userID) {
1370
1458
  throw Error('Call connectUser before creating a channel');
1371
1459
  }
1372
1460
 
1461
+ const now = new Date();
1462
+ const formattedDate = new Intl.DateTimeFormat('en-US', {
1463
+ month: 'short', day: '2-digit', year: 'numeric',
1464
+ hour: '2-digit', minute: '2-digit', hour12: false,
1465
+ }).format(now);
1466
+
1373
1467
  const payload = {
1374
- name: name || `Meeting Public - ${new Date().toISOString()}`,
1468
+ name: name || `Quick Channel - ${formattedDate}`,
1375
1469
  members: [this.userID],
1376
1470
  public: true,
1377
1471
  } as unknown as ChannelData<ErmisChatGenerics>;
1378
1472
 
1379
- const meetingChannel = this.channel('meeting', payload);
1380
- await meetingChannel.create();
1473
+ const quickChannel = this.channel('meeting', payload);
1474
+ await quickChannel.create();
1381
1475
 
1382
- return meetingChannel;
1476
+ return quickChannel;
1383
1477
  }
1384
1478
 
1385
1479
  /**
1386
- * Joins a `meeting` channel.
1387
- * It queries/watches the channel to see if caller is already a member.
1388
- * If not, it accepts the invite to join the channel, then watches it again to reflect changes.
1480
+ * Joins a quick channel by its ID.
1481
+ * Automatically checks whether the caller is already a member.
1482
+ * If not, it joins the channel and synchronizes state.
1389
1483
  *
1390
- * @param channelId - The ID of the meeting channel to join.
1484
+ * @param channelId - The ID of the quick channel to join.
1391
1485
  * @returns A promise that resolves to the joined `Channel` object.
1392
1486
  */
1393
- async joinMeetingChannel(channelId: string): Promise<Channel<ErmisChatGenerics>> {
1487
+ async joinQuickChannel(channelId: string): Promise<Channel<ErmisChatGenerics>> {
1394
1488
  if (!this.userID) {
1395
1489
  throw Error('Call connectUser before joining a channel');
1396
1490
  }
1397
1491
 
1398
- const meetingChannel = this.channel('meeting', channelId);
1399
- await meetingChannel.watch();
1492
+ const quickChannel = this.channel('meeting', channelId);
1493
+ await quickChannel.watch();
1400
1494
 
1401
- const isMember = meetingChannel.state.members && meetingChannel.state.members[this.userID];
1495
+ const isMember = quickChannel.state.members && quickChannel.state.members[this.userID];
1402
1496
 
1403
1497
  if (!isMember) {
1404
- await meetingChannel.acceptInvite('join');
1405
- await meetingChannel.watch();
1498
+ await quickChannel.acceptInvite('join');
1499
+ await quickChannel.watch();
1406
1500
  }
1407
1501
 
1408
- return meetingChannel;
1502
+ return quickChannel;
1409
1503
  }
1410
1504
 
1411
1505
  _normalizeExpiration(timeoutOrExpirationDate?: null | number | string | Date) {
package/src/types.ts CHANGED
@@ -58,6 +58,7 @@ export type ChannelResponse<ErmisChatGenerics extends ExtendableGenerics = Defau
58
58
  member_capabilities?: string[];
59
59
  is_pinned?: boolean;
60
60
  topics_enabled?: boolean;
61
+ parent_cid?: string;
61
62
  is_closed_topic?: boolean;
62
63
  };
63
64
 
@@ -265,6 +266,7 @@ export type Event<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics
265
266
  message?: MessageResponse<ErmisChatGenerics>;
266
267
  online?: boolean;
267
268
  parent_id?: string;
269
+ parent_cid?: string;
268
270
  reaction?: ReactionResponse<ErmisChatGenerics>;
269
271
  received_at?: string | Date;
270
272
  unread_messages?: number;
@@ -300,6 +302,18 @@ export type ChannelFilters = {
300
302
  include_parent?: boolean;
301
303
  };
302
304
 
305
+ export type CreateTopicData = {
306
+ name: string;
307
+ image?: string;
308
+ [key: string]: any;
309
+ };
310
+
311
+ export type EditTopicData = {
312
+ name?: string;
313
+ image?: string;
314
+ description?: string;
315
+ };
316
+
303
317
  export type ChannelSort = {
304
318
  field: string;
305
319
  direction: -1 | 1;