@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.
@@ -998,10 +998,9 @@ var Channel = class {
998
998
  };
999
999
  this.state.addMessageSorted(optimisticMessage);
1000
1000
  try {
1001
- return await this.getClient().post(
1002
- this._channelURL() + "/message",
1003
- { message: { ...message } }
1004
- );
1001
+ return await this.getClient().post(this._channelURL() + "/message", {
1002
+ message: { ...message }
1003
+ });
1005
1004
  } catch (error) {
1006
1005
  const isOfflineError = !error.response || error.code === "ERR_NETWORK" || error.isWSFailure || !this.getClient().wsConnection?.isHealthy;
1007
1006
  const statusToSet = isOfflineError ? "failed_offline" : "error";
@@ -1026,10 +1025,9 @@ var Channel = class {
1026
1025
  messagePayload.show_in_channel = stateMsg.show_in_channel;
1027
1026
  }
1028
1027
  try {
1029
- return await this.getClient().post(
1030
- this._channelURL() + "/message",
1031
- { message: messagePayload }
1032
- );
1028
+ return await this.getClient().post(this._channelURL() + "/message", {
1029
+ message: messagePayload
1030
+ });
1033
1031
  } catch (error) {
1034
1032
  const isOfflineError = !error.response || error.code === "ERR_NETWORK" || error.isWSFailure || !this.getClient().wsConnection?.isHealthy;
1035
1033
  this.state.updateMessageStatus(messageId, isOfflineError ? "failed_offline" : "error");
@@ -1070,6 +1068,44 @@ var Channel = class {
1070
1068
  this.getClient().baseURL + `/messages/${this.type}/${this.id}/${messageID}/unpin`
1071
1069
  );
1072
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
+ }
1073
1109
  async editMessage(oldMessageID, message) {
1074
1110
  return await this.getClient().post(this.getClient().baseURL + `/messages/${this.type}/${this.id}/${oldMessageID}`, {
1075
1111
  message
@@ -1096,9 +1132,7 @@ var Channel = class {
1096
1132
  return file;
1097
1133
  });
1098
1134
  const uploadResults = await Promise.allSettled(
1099
- processedFiles.map(
1100
- (file) => this.sendFile(file, file.name, file.type)
1101
- )
1135
+ processedFiles.map((file) => this.sendFile(file, file.name, file.type))
1102
1136
  );
1103
1137
  const thumbUrls = /* @__PURE__ */ new Map();
1104
1138
  const thumbPromises = [];
@@ -1110,11 +1144,7 @@ var Channel = class {
1110
1144
  try {
1111
1145
  const thumbBlob = await this.getThumbBlobVideo(files[i]);
1112
1146
  if (thumbBlob) {
1113
- const thumbFile = new File(
1114
- [thumbBlob],
1115
- `thumb_${processedFiles[i].name}.jpg`,
1116
- { type: "image/jpeg" }
1117
- );
1147
+ const thumbFile = new File([thumbBlob], `thumb_${processedFiles[i].name}.jpg`, { type: "image/jpeg" });
1118
1148
  const thumbResp = await this.sendFile(thumbFile, thumbFile.name, "image/jpeg");
1119
1149
  thumbUrls.set(i, thumbResp.file);
1120
1150
  }
@@ -1132,9 +1162,7 @@ var Channel = class {
1132
1162
  const uploadedUrl = result.value.file;
1133
1163
  const thumbUrl = thumbUrls.get(i);
1134
1164
  const voiceMeta = options?.voiceMetadata?.get(i);
1135
- attachments.push(
1136
- buildAttachmentPayload(processedFiles[i], uploadedUrl, thumbUrl, voiceMeta)
1137
- );
1165
+ attachments.push(buildAttachmentPayload(processedFiles[i], uploadedUrl, thumbUrl, voiceMeta));
1138
1166
  } else {
1139
1167
  failedFiles.push({
1140
1168
  file: files[i],
@@ -1482,7 +1510,7 @@ var Channel = class {
1482
1510
  if (this.id) {
1483
1511
  queryURL += `/${this.id}`;
1484
1512
  } else {
1485
- if (this.type === "team") {
1513
+ if (this.type === "team" || this.type === "meeting") {
1486
1514
  const uuid = randomId();
1487
1515
  this.id = `${project_id}:${uuid}`;
1488
1516
  queryURL += `/${this.id}`;
@@ -1983,6 +2011,18 @@ var Channel = class {
1983
2011
  delete channelState.members[event.user.id];
1984
2012
  }
1985
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;
1986
2026
  case "channel.updated":
1987
2027
  if (event.channel) {
1988
2028
  channel.data = {
@@ -2121,7 +2161,12 @@ var Channel = class {
2121
2161
  const topic = this.getClient().channel(event.channel_type || "", event.channel_id || "");
2122
2162
  topic.data = event.channel;
2123
2163
  topic._initializeState(topicState, "latest");
2124
- 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
+ }
2125
2170
  break;
2126
2171
  case "channel.topic.closed":
2127
2172
  if (channel.data) {
@@ -2940,7 +2985,6 @@ var ErmisChat = class _ErmisChat {
2940
2985
  params.avatar = user.avatar;
2941
2986
  }
2942
2987
  const url = this.userBaseURL + "/get_token/external_auth";
2943
- const query = new URLSearchParams(params).toString();
2944
2988
  const headers = {
2945
2989
  "Content-Type": "application/json"
2946
2990
  };
@@ -2948,21 +2992,21 @@ var ErmisChat = class _ErmisChat {
2948
2992
  const tokenStr = typeof token === "string" && token.startsWith("Bearer ") ? token : `Bearer ${token}`;
2949
2993
  headers["Authorization"] = tokenStr;
2950
2994
  }
2951
- const response = await fetch(`${url}?${query}`, {
2952
- method: "GET",
2953
- headers
2954
- });
2955
- if (!response.ok) {
2956
- let errorMsg = "";
2957
- try {
2958
- const errorData = await response.json();
2959
- errorMsg = errorData.message || JSON.stringify(errorData);
2960
- } catch {
2961
- 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;
2962
3007
  }
2963
3008
  throw new Error(errorMsg);
2964
3009
  }
2965
- return await response.json();
2966
3010
  }
2967
3011
  /**
2968
3012
  * Connects a user to the Ermis network and establishes the WebSocket connection.
@@ -2973,19 +3017,24 @@ var ErmisChat = class _ErmisChat {
2973
3017
  * @param extenal_auth - Set to `true` to use your custom backend external authentication flow.
2974
3018
  * @returns A promise resolving to the API connection response once authenticated.
2975
3019
  */
2976
- connectUser = async (user, userTokenOrProvider, extenal_auth) => {
3020
+ connectUser = async (user, userTokenOrProvider, external_auth) => {
2977
3021
  this.logger("info", "client:connectUser() - started", {
2978
3022
  tags: ["connection", "client"]
2979
3023
  });
2980
3024
  if (!user.id) {
2981
3025
  throw new Error('The "id" field on the user is missing');
2982
3026
  }
2983
- if (extenal_auth) {
3027
+ let connectionUser = user;
3028
+ let connectionToken = userTokenOrProvider;
3029
+ if (external_auth) {
2984
3030
  const external_auth_token = await this.getExternalAuthToken(user, userTokenOrProvider);
2985
- userTokenOrProvider = external_auth_token.token;
2986
- 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
+ };
2987
3036
  }
2988
- if (this.userID === user.id && this.setUserPromise) {
3037
+ if (this.userID === connectionUser.id && this.setUserPromise) {
2989
3038
  console.warn(
2990
3039
  "Consecutive calls to connectUser is detected, ideally you should only call this function once in your app."
2991
3040
  );
@@ -3001,10 +3050,10 @@ var ErmisChat = class _ErmisChat {
3001
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.'
3002
3051
  );
3003
3052
  }
3004
- this.userID = user.id;
3005
- const setTokenPromise = this._setToken(user, userTokenOrProvider);
3006
- this._setUser(user);
3007
- 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 || "" });
3008
3057
  const wsPromise = this.openConnection();
3009
3058
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
3010
3059
  (result) => result[1]
@@ -3230,7 +3279,7 @@ var ErmisChat = class _ErmisChat {
3230
3279
  }
3231
3280
  dispatchEvent = (event) => {
3232
3281
  if (!event.received_at) event.received_at = /* @__PURE__ */ new Date();
3233
- if (event.type === "channel.created") {
3282
+ if (event.type === "channel.created" || event.type === "channel.topic.created") {
3234
3283
  this._handleChannelCreatedEvent(event).then(() => {
3235
3284
  this._afterDispatchEvent(event);
3236
3285
  });
@@ -3364,6 +3413,79 @@ var ErmisChat = class _ErmisChat {
3364
3413
  });
3365
3414
  }
3366
3415
  }
3416
+ if (event.type === "message.new" && event.channel_type === "topic") {
3417
+ postListenerCallbacks.push(() => {
3418
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3419
+ if (parentCid && this.activeChannels[parentCid]) {
3420
+ const parentChannel = this.activeChannels[parentCid];
3421
+ if (parentChannel.state.topics) {
3422
+ parentChannel.state.topics.sort((a, b) => {
3423
+ const aLatest = a.state?.latestMessages?.[a.state.latestMessages.length - 1]?.created_at;
3424
+ const bLatest = b.state?.latestMessages?.[b.state.latestMessages.length - 1]?.created_at;
3425
+ const aTime = aLatest ? new Date(aLatest).getTime() : 0;
3426
+ const bTime = bLatest ? new Date(bLatest).getTime() : 0;
3427
+ return bTime - aTime;
3428
+ });
3429
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3430
+ }
3431
+ }
3432
+ });
3433
+ }
3434
+ if (event.type === "channel.topic.updated") {
3435
+ postListenerCallbacks.push(() => {
3436
+ const parentCid = event.parent_cid || event.channel?.parent_cid;
3437
+ if (parentCid && this.activeChannels[parentCid]) {
3438
+ const parentChannel = this.activeChannels[parentCid];
3439
+ if (parentChannel.state?.topics && event.channel) {
3440
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3441
+ if (topicIndex !== -1) {
3442
+ const t = parentChannel.state.topics[topicIndex];
3443
+ if (t.data) {
3444
+ t.data = { ...t.data, ...event.channel };
3445
+ } else if (t.channel) {
3446
+ t.channel = { ...t.channel, ...event.channel };
3447
+ } else {
3448
+ Object.assign(t, event.channel);
3449
+ }
3450
+ }
3451
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3452
+ }
3453
+ }
3454
+ if (event.cid && this.activeChannels[event.cid]) {
3455
+ const topicChannel = this.activeChannels[event.cid];
3456
+ if (event.channel) {
3457
+ topicChannel.data = { ...topicChannel.data, ...event.channel };
3458
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3459
+ }
3460
+ }
3461
+ });
3462
+ }
3463
+ if (event.type === "channel.topic.closed" || event.type === "channel.topic.reopen") {
3464
+ postListenerCallbacks.push(() => {
3465
+ const isClosed = event.type === "channel.topic.closed";
3466
+ const parentCid = event.parent_cid;
3467
+ if (parentCid && this.activeChannels[parentCid]) {
3468
+ const parentChannel = this.activeChannels[parentCid];
3469
+ if (parentChannel.state?.topics) {
3470
+ const topicIndex = parentChannel.state.topics.findIndex((t) => t.cid === event.cid || t.channel?.cid === event.cid);
3471
+ if (topicIndex !== -1) {
3472
+ const t = parentChannel.state.topics[topicIndex];
3473
+ if (t.data) t.data.is_closed_topic = isClosed;
3474
+ else if (t.channel) t.channel.is_closed_topic = isClosed;
3475
+ else t.is_closed_topic = isClosed;
3476
+ }
3477
+ parentChannel._callChannelListeners({ ...event, type: "channel.updated", channel: parentChannel.data });
3478
+ }
3479
+ }
3480
+ if (event.cid && this.activeChannels[event.cid]) {
3481
+ const topicChannel = this.activeChannels[event.cid];
3482
+ if (topicChannel.data) {
3483
+ topicChannel.data.is_closed_topic = isClosed;
3484
+ }
3485
+ topicChannel._callChannelListeners({ ...event, type: "channel.updated", channel: topicChannel.data });
3486
+ }
3487
+ });
3488
+ }
3367
3489
  if (event.type === "connection.recovered") {
3368
3490
  postListenerCallbacks.push(() => {
3369
3491
  Object.values(this.activeChannels).forEach((channel) => {
@@ -3601,7 +3723,14 @@ var ErmisChat = class _ErmisChat {
3601
3723
  _updateProjectID(project_id) {
3602
3724
  this.projectId = project_id;
3603
3725
  }
3604
- async uploadFile(file) {
3726
+ /**
3727
+ * Uploads a new avatar image for the current user.
3728
+ * The user's avatar URL is automatically updated in both the client and the local state.
3729
+ *
3730
+ * @param file - The image file to upload.
3731
+ * @returns The response containing the new avatar URL.
3732
+ */
3733
+ async uploadAvatar(file) {
3605
3734
  const formData = new FormData();
3606
3735
  formData.append("avatar", file);
3607
3736
  let response = await this.post(this.userBaseURL + "/users/upload", formData, {
@@ -3616,12 +3745,8 @@ var ErmisChat = class _ErmisChat {
3616
3745
  }
3617
3746
  return response;
3618
3747
  }
3619
- async updateProfile(name, about_me) {
3620
- let body = {
3621
- name,
3622
- about_me
3623
- };
3624
- let response = await this.patch(this.userBaseURL + "/users/update", body);
3748
+ async updateProfile(updates) {
3749
+ let response = await this.patch(this.userBaseURL + "/users/update", updates);
3625
3750
  this.user = response;
3626
3751
  this.state.updateUser(response);
3627
3752
  return response;
@@ -3773,6 +3898,57 @@ var ErmisChat = class _ErmisChat {
3773
3898
  this.activeChannels[channel.cid] = channel;
3774
3899
  return channel;
3775
3900
  };
3901
+ /**
3902
+ * Creates a quick channel and immediately registers it on the server.
3903
+ * Quick channels are public group channels that anyone can join without an invitation.
3904
+ * The creator is added as the first member automatically.
3905
+ *
3906
+ * @param name - An optional display name for the channel.
3907
+ * @returns A promise that resolves to the created `Channel` object.
3908
+ */
3909
+ async createQuickChannel(name) {
3910
+ if (!this.userID) {
3911
+ throw Error("Call connectUser before creating a channel");
3912
+ }
3913
+ const now = /* @__PURE__ */ new Date();
3914
+ const formattedDate = new Intl.DateTimeFormat("en-US", {
3915
+ month: "short",
3916
+ day: "2-digit",
3917
+ year: "numeric",
3918
+ hour: "2-digit",
3919
+ minute: "2-digit",
3920
+ hour12: false
3921
+ }).format(now);
3922
+ const payload = {
3923
+ name: name || `Quick Channel - ${formattedDate}`,
3924
+ members: [this.userID],
3925
+ public: true
3926
+ };
3927
+ const quickChannel = this.channel("meeting", payload);
3928
+ await quickChannel.create();
3929
+ return quickChannel;
3930
+ }
3931
+ /**
3932
+ * Joins a quick channel by its ID.
3933
+ * Automatically checks whether the caller is already a member.
3934
+ * If not, it joins the channel and synchronizes state.
3935
+ *
3936
+ * @param channelId - The ID of the quick channel to join.
3937
+ * @returns A promise that resolves to the joined `Channel` object.
3938
+ */
3939
+ async joinQuickChannel(channelId) {
3940
+ if (!this.userID) {
3941
+ throw Error("Call connectUser before joining a channel");
3942
+ }
3943
+ const quickChannel = this.channel("meeting", channelId);
3944
+ await quickChannel.watch();
3945
+ const isMember = quickChannel.state.members && quickChannel.state.members[this.userID];
3946
+ if (!isMember) {
3947
+ await quickChannel.acceptInvite("join");
3948
+ await quickChannel.watch();
3949
+ }
3950
+ return quickChannel;
3951
+ }
3776
3952
  _normalizeExpiration(timeoutOrExpirationDate) {
3777
3953
  let pinExpires = null;
3778
3954
  if (typeof timeoutOrExpirationDate === "number") {
@@ -3787,7 +3963,7 @@ var ErmisChat = class _ErmisChat {
3787
3963
  return pinExpires;
3788
3964
  }
3789
3965
  getUserAgent() {
3790
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.5"}`;
3966
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.7"}`;
3791
3967
  }
3792
3968
  setUserAgent(userAgent) {
3793
3969
  this.userAgent = userAgent;
@@ -7145,7 +7321,7 @@ var ErmisAuthProvider = class {
7145
7321
  return data;
7146
7322
  }
7147
7323
  getUserAgent() {
7148
- return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.5"}`;
7324
+ return this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? "node" : "browser"}-${"1.0.7"}`;
7149
7325
  }
7150
7326
  setUserAgent(userAgent) {
7151
7327
  this.userAgent = userAgent;