@ermis-network/ermis-chat-sdk 1.0.5 → 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/dist/index.mjs CHANGED
@@ -936,10 +936,9 @@ var Channel = class {
936
936
  };
937
937
  this.state.addMessageSorted(optimisticMessage);
938
938
  try {
939
- return await this.getClient().post(
940
- this._channelURL() + "/message",
941
- { message: { ...message } }
942
- );
939
+ return await this.getClient().post(this._channelURL() + "/message", {
940
+ message: { ...message }
941
+ });
943
942
  } catch (error) {
944
943
  const isOfflineError = !error.response || error.code === "ERR_NETWORK" || error.isWSFailure || !this.getClient().wsConnection?.isHealthy;
945
944
  const statusToSet = isOfflineError ? "failed_offline" : "error";
@@ -964,10 +963,9 @@ var Channel = class {
964
963
  messagePayload.show_in_channel = stateMsg.show_in_channel;
965
964
  }
966
965
  try {
967
- return await this.getClient().post(
968
- this._channelURL() + "/message",
969
- { message: messagePayload }
970
- );
966
+ return await this.getClient().post(this._channelURL() + "/message", {
967
+ message: messagePayload
968
+ });
971
969
  } catch (error) {
972
970
  const isOfflineError = !error.response || error.code === "ERR_NETWORK" || error.isWSFailure || !this.getClient().wsConnection?.isHealthy;
973
971
  this.state.updateMessageStatus(messageId, isOfflineError ? "failed_offline" : "error");
@@ -1008,6 +1006,44 @@ var Channel = class {
1008
1006
  this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/unpin`
1009
1007
  );
1010
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
+ }
1011
1047
  async editMessage(oldMessageID, message) {
1012
1048
  return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
1013
1049
  message
@@ -1034,9 +1070,7 @@ var Channel = class {
1034
1070
  return file;
1035
1071
  });
1036
1072
  const uploadResults = await Promise.allSettled(
1037
- processedFiles.map(
1038
- (file) => this.sendFile(file, file.name, file.type)
1039
- )
1073
+ processedFiles.map((file) => this.sendFile(file, file.name, file.type))
1040
1074
  );
1041
1075
  const thumbUrls = /* @__PURE__ */ new Map();
1042
1076
  const thumbPromises = [];
@@ -1048,11 +1082,7 @@ var Channel = class {
1048
1082
  try {
1049
1083
  const thumbBlob = await this.getThumbBlobVideo(files[i]);
1050
1084
  if (thumbBlob) {
1051
- const thumbFile = new File(
1052
- [thumbBlob],
1053
- `thumb_${processedFiles[i].name}.jpg`,
1054
- { type: "image/jpeg" }
1055
- );
1085
+ const thumbFile = new File([thumbBlob], `thumb_${processedFiles[i].name}.jpg`, { type: "image/jpeg" });
1056
1086
  const thumbResp = await this.sendFile(thumbFile, thumbFile.name, "image/jpeg");
1057
1087
  thumbUrls.set(i, thumbResp.file);
1058
1088
  }
@@ -1070,9 +1100,7 @@ var Channel = class {
1070
1100
  const uploadedUrl = result.value.file;
1071
1101
  const thumbUrl = thumbUrls.get(i);
1072
1102
  const voiceMeta = options?.voiceMetadata?.get(i);
1073
- attachments.push(
1074
- buildAttachmentPayload(processedFiles[i], uploadedUrl, thumbUrl, voiceMeta)
1075
- );
1103
+ attachments.push(buildAttachmentPayload(processedFiles[i], uploadedUrl, thumbUrl, voiceMeta));
1076
1104
  } else {
1077
1105
  failedFiles.push({
1078
1106
  file: files[i],
@@ -1420,7 +1448,7 @@ var Channel = class {
1420
1448
  if (this.id) {
1421
1449
  queryURL += `/${this.id}`;
1422
1450
  } else {
1423
- if (this.type === "team") {
1451
+ if (this.type === "team" || this.type === "meeting") {
1424
1452
  const uuid = randomId();
1425
1453
  this.id = `${project_id}:${uuid}`;
1426
1454
  queryURL += `/${this.id}`;
@@ -1921,6 +1949,18 @@ var Channel = class {
1921
1949
  delete channelState.members[event.user.id];
1922
1950
  }
1923
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;
1924
1964
  case "channel.updated":
1925
1965
  if (event.channel) {
1926
1966
  channel.data = {
@@ -2059,7 +2099,12 @@ var Channel = class {
2059
2099
  const topic = this.getClient().channel(event.channel_type || "", event.channel_id || "");
2060
2100
  topic.data = event.channel;
2061
2101
  topic._initializeState(topicState, "latest");
2062
- 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
+ }
2063
2108
  break;
2064
2109
  case "channel.topic.closed":
2065
2110
  if (channel.data) {
@@ -2878,7 +2923,6 @@ var ErmisChat = class _ErmisChat {
2878
2923
  params.avatar = user.avatar;
2879
2924
  }
2880
2925
  const url = this.userBaseURL + "/get_token/external_auth";
2881
- const query = new URLSearchParams(params).toString();
2882
2926
  const headers = {
2883
2927
  "Content-Type": "application/json"
2884
2928
  };
@@ -2886,21 +2930,21 @@ var ErmisChat = class _ErmisChat {
2886
2930
  const tokenStr = typeof token === "string" && token.startsWith("Bearer ") ? token : `Bearer ${token}`;
2887
2931
  headers["Authorization"] = tokenStr;
2888
2932
  }
2889
- const response = await fetch(`${url}?${query}`, {
2890
- method: "GET",
2891
- headers
2892
- });
2893
- if (!response.ok) {
2894
- let errorMsg = "";
2895
- try {
2896
- const errorData = await response.json();
2897
- errorMsg = errorData.message || JSON.stringify(errorData);
2898
- } catch {
2899
- 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;
2900
2945
  }
2901
2946
  throw new Error(errorMsg);
2902
2947
  }
2903
- return await response.json();
2904
2948
  }
2905
2949
  /**
2906
2950
  * Connects a user to the Ermis network and establishes the WebSocket connection.
@@ -2911,19 +2955,24 @@ var ErmisChat = class _ErmisChat {
2911
2955
  * @param extenal_auth - Set to `true` to use your custom backend external authentication flow.
2912
2956
  * @returns A promise resolving to the API connection response once authenticated.
2913
2957
  */
2914
- connectUser = async (user, userTokenOrProvider, extenal_auth) => {
2958
+ connectUser = async (user, userTokenOrProvider, external_auth) => {
2915
2959
  this.logger("info", "client:connectUser() - started", {
2916
2960
  tags: ["connection", "client"]
2917
2961
  });
2918
2962
  if (!user.id) {
2919
2963
  throw new Error('The "id" field on the user is missing');
2920
2964
  }
2921
- if (extenal_auth) {
2965
+ let connectionUser = user;
2966
+ let connectionToken = userTokenOrProvider;
2967
+ if (external_auth) {
2922
2968
  const external_auth_token = await this.getExternalAuthToken(user, userTokenOrProvider);
2923
- userTokenOrProvider = external_auth_token.token;
2924
- 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
+ };
2925
2974
  }
2926
- if (this.userID === user.id && this.setUserPromise) {
2975
+ if (this.userID === connectionUser.id && this.setUserPromise) {
2927
2976
  console.warn(
2928
2977
  "Consecutive calls to connectUser is detected, ideally you should only call this function once in your app."
2929
2978
  );
@@ -2939,10 +2988,10 @@ var ErmisChat = class _ErmisChat {
2939
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.'
2940
2989
  );
2941
2990
  }
2942
- this.userID = user.id;
2943
- const setTokenPromise = this._setToken(user, userTokenOrProvider);
2944
- this._setUser(user);
2945
- 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 || "" });
2946
2995
  const wsPromise = this.openConnection();
2947
2996
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
2948
2997
  (result) => result[1]
@@ -3168,7 +3217,7 @@ var ErmisChat = class _ErmisChat {
3168
3217
  }
3169
3218
  dispatchEvent = (event) => {
3170
3219
  if (!event.received_at) event.received_at = /* @__PURE__ */ new Date();
3171
- if (event.type === "channel.created") {
3220
+ if (event.type === "channel.created" || event.type === "channel.topic.created") {
3172
3221
  this._handleChannelCreatedEvent(event).then(() => {
3173
3222
  this._afterDispatchEvent(event);
3174
3223
  });
@@ -3302,6 +3351,79 @@ var ErmisChat = class _ErmisChat {
3302
3351
  });
3303
3352
  }
3304
3353
  }
3354
+ if (event.type === "message.new" && event.channel_type === "topic") {
3355
+ postListenerCallbacks.push(() => {
3356
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3357
+ if (parentCid && this.activeChannels[parentCid]) {
3358
+ const parentChannel = this.activeChannels[parentCid];
3359
+ if (parentChannel.state.topics) {
3360
+ parentChannel.state.topics.sort((a, b) => {
3361
+ const aLatest = a.state?.latestMessages?.[a.state.latestMessages.length - 1]?.created_at;
3362
+ const bLatest = b.state?.latestMessages?.[b.state.latestMessages.length - 1]?.created_at;
3363
+ const aTime = aLatest ? new Date(aLatest).getTime() : 0;
3364
+ const bTime = bLatest ? new Date(bLatest).getTime() : 0;
3365
+ return bTime - aTime;
3366
+ });
3367
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3368
+ }
3369
+ }
3370
+ });
3371
+ }
3372
+ if (event.type === "channel.topic.updated") {
3373
+ postListenerCallbacks.push(() => {
3374
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3375
+ if (parentCid && this.activeChannels[parentCid]) {
3376
+ const parentChannel = this.activeChannels[parentCid];
3377
+ if (parentChannel.state?.topics && event.channel) {
3378
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3379
+ if (topicIndex !== -1) {
3380
+ const t = parentChannel.state.topics[topicIndex];
3381
+ if (t.data) {
3382
+ t.data = { ...t.data, ...event.channel };
3383
+ } else if (t.channel) {
3384
+ t.channel = { ...t.channel, ...event.channel };
3385
+ } else {
3386
+ Object.assign(t, event.channel);
3387
+ }
3388
+ }
3389
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3390
+ }
3391
+ }
3392
+ if (event.cid && this.activeChannels[event.cid]) {
3393
+ const topicChannel = this.activeChannels[event.cid];
3394
+ if (event.channel) {
3395
+ topicChannel.data = { ...topicChannel.data, ...event.channel };
3396
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3397
+ }
3398
+ }
3399
+ });
3400
+ }
3401
+ if (event.type === "channel.topic.closed" || event.type === "channel.topic.reopen") {
3402
+ postListenerCallbacks.push(() => {
3403
+ const isClosed = event.type === "channel.topic.closed";
3404
+ const parentCid = event.parent_cid;
3405
+ if (parentCid && this.activeChannels[parentCid]) {
3406
+ const parentChannel = this.activeChannels[parentCid];
3407
+ if (parentChannel.state?.topics) {
3408
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3409
+ if (topicIndex !== -1) {
3410
+ const t = parentChannel.state.topics[topicIndex];
3411
+ if (t.data) t.data.is_closed_topic = isClosed;
3412
+ else if (t.channel) t.channel.is_closed_topic = isClosed;
3413
+ else t.is_closed_topic = isClosed;
3414
+ }
3415
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3416
+ }
3417
+ }
3418
+ if (event.cid && this.activeChannels[event.cid]) {
3419
+ const topicChannel = this.activeChannels[event.cid];
3420
+ if (topicChannel.data) {
3421
+ topicChannel.data.is_closed_topic = isClosed;
3422
+ }
3423
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3424
+ }
3425
+ });
3426
+ }
3305
3427
  if (event.type === "connection.recovered") {
3306
3428
  postListenerCallbacks.push(() => {
3307
3429
  Object.values(this.activeChannels).forEach((channel) => {
@@ -3539,7 +3661,14 @@ var ErmisChat = class _ErmisChat {
3539
3661
  _updateProjectID(project_id) {
3540
3662
  this.projectId = project_id;
3541
3663
  }
3542
- async uploadFile(file) {
3664
+ /**
3665
+ * Uploads a new avatar image for the current user.
3666
+ * The user's avatar URL is automatically updated in both the client and the local state.
3667
+ *
3668
+ * @param file - The image file to upload.
3669
+ * @returns The response containing the new avatar URL.
3670
+ */
3671
+ async uploadAvatar(file) {
3543
3672
  const formData = new FormData();
3544
3673
  formData.append("avatar", file);
3545
3674
  let response = await this.post(this.userBaseURL + "/users/upload", formData, {
@@ -3554,12 +3683,8 @@ var ErmisChat = class _ErmisChat {
3554
3683
  }
3555
3684
  return response;
3556
3685
  }
3557
- async updateProfile(name, about_me) {
3558
- let body = {
3559
- name,
3560
- about_me
3561
- };
3562
- let response = await this.patch(this.userBaseURL + "/users/update", body);
3686
+ async updateProfile(updates) {
3687
+ let response = await this.patch(this.userBaseURL + "/users/update", updates);
3563
3688
  this.user = response;
3564
3689
  this.state.updateUser(response);
3565
3690
  return response;
@@ -3711,6 +3836,57 @@ var ErmisChat = class _ErmisChat {
3711
3836
  this.activeChannels[channel.cid] = channel;
3712
3837
  return channel;
3713
3838
  };
3839
+ /**
3840
+ * Creates a quick channel and immediately registers it on the server.
3841
+ * Quick channels are public group channels that anyone can join without an invitation.
3842
+ * The creator is added as the first member automatically.
3843
+ *
3844
+ * @param name - An optional display name for the channel.
3845
+ * @returns A promise that resolves to the created `Channel` object.
3846
+ */
3847
+ async createQuickChannel(name) {
3848
+ if (!this.userID) {
3849
+ throw Error("Call connectUser before creating a channel");
3850
+ }
3851
+ const now = /* @__PURE__ */ new Date();
3852
+ const formattedDate = new Intl.DateTimeFormat("en-US", {
3853
+ month: "short",
3854
+ day: "2-digit",
3855
+ year: "numeric",
3856
+ hour: "2-digit",
3857
+ minute: "2-digit",
3858
+ hour12: false
3859
+ }).format(now);
3860
+ const payload = {
3861
+ name: name || `Quick Channel - ${formattedDate}`,
3862
+ members: [this.userID],
3863
+ public: true
3864
+ };
3865
+ const quickChannel = this.channel("meeting", payload);
3866
+ await quickChannel.create();
3867
+ return quickChannel;
3868
+ }
3869
+ /**
3870
+ * Joins a quick channel by its ID.
3871
+ * Automatically checks whether the caller is already a member.
3872
+ * If not, it joins the channel and synchronizes state.
3873
+ *
3874
+ * @param channelId - The ID of the quick channel to join.
3875
+ * @returns A promise that resolves to the joined `Channel` object.
3876
+ */
3877
+ async joinQuickChannel(channelId) {
3878
+ if (!this.userID) {
3879
+ throw Error("Call connectUser before joining a channel");
3880
+ }
3881
+ const quickChannel = this.channel("meeting", channelId);
3882
+ await quickChannel.watch();
3883
+ const isMember = quickChannel.state.members && quickChannel.state.members[this.userID];
3884
+ if (!isMember) {
3885
+ await quickChannel.acceptInvite("join");
3886
+ await quickChannel.watch();
3887
+ }
3888
+ return quickChannel;
3889
+ }
3714
3890
  _normalizeExpiration(timeoutOrExpirationDate) {
3715
3891
  let pinExpires = null;
3716
3892
  if (typeof timeoutOrExpirationDate === "number") {
@@ -3725,7 +3901,7 @@ var ErmisChat = class _ErmisChat {
3725
3901
  return pinExpires;
3726
3902
  }
3727
3903
  getUserAgent() {
3728
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.5"}`;
3904
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.7"}`;
3729
3905
  }
3730
3906
  setUserAgent(userAgent) {
3731
3907
  this.userAgent = userAgent;
@@ -7084,7 +7260,7 @@ var ErmisAuthProvider = class {
7084
7260
  return data;
7085
7261
  }
7086
7262
  getUserAgent() {
7087
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.5"}`;
7263
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.7"}`;
7088
7264
  }
7089
7265
  setUserAgent(userAgent) {
7090
7266
  this.userAgent = userAgent;