@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.
@@ -1068,6 +1068,44 @@ var Channel = class {
1068
1068
  this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/unpin`
1069
1069
  );
1070
1070
  }
1071
+ async pin() {
1072
+ if (this.data) this.data.is_pinned = true;
1073
+ this.getClient().dispatchEvent({
1074
+ type: "channel.pinned",
1075
+ cid: this.cid,
1076
+ channel: this.data
1077
+ });
1078
+ try {
1079
+ return await this.getClient().pinChannel(this.type, this.id);
1080
+ } catch (e) {
1081
+ if (this.data) this.data.is_pinned = false;
1082
+ this.getClient().dispatchEvent({
1083
+ type: "channel.unpinned",
1084
+ cid: this.cid,
1085
+ channel: this.data
1086
+ });
1087
+ throw e;
1088
+ }
1089
+ }
1090
+ async unpin() {
1091
+ if (this.data) this.data.is_pinned = false;
1092
+ this.getClient().dispatchEvent({
1093
+ type: "channel.unpinned",
1094
+ cid: this.cid,
1095
+ channel: this.data
1096
+ });
1097
+ try {
1098
+ return await this.getClient().unpinChannel(this.type, this.id);
1099
+ } catch (e) {
1100
+ if (this.data) this.data.is_pinned = true;
1101
+ this.getClient().dispatchEvent({
1102
+ type: "channel.pinned",
1103
+ cid: this.cid,
1104
+ channel: this.data
1105
+ });
1106
+ throw e;
1107
+ }
1108
+ }
1071
1109
  async editMessage(oldMessageID, message) {
1072
1110
  return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
1073
1111
  message
@@ -1973,6 +2011,18 @@ var Channel = class {
1973
2011
  delete channelState.members[event.user.id];
1974
2012
  }
1975
2013
  break;
2014
+ case "channel.topic.enabled":
2015
+ if (channel.data) {
2016
+ channel.data.topics_enabled = true;
2017
+ }
2018
+ channelState.topics = channelState.topics || [];
2019
+ break;
2020
+ case "channel.topic.disabled":
2021
+ if (channel.data) {
2022
+ channel.data.topics_enabled = false;
2023
+ }
2024
+ channelState.topics = [];
2025
+ break;
1976
2026
  case "channel.updated":
1977
2027
  if (event.channel) {
1978
2028
  channel.data = {
@@ -2111,7 +2161,12 @@ var Channel = class {
2111
2161
  const topic = this.getClient().channel(event.channel_type || "", event.channel_id || "");
2112
2162
  topic.data = event.channel;
2113
2163
  topic._initializeState(topicState, "latest");
2114
- channelState.topics?.unshift(topic);
2164
+ if (!channelState.topics) {
2165
+ channelState.topics = [];
2166
+ }
2167
+ if (!channelState.topics.some((t) => t.cid === topic.cid)) {
2168
+ channelState.topics.push(topic);
2169
+ }
2115
2170
  break;
2116
2171
  case "channel.topic.closed":
2117
2172
  if (channel.data) {
@@ -2930,7 +2985,6 @@ var ErmisChat = class _ErmisChat {
2930
2985
  params.avatar = user.avatar;
2931
2986
  }
2932
2987
  const url = this.userBaseURL + "/get_token/external_auth";
2933
- const query = new URLSearchParams(params).toString();
2934
2988
  const headers = {
2935
2989
  "Content-Type": "application/json"
2936
2990
  };
@@ -2938,21 +2992,21 @@ var ErmisChat = class _ErmisChat {
2938
2992
  const tokenStr = typeof token === "string" && token.startsWith("Bearer ") ? token : `Bearer ${token}`;
2939
2993
  headers["Authorization"] = tokenStr;
2940
2994
  }
2941
- const response = await fetch(`${url}?${query}`, {
2942
- method: "GET",
2943
- headers
2944
- });
2945
- if (!response.ok) {
2946
- let errorMsg = "";
2947
- try {
2948
- const errorData = await response.json();
2949
- errorMsg = errorData.message || JSON.stringify(errorData);
2950
- } catch {
2951
- errorMsg = await response.text();
2995
+ try {
2996
+ const response = await this.axiosInstance.get(url, {
2997
+ params,
2998
+ headers
2999
+ });
3000
+ return response.data;
3001
+ } catch (error) {
3002
+ let errorMsg = "Failed to fetch external auth token";
3003
+ if (error.response && error.response.data) {
3004
+ errorMsg = error.response.data.message || JSON.stringify(error.response.data);
3005
+ } else if (error.message) {
3006
+ errorMsg = error.message;
2952
3007
  }
2953
3008
  throw new Error(errorMsg);
2954
3009
  }
2955
- return await response.json();
2956
3010
  }
2957
3011
  /**
2958
3012
  * Connects a user to the Ermis network and establishes the WebSocket connection.
@@ -2963,19 +3017,24 @@ var ErmisChat = class _ErmisChat {
2963
3017
  * @param extenal_auth - Set to `true` to use your custom backend external authentication flow.
2964
3018
  * @returns A promise resolving to the API connection response once authenticated.
2965
3019
  */
2966
- connectUser = async (user, userTokenOrProvider, extenal_auth) => {
3020
+ connectUser = async (user, userTokenOrProvider, external_auth) => {
2967
3021
  this.logger("info", "client:connectUser() - started", {
2968
3022
  tags: ["connection", "client"]
2969
3023
  });
2970
3024
  if (!user.id) {
2971
3025
  throw new Error('The "id" field on the user is missing');
2972
3026
  }
2973
- if (extenal_auth) {
3027
+ let connectionUser = user;
3028
+ let connectionToken = userTokenOrProvider;
3029
+ if (external_auth) {
2974
3030
  const external_auth_token = await this.getExternalAuthToken(user, userTokenOrProvider);
2975
- userTokenOrProvider = external_auth_token.token;
2976
- user.id = external_auth_token.user_id;
3031
+ connectionToken = external_auth_token.token;
3032
+ connectionUser = {
3033
+ ...user,
3034
+ id: external_auth_token.user_id
3035
+ };
2977
3036
  }
2978
- if (this.userID === user.id && this.setUserPromise) {
3037
+ if (this.userID === connectionUser.id && this.setUserPromise) {
2979
3038
  console.warn(
2980
3039
  "Consecutive calls to connectUser is detected, ideally you should only call this function once in your app."
2981
3040
  );
@@ -2991,10 +3050,10 @@ var ErmisChat = class _ErmisChat {
2991
3050
  '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.'
2992
3051
  );
2993
3052
  }
2994
- this.userID = user.id;
2995
- const setTokenPromise = this._setToken(user, userTokenOrProvider);
2996
- this._setUser(user);
2997
- this.state.updateUser({ id: user.id, name: user?.name || user.id, avatar: user?.avatar || "" });
3053
+ this.userID = connectionUser.id;
3054
+ const setTokenPromise = this._setToken(connectionUser, connectionToken);
3055
+ this._setUser(connectionUser);
3056
+ this.state.updateUser({ id: connectionUser.id, name: connectionUser?.name || connectionUser.id, avatar: connectionUser?.avatar || "" });
2998
3057
  const wsPromise = this.openConnection();
2999
3058
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
3000
3059
  (result) => result[1]
@@ -3200,6 +3259,20 @@ var ErmisChat = class _ErmisChat {
3200
3259
  }
3201
3260
  });
3202
3261
  }
3262
+ /**
3263
+ * Downloads a media file as a Blob via the SDK's configured axiosInstance.
3264
+ * This avoids CORS issues that arise when using `fetch()` directly from the browser,
3265
+ * because axios is routed through the SDK's authenticated transport layer.
3266
+ *
3267
+ * @param url - The full URL of the media file to download.
3268
+ * @returns A Blob of the file content.
3269
+ */
3270
+ async downloadMedia(url) {
3271
+ const response = await this.axiosInstance.get(url, {
3272
+ responseType: "blob"
3273
+ });
3274
+ return response.data;
3275
+ }
3203
3276
  errorFromResponse(response) {
3204
3277
  let err;
3205
3278
  err = new ErrorFromResponse(`ErmisChat error HTTP code: ${response.status}`);
@@ -3220,7 +3293,7 @@ var ErmisChat = class _ErmisChat {
3220
3293
  }
3221
3294
  dispatchEvent = (event) => {
3222
3295
  if (!event.received_at) event.received_at = /* @__PURE__ */ new Date();
3223
- if (event.type === "channel.created") {
3296
+ if (event.type === "channel.created" || event.type === "channel.topic.created") {
3224
3297
  this._handleChannelCreatedEvent(event).then(() => {
3225
3298
  this._afterDispatchEvent(event);
3226
3299
  });
@@ -3354,6 +3427,79 @@ var ErmisChat = class _ErmisChat {
3354
3427
  });
3355
3428
  }
3356
3429
  }
3430
+ if (event.type === "message.new" && event.channel_type === "topic") {
3431
+ postListenerCallbacks.push(() => {
3432
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3433
+ if (parentCid && this.activeChannels[parentCid]) {
3434
+ const parentChannel = this.activeChannels[parentCid];
3435
+ if (parentChannel.state.topics) {
3436
+ parentChannel.state.topics.sort((a, b) => {
3437
+ const aLatest = a.state?.latestMessages?.[a.state.latestMessages.length - 1]?.created_at;
3438
+ const bLatest = b.state?.latestMessages?.[b.state.latestMessages.length - 1]?.created_at;
3439
+ const aTime = aLatest ? new Date(aLatest).getTime() : 0;
3440
+ const bTime = bLatest ? new Date(bLatest).getTime() : 0;
3441
+ return bTime - aTime;
3442
+ });
3443
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3444
+ }
3445
+ }
3446
+ });
3447
+ }
3448
+ if (event.type === "channel.topic.updated") {
3449
+ postListenerCallbacks.push(() => {
3450
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3451
+ if (parentCid && this.activeChannels[parentCid]) {
3452
+ const parentChannel = this.activeChannels[parentCid];
3453
+ if (parentChannel.state?.topics && event.channel) {
3454
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3455
+ if (topicIndex !== -1) {
3456
+ const t = parentChannel.state.topics[topicIndex];
3457
+ if (t.data) {
3458
+ t.data = { ...t.data, ...event.channel };
3459
+ } else if (t.channel) {
3460
+ t.channel = { ...t.channel, ...event.channel };
3461
+ } else {
3462
+ Object.assign(t, event.channel);
3463
+ }
3464
+ }
3465
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3466
+ }
3467
+ }
3468
+ if (event.cid && this.activeChannels[event.cid]) {
3469
+ const topicChannel = this.activeChannels[event.cid];
3470
+ if (event.channel) {
3471
+ topicChannel.data = { ...topicChannel.data, ...event.channel };
3472
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3473
+ }
3474
+ }
3475
+ });
3476
+ }
3477
+ if (event.type === "channel.topic.closed" || event.type === "channel.topic.reopen") {
3478
+ postListenerCallbacks.push(() => {
3479
+ const isClosed = event.type === "channel.topic.closed";
3480
+ const parentCid = event.parent_cid;
3481
+ if (parentCid && this.activeChannels[parentCid]) {
3482
+ const parentChannel = this.activeChannels[parentCid];
3483
+ if (parentChannel.state?.topics) {
3484
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3485
+ if (topicIndex !== -1) {
3486
+ const t = parentChannel.state.topics[topicIndex];
3487
+ if (t.data) t.data.is_closed_topic = isClosed;
3488
+ else if (t.channel) t.channel.is_closed_topic = isClosed;
3489
+ else t.is_closed_topic = isClosed;
3490
+ }
3491
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3492
+ }
3493
+ }
3494
+ if (event.cid && this.activeChannels[event.cid]) {
3495
+ const topicChannel = this.activeChannels[event.cid];
3496
+ if (topicChannel.data) {
3497
+ topicChannel.data.is_closed_topic = isClosed;
3498
+ }
3499
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3500
+ }
3501
+ });
3502
+ }
3357
3503
  if (event.type === "connection.recovered") {
3358
3504
  postListenerCallbacks.push(() => {
3359
3505
  Object.values(this.activeChannels).forEach((channel) => {
@@ -3591,7 +3737,14 @@ var ErmisChat = class _ErmisChat {
3591
3737
  _updateProjectID(project_id) {
3592
3738
  this.projectId = project_id;
3593
3739
  }
3594
- async uploadFile(file) {
3740
+ /**
3741
+ * Uploads a new avatar image for the current user.
3742
+ * The user's avatar URL is automatically updated in both the client and the local state.
3743
+ *
3744
+ * @param file - The image file to upload.
3745
+ * @returns The response containing the new avatar URL.
3746
+ */
3747
+ async uploadAvatar(file) {
3595
3748
  const formData = new FormData();
3596
3749
  formData.append("avatar", file);
3597
3750
  let response = await this.post(this.userBaseURL + "/users/upload", formData, {
@@ -3606,12 +3759,8 @@ var ErmisChat = class _ErmisChat {
3606
3759
  }
3607
3760
  return response;
3608
3761
  }
3609
- async updateProfile(name, about_me) {
3610
- let body = {
3611
- name,
3612
- about_me
3613
- };
3614
- let response = await this.patch(this.userBaseURL + "/users/update", body);
3762
+ async updateProfile(updates) {
3763
+ let response = await this.patch(this.userBaseURL + "/users/update", updates);
3615
3764
  this.user = response;
3616
3765
  this.state.updateUser(response);
3617
3766
  return response;
@@ -3764,45 +3913,55 @@ var ErmisChat = class _ErmisChat {
3764
3913
  return channel;
3765
3914
  };
3766
3915
  /**
3767
- * Creates an interactive `meeting` Channel locally, and immediately creates it on the server.
3768
- * Consumers can customize the `name` field. `members` and `public` fields are constrained.
3916
+ * Creates a quick channel and immediately registers it on the server.
3917
+ * Quick channels are public group channels that anyone can join without an invitation.
3918
+ * The creator is added as the first member automatically.
3769
3919
  *
3770
- * @param name - The custom name for the meeting channel.
3920
+ * @param name - An optional display name for the channel.
3771
3921
  * @returns A promise that resolves to the created `Channel` object.
3772
3922
  */
3773
- async createMeetingChannel(name) {
3923
+ async createQuickChannel(name) {
3774
3924
  if (!this.userID) {
3775
3925
  throw Error("Call connectUser before creating a channel");
3776
3926
  }
3927
+ const now = /* @__PURE__ */ new Date();
3928
+ const formattedDate = new Intl.DateTimeFormat("en-US", {
3929
+ month: "short",
3930
+ day: "2-digit",
3931
+ year: "numeric",
3932
+ hour: "2-digit",
3933
+ minute: "2-digit",
3934
+ hour12: false
3935
+ }).format(now);
3777
3936
  const payload = {
3778
- name: name || `Meeting Public - ${(/* @__PURE__ */ new Date()).toISOString()}`,
3937
+ name: name || `Quick Channel - ${formattedDate}`,
3779
3938
  members: [this.userID],
3780
3939
  public: true
3781
3940
  };
3782
- const meetingChannel = this.channel("meeting", payload);
3783
- await meetingChannel.create();
3784
- return meetingChannel;
3941
+ const quickChannel = this.channel("meeting", payload);
3942
+ await quickChannel.create();
3943
+ return quickChannel;
3785
3944
  }
3786
3945
  /**
3787
- * Joins a `meeting` channel.
3788
- * It queries/watches the channel to see if caller is already a member.
3789
- * If not, it accepts the invite to join the channel, then watches it again to reflect changes.
3946
+ * Joins a quick channel by its ID.
3947
+ * Automatically checks whether the caller is already a member.
3948
+ * If not, it joins the channel and synchronizes state.
3790
3949
  *
3791
- * @param channelId - The ID of the meeting channel to join.
3950
+ * @param channelId - The ID of the quick channel to join.
3792
3951
  * @returns A promise that resolves to the joined `Channel` object.
3793
3952
  */
3794
- async joinMeetingChannel(channelId) {
3953
+ async joinQuickChannel(channelId) {
3795
3954
  if (!this.userID) {
3796
3955
  throw Error("Call connectUser before joining a channel");
3797
3956
  }
3798
- const meetingChannel = this.channel("meeting", channelId);
3799
- await meetingChannel.watch();
3800
- const isMember = meetingChannel.state.members && meetingChannel.state.members[this.userID];
3957
+ const quickChannel = this.channel("meeting", channelId);
3958
+ await quickChannel.watch();
3959
+ const isMember = quickChannel.state.members && quickChannel.state.members[this.userID];
3801
3960
  if (!isMember) {
3802
- await meetingChannel.acceptInvite("join");
3803
- await meetingChannel.watch();
3961
+ await quickChannel.acceptInvite("join");
3962
+ await quickChannel.watch();
3804
3963
  }
3805
- return meetingChannel;
3964
+ return quickChannel;
3806
3965
  }
3807
3966
  _normalizeExpiration(timeoutOrExpirationDate) {
3808
3967
  let pinExpires = null;
@@ -3818,7 +3977,7 @@ var ErmisChat = class _ErmisChat {
3818
3977
  return pinExpires;
3819
3978
  }
3820
3979
  getUserAgent() {
3821
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.6"}`;
3980
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.8"}`;
3822
3981
  }
3823
3982
  setUserAgent(userAgent) {
3824
3983
  this.userAgent = userAgent;
@@ -7176,7 +7335,7 @@ var ErmisAuthProvider = class {
7176
7335
  return data;
7177
7336
  }
7178
7337
  getUserAgent() {
7179
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.6"}`;
7338
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.8"}`;
7180
7339
  }
7181
7340
  setUserAgent(userAgent) {
7182
7341
  this.userAgent = userAgent;