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

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/dist/index.mjs CHANGED
@@ -1006,6 +1006,44 @@ var Channel = class {
1006
1006
  this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/unpin`
1007
1007
  );
1008
1008
  }
1009
+ async pin() {
1010
+ if (this.data) this.data.is_pinned = true;
1011
+ this.getClient().dispatchEvent({
1012
+ type: "channel.pinned",
1013
+ cid: this.cid,
1014
+ channel: this.data
1015
+ });
1016
+ try {
1017
+ return await this.getClient().pinChannel(this.type, this.id);
1018
+ } catch (e) {
1019
+ if (this.data) this.data.is_pinned = false;
1020
+ this.getClient().dispatchEvent({
1021
+ type: "channel.unpinned",
1022
+ cid: this.cid,
1023
+ channel: this.data
1024
+ });
1025
+ throw e;
1026
+ }
1027
+ }
1028
+ async unpin() {
1029
+ if (this.data) this.data.is_pinned = false;
1030
+ this.getClient().dispatchEvent({
1031
+ type: "channel.unpinned",
1032
+ cid: this.cid,
1033
+ channel: this.data
1034
+ });
1035
+ try {
1036
+ return await this.getClient().unpinChannel(this.type, this.id);
1037
+ } catch (e) {
1038
+ if (this.data) this.data.is_pinned = true;
1039
+ this.getClient().dispatchEvent({
1040
+ type: "channel.pinned",
1041
+ cid: this.cid,
1042
+ channel: this.data
1043
+ });
1044
+ throw e;
1045
+ }
1046
+ }
1009
1047
  async editMessage(oldMessageID, message) {
1010
1048
  return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
1011
1049
  message
@@ -1911,6 +1949,18 @@ var Channel = class {
1911
1949
  delete channelState.members[event.user.id];
1912
1950
  }
1913
1951
  break;
1952
+ case "channel.topic.enabled":
1953
+ if (channel.data) {
1954
+ channel.data.topics_enabled = true;
1955
+ }
1956
+ channelState.topics = channelState.topics || [];
1957
+ break;
1958
+ case "channel.topic.disabled":
1959
+ if (channel.data) {
1960
+ channel.data.topics_enabled = false;
1961
+ }
1962
+ channelState.topics = [];
1963
+ break;
1914
1964
  case "channel.updated":
1915
1965
  if (event.channel) {
1916
1966
  channel.data = {
@@ -2049,7 +2099,12 @@ var Channel = class {
2049
2099
  const topic = this.getClient().channel(event.channel_type || "", event.channel_id || "");
2050
2100
  topic.data = event.channel;
2051
2101
  topic._initializeState(topicState, "latest");
2052
- channelState.topics?.unshift(topic);
2102
+ if (!channelState.topics) {
2103
+ channelState.topics = [];
2104
+ }
2105
+ if (!channelState.topics.some((t) => t.cid === topic.cid)) {
2106
+ channelState.topics.push(topic);
2107
+ }
2053
2108
  break;
2054
2109
  case "channel.topic.closed":
2055
2110
  if (channel.data) {
@@ -2868,7 +2923,6 @@ var ErmisChat = class _ErmisChat {
2868
2923
  params.avatar = user.avatar;
2869
2924
  }
2870
2925
  const url = this.userBaseURL + "/get_token/external_auth";
2871
- const query = new URLSearchParams(params).toString();
2872
2926
  const headers = {
2873
2927
  "Content-Type": "application/json"
2874
2928
  };
@@ -2876,21 +2930,21 @@ var ErmisChat = class _ErmisChat {
2876
2930
  const tokenStr = typeof token === "string" && token.startsWith("Bearer ") ? token : `Bearer ${token}`;
2877
2931
  headers["Authorization"] = tokenStr;
2878
2932
  }
2879
- const response = await fetch(`${url}?${query}`, {
2880
- method: "GET",
2881
- headers
2882
- });
2883
- if (!response.ok) {
2884
- let errorMsg = "";
2885
- try {
2886
- const errorData = await response.json();
2887
- errorMsg = errorData.message || JSON.stringify(errorData);
2888
- } catch {
2889
- errorMsg = await response.text();
2933
+ try {
2934
+ const response = await this.axiosInstance.get(url, {
2935
+ params,
2936
+ headers
2937
+ });
2938
+ return response.data;
2939
+ } catch (error) {
2940
+ let errorMsg = "Failed to fetch external auth token";
2941
+ if (error.response && error.response.data) {
2942
+ errorMsg = error.response.data.message || JSON.stringify(error.response.data);
2943
+ } else if (error.message) {
2944
+ errorMsg = error.message;
2890
2945
  }
2891
2946
  throw new Error(errorMsg);
2892
2947
  }
2893
- return await response.json();
2894
2948
  }
2895
2949
  /**
2896
2950
  * Connects a user to the Ermis network and establishes the WebSocket connection.
@@ -2901,19 +2955,24 @@ var ErmisChat = class _ErmisChat {
2901
2955
  * @param extenal_auth - Set to `true` to use your custom backend external authentication flow.
2902
2956
  * @returns A promise resolving to the API connection response once authenticated.
2903
2957
  */
2904
- connectUser = async (user, userTokenOrProvider, extenal_auth) => {
2958
+ connectUser = async (user, userTokenOrProvider, external_auth) => {
2905
2959
  this.logger("info", "client:connectUser() - started", {
2906
2960
  tags: ["connection", "client"]
2907
2961
  });
2908
2962
  if (!user.id) {
2909
2963
  throw new Error('The "id" field on the user is missing');
2910
2964
  }
2911
- if (extenal_auth) {
2965
+ let connectionUser = user;
2966
+ let connectionToken = userTokenOrProvider;
2967
+ if (external_auth) {
2912
2968
  const external_auth_token = await this.getExternalAuthToken(user, userTokenOrProvider);
2913
- userTokenOrProvider = external_auth_token.token;
2914
- user.id = external_auth_token.user_id;
2969
+ connectionToken = external_auth_token.token;
2970
+ connectionUser = {
2971
+ ...user,
2972
+ id: external_auth_token.user_id
2973
+ };
2915
2974
  }
2916
- if (this.userID === user.id && this.setUserPromise) {
2975
+ if (this.userID === connectionUser.id && this.setUserPromise) {
2917
2976
  console.warn(
2918
2977
  "Consecutive calls to connectUser is detected, ideally you should only call this function once in your app."
2919
2978
  );
@@ -2929,10 +2988,10 @@ var ErmisChat = class _ErmisChat {
2929
2988
  'Please do not use connectUser server side. connectUser impacts MAU and concurrent connection usage and thus your bill. If you have a valid use-case, add "allowServerSideConnect: true" to the client options to disable this warning.'
2930
2989
  );
2931
2990
  }
2932
- this.userID = user.id;
2933
- const setTokenPromise = this._setToken(user, userTokenOrProvider);
2934
- this._setUser(user);
2935
- this.state.updateUser({ id: user.id, name: user?.name || user.id, avatar: user?.avatar || "" });
2991
+ this.userID = connectionUser.id;
2992
+ const setTokenPromise = this._setToken(connectionUser, connectionToken);
2993
+ this._setUser(connectionUser);
2994
+ this.state.updateUser({ id: connectionUser.id, name: connectionUser?.name || connectionUser.id, avatar: connectionUser?.avatar || "" });
2936
2995
  const wsPromise = this.openConnection();
2937
2996
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
2938
2997
  (result) => result[1]
@@ -3138,6 +3197,20 @@ var ErmisChat = class _ErmisChat {
3138
3197
  }
3139
3198
  });
3140
3199
  }
3200
+ /**
3201
+ * Downloads a media file as a Blob via the SDK's configured axiosInstance.
3202
+ * This avoids CORS issues that arise when using `fetch()` directly from the browser,
3203
+ * because axios is routed through the SDK's authenticated transport layer.
3204
+ *
3205
+ * @param url - The full URL of the media file to download.
3206
+ * @returns A Blob of the file content.
3207
+ */
3208
+ async downloadMedia(url) {
3209
+ const response = await this.axiosInstance.get(url, {
3210
+ responseType: "blob"
3211
+ });
3212
+ return response.data;
3213
+ }
3141
3214
  errorFromResponse(response) {
3142
3215
  let err;
3143
3216
  err = new ErrorFromResponse(`ErmisChat error HTTP code: ${response.status}`);
@@ -3158,7 +3231,7 @@ var ErmisChat = class _ErmisChat {
3158
3231
  }
3159
3232
  dispatchEvent = (event) => {
3160
3233
  if (!event.received_at) event.received_at = /* @__PURE__ */ new Date();
3161
- if (event.type === "channel.created") {
3234
+ if (event.type === "channel.created" || event.type === "channel.topic.created") {
3162
3235
  this._handleChannelCreatedEvent(event).then(() => {
3163
3236
  this._afterDispatchEvent(event);
3164
3237
  });
@@ -3292,6 +3365,79 @@ var ErmisChat = class _ErmisChat {
3292
3365
  });
3293
3366
  }
3294
3367
  }
3368
+ if (event.type === "message.new" && event.channel_type === "topic") {
3369
+ postListenerCallbacks.push(() => {
3370
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3371
+ if (parentCid && this.activeChannels[parentCid]) {
3372
+ const parentChannel = this.activeChannels[parentCid];
3373
+ if (parentChannel.state.topics) {
3374
+ parentChannel.state.topics.sort((a, b) => {
3375
+ const aLatest = a.state?.latestMessages?.[a.state.latestMessages.length - 1]?.created_at;
3376
+ const bLatest = b.state?.latestMessages?.[b.state.latestMessages.length - 1]?.created_at;
3377
+ const aTime = aLatest ? new Date(aLatest).getTime() : 0;
3378
+ const bTime = bLatest ? new Date(bLatest).getTime() : 0;
3379
+ return bTime - aTime;
3380
+ });
3381
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3382
+ }
3383
+ }
3384
+ });
3385
+ }
3386
+ if (event.type === "channel.topic.updated") {
3387
+ postListenerCallbacks.push(() => {
3388
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3389
+ if (parentCid && this.activeChannels[parentCid]) {
3390
+ const parentChannel = this.activeChannels[parentCid];
3391
+ if (parentChannel.state?.topics && event.channel) {
3392
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3393
+ if (topicIndex !== -1) {
3394
+ const t = parentChannel.state.topics[topicIndex];
3395
+ if (t.data) {
3396
+ t.data = { ...t.data, ...event.channel };
3397
+ } else if (t.channel) {
3398
+ t.channel = { ...t.channel, ...event.channel };
3399
+ } else {
3400
+ Object.assign(t, event.channel);
3401
+ }
3402
+ }
3403
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3404
+ }
3405
+ }
3406
+ if (event.cid && this.activeChannels[event.cid]) {
3407
+ const topicChannel = this.activeChannels[event.cid];
3408
+ if (event.channel) {
3409
+ topicChannel.data = { ...topicChannel.data, ...event.channel };
3410
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3411
+ }
3412
+ }
3413
+ });
3414
+ }
3415
+ if (event.type === "channel.topic.closed" || event.type === "channel.topic.reopen") {
3416
+ postListenerCallbacks.push(() => {
3417
+ const isClosed = event.type === "channel.topic.closed";
3418
+ const parentCid = event.parent_cid;
3419
+ if (parentCid && this.activeChannels[parentCid]) {
3420
+ const parentChannel = this.activeChannels[parentCid];
3421
+ if (parentChannel.state?.topics) {
3422
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3423
+ if (topicIndex !== -1) {
3424
+ const t = parentChannel.state.topics[topicIndex];
3425
+ if (t.data) t.data.is_closed_topic = isClosed;
3426
+ else if (t.channel) t.channel.is_closed_topic = isClosed;
3427
+ else t.is_closed_topic = isClosed;
3428
+ }
3429
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3430
+ }
3431
+ }
3432
+ if (event.cid && this.activeChannels[event.cid]) {
3433
+ const topicChannel = this.activeChannels[event.cid];
3434
+ if (topicChannel.data) {
3435
+ topicChannel.data.is_closed_topic = isClosed;
3436
+ }
3437
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3438
+ }
3439
+ });
3440
+ }
3295
3441
  if (event.type === "connection.recovered") {
3296
3442
  postListenerCallbacks.push(() => {
3297
3443
  Object.values(this.activeChannels).forEach((channel) => {
@@ -3529,7 +3675,14 @@ var ErmisChat = class _ErmisChat {
3529
3675
  _updateProjectID(project_id) {
3530
3676
  this.projectId = project_id;
3531
3677
  }
3532
- async uploadFile(file) {
3678
+ /**
3679
+ * Uploads a new avatar image for the current user.
3680
+ * The user's avatar URL is automatically updated in both the client and the local state.
3681
+ *
3682
+ * @param file - The image file to upload.
3683
+ * @returns The response containing the new avatar URL.
3684
+ */
3685
+ async uploadAvatar(file) {
3533
3686
  const formData = new FormData();
3534
3687
  formData.append("avatar", file);
3535
3688
  let response = await this.post(this.userBaseURL + "/users/upload", formData, {
@@ -3544,12 +3697,8 @@ var ErmisChat = class _ErmisChat {
3544
3697
  }
3545
3698
  return response;
3546
3699
  }
3547
- async updateProfile(name, about_me) {
3548
- let body = {
3549
- name,
3550
- about_me
3551
- };
3552
- let response = await this.patch(this.userBaseURL + "/users/update", body);
3700
+ async updateProfile(updates) {
3701
+ let response = await this.patch(this.userBaseURL + "/users/update", updates);
3553
3702
  this.user = response;
3554
3703
  this.state.updateUser(response);
3555
3704
  return response;
@@ -3702,45 +3851,55 @@ var ErmisChat = class _ErmisChat {
3702
3851
  return channel;
3703
3852
  };
3704
3853
  /**
3705
- * Creates an interactive `meeting` Channel locally, and immediately creates it on the server.
3706
- * Consumers can customize the `name` field. `members` and `public` fields are constrained.
3854
+ * Creates a quick channel and immediately registers it on the server.
3855
+ * Quick channels are public group channels that anyone can join without an invitation.
3856
+ * The creator is added as the first member automatically.
3707
3857
  *
3708
- * @param name - The custom name for the meeting channel.
3858
+ * @param name - An optional display name for the channel.
3709
3859
  * @returns A promise that resolves to the created `Channel` object.
3710
3860
  */
3711
- async createMeetingChannel(name) {
3861
+ async createQuickChannel(name) {
3712
3862
  if (!this.userID) {
3713
3863
  throw Error("Call connectUser before creating a channel");
3714
3864
  }
3865
+ const now = /* @__PURE__ */ new Date();
3866
+ const formattedDate = new Intl.DateTimeFormat("en-US", {
3867
+ month: "short",
3868
+ day: "2-digit",
3869
+ year: "numeric",
3870
+ hour: "2-digit",
3871
+ minute: "2-digit",
3872
+ hour12: false
3873
+ }).format(now);
3715
3874
  const payload = {
3716
- name: name || `Meeting Public - ${(/* @__PURE__ */ new Date()).toISOString()}`,
3875
+ name: name || `Quick Channel - ${formattedDate}`,
3717
3876
  members: [this.userID],
3718
3877
  public: true
3719
3878
  };
3720
- const meetingChannel = this.channel("meeting", payload);
3721
- await meetingChannel.create();
3722
- return meetingChannel;
3879
+ const quickChannel = this.channel("meeting", payload);
3880
+ await quickChannel.create();
3881
+ return quickChannel;
3723
3882
  }
3724
3883
  /**
3725
- * Joins a `meeting` channel.
3726
- * It queries/watches the channel to see if caller is already a member.
3727
- * If not, it accepts the invite to join the channel, then watches it again to reflect changes.
3884
+ * Joins a quick channel by its ID.
3885
+ * Automatically checks whether the caller is already a member.
3886
+ * If not, it joins the channel and synchronizes state.
3728
3887
  *
3729
- * @param channelId - The ID of the meeting channel to join.
3888
+ * @param channelId - The ID of the quick channel to join.
3730
3889
  * @returns A promise that resolves to the joined `Channel` object.
3731
3890
  */
3732
- async joinMeetingChannel(channelId) {
3891
+ async joinQuickChannel(channelId) {
3733
3892
  if (!this.userID) {
3734
3893
  throw Error("Call connectUser before joining a channel");
3735
3894
  }
3736
- const meetingChannel = this.channel("meeting", channelId);
3737
- await meetingChannel.watch();
3738
- const isMember = meetingChannel.state.members && meetingChannel.state.members[this.userID];
3895
+ const quickChannel = this.channel("meeting", channelId);
3896
+ await quickChannel.watch();
3897
+ const isMember = quickChannel.state.members && quickChannel.state.members[this.userID];
3739
3898
  if (!isMember) {
3740
- await meetingChannel.acceptInvite("join");
3741
- await meetingChannel.watch();
3899
+ await quickChannel.acceptInvite("join");
3900
+ await quickChannel.watch();
3742
3901
  }
3743
- return meetingChannel;
3902
+ return quickChannel;
3744
3903
  }
3745
3904
  _normalizeExpiration(timeoutOrExpirationDate) {
3746
3905
  let pinExpires = null;
@@ -3756,7 +3915,7 @@ var ErmisChat = class _ErmisChat {
3756
3915
  return pinExpires;
3757
3916
  }
3758
3917
  getUserAgent() {
3759
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.6"}`;
3918
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.8"}`;
3760
3919
  }
3761
3920
  setUserAgent(userAgent) {
3762
3921
  this.userAgent = userAgent;
@@ -7115,7 +7274,7 @@ var ErmisAuthProvider = class {
7115
7274
  return data;
7116
7275
  }
7117
7276
  getUserAgent() {
7118
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.6"}`;
7277
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.8"}`;
7119
7278
  }
7120
7279
  setUserAgent(userAgent) {
7121
7280
  this.userAgent = userAgent;