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