@scalemule/chat 0.0.1 → 0.0.3

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.
@@ -1,4 +1,4 @@
1
- import { d as ChatEventMap, c as ChatConfig, g as ConnectionStatus, i as CreateConversationOptions, a as ApiResponse, h as Conversation, L as ListConversationsOptions, S as SendMessageOptions, e as ChatMessage, G as GetMessagesOptions, M as MessagesResponse, R as ReadStatus, C as ChannelSettings } from './types-BwgD_Etd.cjs';
1
+ import { e as ChatEventMap, d as ChatConfig, h as ConnectionStatus, j as CreateConversationOptions, a as ApiResponse, i as Conversation, L as ListConversationsOptions, S as SendMessageOptions, f as ChatMessage, G as GetMessagesOptions, M as MessagesResponse, U as UnreadTotalResponse, n as ReadStatus, C as ChannelSettings, c as ChannelWithSettings, k as CreateEphemeralChannelOptions, l as CreateLargeRoomOptions } from './types-FQcFhbJx.js';
2
2
 
3
3
  type Listener<T> = (data: T) => void;
4
4
  declare class EventEmitter<EventMap extends Record<string, any>> {
@@ -31,6 +31,8 @@ declare class ChatClient extends EventEmitter<ChatEventMap> {
31
31
  deleteMessage(messageId: string): Promise<ApiResponse<void>>;
32
32
  getCachedMessages(conversationId: string): ChatMessage[];
33
33
  addReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
34
+ removeReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
35
+ getUnreadTotal(): Promise<ApiResponse<UnreadTotalResponse>>;
34
36
  sendTyping(conversationId: string, isTyping?: boolean): Promise<void>;
35
37
  markRead(conversationId: string): Promise<void>;
36
38
  getReadStatus(conversationId: string): Promise<ApiResponse<{
@@ -48,11 +50,38 @@ declare class ChatClient extends EventEmitter<ChatEventMap> {
48
50
  * Large rooms use `conversation:lr:` prefix to skip MySQL in the realtime service.
49
51
  */
50
52
  setConversationType(conversationId: string, type: Conversation['conversation_type']): void;
53
+ /**
54
+ * Find an active ephemeral or large_room channel by linked session ID.
55
+ * Returns null (not an error) if no active channel exists.
56
+ */
57
+ findChannelBySessionId(linkedSessionId: string): Promise<ChannelWithSettings | null>;
58
+ /**
59
+ * Self-join an ephemeral or large_room channel. Idempotent.
60
+ */
61
+ joinChannel(channelId: string): Promise<ApiResponse<{
62
+ participant_id: string;
63
+ role: string;
64
+ joined_at: string;
65
+ }>>;
66
+ /**
67
+ * Create an ephemeral channel tied to a session (e.g., a video snap).
68
+ */
69
+ createEphemeralChannel(options: CreateEphemeralChannelOptions): Promise<ApiResponse<ChannelWithSettings>>;
70
+ /**
71
+ * Create a large room channel (high-concurrency, skips MySQL tracking in realtime).
72
+ */
73
+ createLargeRoom(options: CreateLargeRoomOptions): Promise<ApiResponse<ChannelWithSettings>>;
74
+ /**
75
+ * Get the global concurrent subscriber count for a conversation.
76
+ */
77
+ getSubscriberCount(conversationId: string): Promise<number>;
51
78
  subscribeToConversation(conversationId: string): () => void;
52
79
  /** Build channel name with correct prefix based on conversation type. */
53
80
  private channelName;
54
81
  destroy(): void;
55
82
  private handleRealtimeMessage;
83
+ private handlePrivateMessage;
84
+ private handleConversationMessage;
56
85
  private flushOfflineQueue;
57
86
  }
58
87
 
@@ -1,4 +1,4 @@
1
- import { d as ChatEventMap, c as ChatConfig, g as ConnectionStatus, i as CreateConversationOptions, a as ApiResponse, h as Conversation, L as ListConversationsOptions, S as SendMessageOptions, e as ChatMessage, G as GetMessagesOptions, M as MessagesResponse, R as ReadStatus, C as ChannelSettings } from './types-BwgD_Etd.js';
1
+ import { e as ChatEventMap, d as ChatConfig, h as ConnectionStatus, j as CreateConversationOptions, a as ApiResponse, i as Conversation, L as ListConversationsOptions, S as SendMessageOptions, f as ChatMessage, G as GetMessagesOptions, M as MessagesResponse, U as UnreadTotalResponse, n as ReadStatus, C as ChannelSettings, c as ChannelWithSettings, k as CreateEphemeralChannelOptions, l as CreateLargeRoomOptions } from './types-FQcFhbJx.cjs';
2
2
 
3
3
  type Listener<T> = (data: T) => void;
4
4
  declare class EventEmitter<EventMap extends Record<string, any>> {
@@ -31,6 +31,8 @@ declare class ChatClient extends EventEmitter<ChatEventMap> {
31
31
  deleteMessage(messageId: string): Promise<ApiResponse<void>>;
32
32
  getCachedMessages(conversationId: string): ChatMessage[];
33
33
  addReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
34
+ removeReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
35
+ getUnreadTotal(): Promise<ApiResponse<UnreadTotalResponse>>;
34
36
  sendTyping(conversationId: string, isTyping?: boolean): Promise<void>;
35
37
  markRead(conversationId: string): Promise<void>;
36
38
  getReadStatus(conversationId: string): Promise<ApiResponse<{
@@ -48,11 +50,38 @@ declare class ChatClient extends EventEmitter<ChatEventMap> {
48
50
  * Large rooms use `conversation:lr:` prefix to skip MySQL in the realtime service.
49
51
  */
50
52
  setConversationType(conversationId: string, type: Conversation['conversation_type']): void;
53
+ /**
54
+ * Find an active ephemeral or large_room channel by linked session ID.
55
+ * Returns null (not an error) if no active channel exists.
56
+ */
57
+ findChannelBySessionId(linkedSessionId: string): Promise<ChannelWithSettings | null>;
58
+ /**
59
+ * Self-join an ephemeral or large_room channel. Idempotent.
60
+ */
61
+ joinChannel(channelId: string): Promise<ApiResponse<{
62
+ participant_id: string;
63
+ role: string;
64
+ joined_at: string;
65
+ }>>;
66
+ /**
67
+ * Create an ephemeral channel tied to a session (e.g., a video snap).
68
+ */
69
+ createEphemeralChannel(options: CreateEphemeralChannelOptions): Promise<ApiResponse<ChannelWithSettings>>;
70
+ /**
71
+ * Create a large room channel (high-concurrency, skips MySQL tracking in realtime).
72
+ */
73
+ createLargeRoom(options: CreateLargeRoomOptions): Promise<ApiResponse<ChannelWithSettings>>;
74
+ /**
75
+ * Get the global concurrent subscriber count for a conversation.
76
+ */
77
+ getSubscriberCount(conversationId: string): Promise<number>;
51
78
  subscribeToConversation(conversationId: string): () => void;
52
79
  /** Build channel name with correct prefix based on conversation type. */
53
80
  private channelName;
54
81
  destroy(): void;
55
82
  private handleRealtimeMessage;
83
+ private handlePrivateMessage;
84
+ private handleConversationMessage;
56
85
  private flushOfflineQueue;
57
86
  }
58
87
 
@@ -1 +1 @@
1
- "use strict";var ChatEmbed=(()=>{var p=class{constructor(){this.listeners=new Map}on(n,e){return this.listeners.has(n)||this.listeners.set(n,new Set),this.listeners.get(n).add(e),()=>this.off(n,e)}off(n,e){this.listeners.get(n)?.delete(e)}once(n,e){let t=(s=>{this.off(n,t),e(s)});return this.on(n,t)}emit(n,...[e]){let t=this.listeners.get(n);if(t)for(let s of t)try{s(e)}catch(i){console.error(`[ScaleMuleChat] Error in ${String(n)} listener:`,i)}}removeAllListeners(n){n?this.listeners.delete(n):this.listeners.clear()}};var v="https://api.scalemule.com";var u=class{constructor(n,e){this.cache=new Map;this.maxMessages=n??200,this.maxConversations=e??50}getMessages(n){return this.cache.get(n)??[]}setMessages(n,e){this.cache.set(n,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(n,e){let t=this.cache.get(n)??[];t.some(s=>s.id===e.id)||(t.push(e),t.sort((s,i)=>s.created_at.localeCompare(i.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(n,t))}updateMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e.id);s>=0&&(t[s]=e)}removeMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e);s>=0&&t.splice(s,1)}clear(n){n?this.cache.delete(n):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let n=this.cache.keys().next().value;n&&this.cache.delete(n)}}};var f="scalemule_chat_offline_queue",d=class{constructor(n=!0){this.queue=[];this.enabled=n,this.enabled&&this.load()}enqueue(n,e,t="text",s){this.enabled&&(this.queue.push({conversationId:n,content:e,message_type:t,attachments:s,timestamp:Date.now()}),this.save())}drain(){let n=[...this.queue];return this.queue=[],this.save(),n}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(f,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let n=localStorage.getItem(f);n&&(this.queue=JSON.parse(n))}}catch{this.queue=[]}}};var g=class{constructor(n){this.baseUrl=n.baseUrl.replace(/\/$/,""),this.apiKey=n.apiKey,this.getToken=n.getToken,this.timeout=n.timeout??1e4}async get(n){return this.request("GET",n)}async post(n,e){return this.request("POST",n,e)}async patch(n,e){return this.request("PATCH",n,e)}async del(n){return this.request("DELETE",n)}async request(n,e,t){let s={"Content-Type":"application/json"};if(this.apiKey&&(s["x-api-key"]=this.apiKey),this.getToken){let r=await this.getToken();r&&(s.Authorization=`Bearer ${r}`)}let i=new AbortController,o=setTimeout(()=>i.abort(),this.timeout);try{let r=await fetch(`${this.baseUrl}${e}`,{method:n,headers:s,body:t?JSON.stringify(t):void 0,signal:i.signal,credentials:"include"});if(clearTimeout(o),r.status===204)return{data:null,error:null};let a=await r.json().catch(()=>null);return r.ok?{data:a?.data!==void 0?a.data:a,error:null}:{data:null,error:{code:a?.error?.code??a?.code??"unknown",message:a?.error?.message??a?.message??r.statusText,status:r.status,details:a?.error?.details??a?.details}}}catch(r){return clearTimeout(o),{data:null,error:{code:"network_error",message:r instanceof Error?r.message:"Network error",status:0}}}}};var l=class extends p{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`,this.wsBaseUrl=t.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=s=>{this.handleMessage(s.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e,credentials:"include"});if(!t.ok)return null;let s=await t.json();return s.ticket??s.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var m=class extends p{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??v;this.http=new g({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new l({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new u(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new d(e.offlineQueue??!0),this.ws.on("status",s=>{switch(s){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",s=>{this.emit("reconnecting",s)}),this.ws.on("message",({channel:s,data:i})=>{this.handleRealtimeMessage(s,i)}),this.ws.on("presence:state",({channel:s,members:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:state",{conversationId:o,members:i})}),this.ws.on("presence:join",({channel:s,user:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:join",{userId:i.user_id,conversationId:o,userData:i.user_data})}),this.ws.on("presence:leave",({channel:s,userId:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:leave",{userId:i,conversationId:o})}),this.ws.on("presence:update",({channel:s,userId:i,status:o,userData:r})=>{let a=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:update",{userId:i,conversationId:a,status:o,userData:r})}),this.ws.on("error",({message:s})=>{this.emit("error",{code:"ws_error",message:s})})}get status(){return this.ws.getStatus()}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t=await this.http.post("/v1/chat/conversations",e);return t.data&&this.trackConversationType(t.data),t}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page));let s=t.toString(),i=await this.http.get(`/v1/chat/conversations${s?"?"+s:""}`);return i.data&&i.data.forEach(o=>this.trackConversationType(o)),i}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let s=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});return s.data?this.cache.addMessage(e,s.data):s.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text"),s}async getMessages(e,t){let s=new URLSearchParams;t?.limit&&s.set("limit",String(t.limit)),t?.before&&s.set("before",t.before),t?.after&&s.set("after",t.after);let i=s.toString(),o=await this.http.get(`/v1/chat/conversations/${e}/messages${i?"?"+i:""}`);return o.data?.messages&&!t?.before&&!t?.after&&this.cache.setMessages(e,o.data.messages),o}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}getCachedMessages(e){return this.cache.getMessages(e)}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let s=this.channelName(e);this.ws.joinPresence(s,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,s){let i=this.channelName(e);this.ws.send({type:"presence_update",channel:i,status:t,user_data:s})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),s=this.ws.subscribe(t);return this.conversationSubs.set(e,s),()=>{this.conversationSubs.delete(e),s()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(!e.startsWith("conversation:"))return;let s=e.replace(/^conversation:(?:lr:|bc:)?/,""),i=t;if(!i)return;let o=i.event??i.type,r=i.data??i;switch(o){case"new_message":{let a=r;this.cache.addMessage(s,a),this.emit("message",{message:a,conversationId:s});break}case"message_edited":{let a=r;this.cache.updateMessage(s,a),this.emit("message:updated",{message:a,conversationId:s});break}case"message_deleted":{let a=r.message_id??r.id;a&&(this.cache.removeMessage(s,a),this.emit("message:deleted",{messageId:a,conversationId:s}));break}case"user_typing":{let a=r.user_id;a&&this.emit("typing",{userId:a,conversationId:s});break}case"user_stopped_typing":{let a=r.user_id;a&&this.emit("typing:stop",{userId:a,conversationId:s});break}case"typing_batch":{let a=r.users??[];for(let c of a)this.emit("typing",{userId:c,conversationId:s});break}case"messages_read":{let a=r.user_id,c=r.last_read_at;a&&c&&this.emit("read",{userId:a,conversationId:s,lastReadAt:c});break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type})}};function y(){let h=new URLSearchParams(window.location.search),n=window.location.pathname.split("/"),e=n[n.length-1]||void 0,t=h.get("token")??void 0,s;try{s={...window.location.hash?JSON.parse(decodeURIComponent(window.location.hash.slice(1))):{},embedToken:t}}catch{s={embedToken:t}}let i=new m(s);i.on("message",({message:r,conversationId:a})=>{window.parent.postMessage({type:"message",payload:{message:r,conversationId:a}},"*")}),i.on("connected",()=>{window.parent.postMessage({type:"connected"},"*")});let o=!1;window.addEventListener("message",r=>{let{method:a,args:c}=r.data??{};switch(a){case"sendMessage":e&&c?.content&&i.sendMessage(e,{content:c.content});break;case"disconnect":o=!0,i.disconnect();break}}),e&&i.getConversation(e).then(()=>{o||(i.subscribeToConversation(e),i.connect())})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y());})();
1
+ "use strict";var ChatEmbed=(()=>{var p=class{constructor(){this.listeners=new Map}on(n,e){return this.listeners.has(n)||this.listeners.set(n,new Set),this.listeners.get(n).add(e),()=>this.off(n,e)}off(n,e){this.listeners.get(n)?.delete(e)}once(n,e){let t=(s=>{this.off(n,t),e(s)});return this.on(n,t)}emit(n,...[e]){let t=this.listeners.get(n);if(t)for(let s of t)try{s(e)}catch(i){console.error(`[ScaleMuleChat] Error in ${String(n)} listener:`,i)}}removeAllListeners(n){n?this.listeners.delete(n):this.listeners.clear()}};var v="https://api.scalemule.com";var d=class{constructor(n,e){this.cache=new Map;this.maxMessages=n??200,this.maxConversations=e??50}getMessages(n){return this.cache.get(n)??[]}setMessages(n,e){this.cache.set(n,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(n,e){let t=this.cache.get(n)??[];t.some(s=>s.id===e.id)||(t.push(e),t.sort((s,i)=>s.created_at.localeCompare(i.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(n,t))}updateMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e.id);s>=0&&(t[s]=e)}removeMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e);s>=0&&t.splice(s,1)}clear(n){n?this.cache.delete(n):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let n=this.cache.keys().next().value;n&&this.cache.delete(n)}}};var f="scalemule_chat_offline_queue",u=class{constructor(n=!0){this.queue=[];this.enabled=n,this.enabled&&this.load()}enqueue(n,e,t="text",s){this.enabled&&(this.queue.push({conversationId:n,content:e,message_type:t,attachments:s,timestamp:Date.now()}),this.save())}drain(){let n=[...this.queue];return this.queue=[],this.save(),n}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(f,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let n=localStorage.getItem(f);n&&(this.queue=JSON.parse(n))}}catch{this.queue=[]}}};var g=class{constructor(n){this.baseUrl=n.baseUrl.replace(/\/$/,""),this.apiKey=n.apiKey,this.getToken=n.getToken,this.timeout=n.timeout??1e4}async get(n){return this.request("GET",n)}async post(n,e){return this.request("POST",n,e)}async patch(n,e){return this.request("PATCH",n,e)}async del(n){return this.request("DELETE",n)}async request(n,e,t){let s={"Content-Type":"application/json"};if(this.apiKey&&(s["x-api-key"]=this.apiKey),this.getToken){let r=await this.getToken();r&&(s.Authorization=`Bearer ${r}`)}let i=new AbortController,o=setTimeout(()=>i.abort(),this.timeout);try{let r=await fetch(`${this.baseUrl}${e}`,{method:n,headers:s,body:t?JSON.stringify(t):void 0,signal:i.signal,credentials:"include"});if(clearTimeout(o),r.status===204)return{data:null,error:null};let a=await r.json().catch(()=>null);return r.ok?{data:a?.data!==void 0?a.data:a,error:null}:{data:null,error:{code:a?.error?.code??a?.code??"unknown",message:a?.error?.message??a?.message??r.statusText,status:r.status,details:a?.error?.details??a?.details}}}catch(r){return clearTimeout(o),{data:null,error:{code:"network_error",message:r instanceof Error?r.message:"Network error",status:0}}}}};var l=class extends p{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`,this.wsBaseUrl=t.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=s=>{this.handleMessage(s.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e,credentials:"include"});if(!t.ok)return null;let s=await t.json();return s.ticket??s.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var m=class extends p{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??v;this.http=new g({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new l({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new d(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new u(e.offlineQueue??!0),this.ws.on("status",s=>{switch(s){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",s=>{this.emit("reconnecting",s)}),this.ws.on("message",({channel:s,data:i})=>{this.handleRealtimeMessage(s,i)}),this.ws.on("presence:state",({channel:s,members:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:state",{conversationId:o,members:i})}),this.ws.on("presence:join",({channel:s,user:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:join",{userId:i.user_id,conversationId:o,userData:i.user_data})}),this.ws.on("presence:leave",({channel:s,userId:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:leave",{userId:i,conversationId:o})}),this.ws.on("presence:update",({channel:s,userId:i,status:o,userData:r})=>{let a=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:update",{userId:i,conversationId:a,status:o,userData:r})}),this.ws.on("error",({message:s})=>{this.emit("error",{code:"ws_error",message:s})})}get status(){return this.ws.getStatus()}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t={...e,conversation_type:e.conversation_type??"direct"},s=await this.http.post("/v1/chat/conversations",t);return s.data&&this.trackConversationType(s.data),s}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page)),e?.conversation_type&&t.set("conversation_type",e.conversation_type);let s=t.toString(),i=await this.http.get(`/v1/chat/conversations${s?"?"+s:""}`);return i.data&&i.data.forEach(o=>this.trackConversationType(o)),i}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let s=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});return s.data?this.cache.addMessage(e,s.data):s.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text"),s}async getMessages(e,t){let s=new URLSearchParams;t?.limit&&s.set("limit",String(t.limit)),t?.before&&s.set("before",t.before),t?.after&&s.set("after",t.after);let i=s.toString(),o=await this.http.get(`/v1/chat/conversations/${e}/messages${i?"?"+i:""}`);return o.data?.messages&&(t?.after||(o.data.messages=o.data.messages.slice().reverse()),!t?.before&&!t?.after&&this.cache.setMessages(e,o.data.messages)),o}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}getCachedMessages(e){return this.cache.getMessages(e)}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async removeReaction(e,t){return this.http.del(`/v1/chat/messages/${e}/reactions/${encodeURIComponent(t)}`)}async getUnreadTotal(){return this.http.get("/v1/chat/conversations/unread-total")}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let s=this.channelName(e);this.ws.joinPresence(s,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,s){let i=this.channelName(e);this.ws.send({type:"presence_update",channel:i,status:t,user_data:s})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}async findChannelBySessionId(e){let t=await this.http.get(`/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(e)}`);return t.error?.status===404?null:(t.data&&this.conversationTypes.set(t.data.id,t.data.channel_type),t.data)}async joinChannel(e){return this.http.post(`/v1/chat/channels/${e}/join`,{})}async createEphemeralChannel(e){let t=await this.http.post("/v1/chat/channels/ephemeral",e);return t.data&&this.conversationTypes.set(t.data.id,"ephemeral"),t}async createLargeRoom(e){let t=await this.http.post("/v1/chat/channels/large-room",e);return t.data&&this.conversationTypes.set(t.data.id,"large_room"),t}async getSubscriberCount(e){return(await this.http.get(`/v1/chat/conversations/${e}/subscriber-count`)).data?.count??0}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),s=this.ws.subscribe(t);return this.conversationSubs.set(e,s),()=>{this.conversationSubs.delete(e),s()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(e.startsWith("conversation:")){this.handleConversationMessage(e,t);return}if(e.startsWith("private:")){this.handlePrivateMessage(t);return}}handlePrivateMessage(e){let t=e;if(!t)return;let s=t.event??t.type,i=t.data??t;if(s==="new_message"){let o=i.conversation_id,r=i.id??i.message_id,a=i.sender_id,c=i.content??"";o&&this.emit("inbox:update",{conversationId:o,messageId:r,senderId:a,preview:c})}}handleConversationMessage(e,t){let s=e.replace(/^conversation:(?:lr:|bc:)?/,""),i=t;if(!i)return;let o=i.event??i.type,r=i.data??i;switch(o){case"new_message":{let a=r;this.cache.addMessage(s,a),this.emit("message",{message:a,conversationId:s});break}case"message_edited":{let a=r;this.cache.updateMessage(s,a),this.emit("message:updated",{message:a,conversationId:s});break}case"message_deleted":{let a=r.message_id??r.id;a&&(this.cache.removeMessage(s,a),this.emit("message:deleted",{messageId:a,conversationId:s}));break}case"user_typing":{let a=r.user_id;a&&this.emit("typing",{userId:a,conversationId:s});break}case"user_stopped_typing":{let a=r.user_id;a&&this.emit("typing:stop",{userId:a,conversationId:s});break}case"typing_batch":{let a=r.users??[];for(let c of a)this.emit("typing",{userId:c,conversationId:s});break}case"messages_read":{let a=r.user_id,c=r.last_read_at;a&&c&&this.emit("read",{userId:a,conversationId:s,lastReadAt:c});break}case"room_upgraded":{if(r.new_type==="large_room"){this.conversationTypes.set(s,"large_room");let c=this.conversationSubs.get(s);c&&(c(),this.conversationSubs.delete(s),this.subscribeToConversation(s)),this.emit("room_upgraded",{conversationId:s,newType:"large_room"})}break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type})}};function y(){let h=new URLSearchParams(window.location.search),n=window.location.pathname.split("/"),e=n[n.length-1]||void 0,t=h.get("token")??void 0,s;try{s={...window.location.hash?JSON.parse(decodeURIComponent(window.location.hash.slice(1))):{},embedToken:t}}catch{s={embedToken:t}}let i=new m(s);i.on("message",({message:r,conversationId:a})=>{window.parent.postMessage({type:"message",payload:{message:r,conversationId:a}},"*")}),i.on("connected",()=>{window.parent.postMessage({type:"connected"},"*")});let o=!1;window.addEventListener("message",r=>{let{method:a,args:c}=r.data??{};switch(a){case"sendMessage":e&&c?.content&&i.sendMessage(e,{content:c.content});break;case"disconnect":o=!0,i.disconnect();break}}),e&&i.getConversation(e).then(()=>{o||(i.subscribeToConversation(e),i.connect())})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y());})();
@@ -1,4 +1,4 @@
1
- "use strict";var ScaleMuleChat=(()=>{var y=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var x=(c,n)=>{for(var e in n)y(c,e,{get:n[e],enumerable:!0})},R=(c,n,e,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of S(n))!A.call(c,s)&&s!==e&&y(c,s,{get:()=>n[s],enumerable:!(t=M(n,s))||t.enumerable});return c};var P=c=>R(y({},"__esModule",{value:!0}),c);var W={};x(W,{ChatClient:()=>p,create:()=>$,version:()=>H});var u=class{constructor(){this.listeners=new Map}on(n,e){return this.listeners.has(n)||this.listeners.set(n,new Set),this.listeners.get(n).add(e),()=>this.off(n,e)}off(n,e){this.listeners.get(n)?.delete(e)}once(n,e){let t=(s=>{this.off(n,t),e(s)});return this.on(n,t)}emit(n,...[e]){let t=this.listeners.get(n);if(t)for(let s of t)try{s(e)}catch(i){console.error(`[ScaleMuleChat] Error in ${String(n)} listener:`,i)}}removeAllListeners(n){n?this.listeners.delete(n):this.listeners.clear()}};var T="https://api.scalemule.com";var l=class{constructor(n,e){this.cache=new Map;this.maxMessages=n??200,this.maxConversations=e??50}getMessages(n){return this.cache.get(n)??[]}setMessages(n,e){this.cache.set(n,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(n,e){let t=this.cache.get(n)??[];t.some(s=>s.id===e.id)||(t.push(e),t.sort((s,i)=>s.created_at.localeCompare(i.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(n,t))}updateMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e.id);s>=0&&(t[s]=e)}removeMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e);s>=0&&t.splice(s,1)}clear(n){n?this.cache.delete(n):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let n=this.cache.keys().next().value;n&&this.cache.delete(n)}}};var w="scalemule_chat_offline_queue",g=class{constructor(n=!0){this.queue=[];this.enabled=n,this.enabled&&this.load()}enqueue(n,e,t="text",s){this.enabled&&(this.queue.push({conversationId:n,content:e,message_type:t,attachments:s,timestamp:Date.now()}),this.save())}drain(){let n=[...this.queue];return this.queue=[],this.save(),n}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(w,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let n=localStorage.getItem(w);n&&(this.queue=JSON.parse(n))}}catch{this.queue=[]}}};var m=class{constructor(n){this.baseUrl=n.baseUrl.replace(/\/$/,""),this.apiKey=n.apiKey,this.getToken=n.getToken,this.timeout=n.timeout??1e4}async get(n){return this.request("GET",n)}async post(n,e){return this.request("POST",n,e)}async patch(n,e){return this.request("PATCH",n,e)}async del(n){return this.request("DELETE",n)}async request(n,e,t){let s={"Content-Type":"application/json"};if(this.apiKey&&(s["x-api-key"]=this.apiKey),this.getToken){let r=await this.getToken();r&&(s.Authorization=`Bearer ${r}`)}let i=new AbortController,o=setTimeout(()=>i.abort(),this.timeout);try{let r=await fetch(`${this.baseUrl}${e}`,{method:n,headers:s,body:t?JSON.stringify(t):void 0,signal:i.signal,credentials:"include"});if(clearTimeout(o),r.status===204)return{data:null,error:null};let a=await r.json().catch(()=>null);return r.ok?{data:a?.data!==void 0?a.data:a,error:null}:{data:null,error:{code:a?.error?.code??a?.code??"unknown",message:a?.error?.message??a?.message??r.statusText,status:r.status,details:a?.error?.details??a?.details}}}catch(r){return clearTimeout(o),{data:null,error:{code:"network_error",message:r instanceof Error?r.message:"Network error",status:0}}}}};var v=class extends u{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`,this.wsBaseUrl=t.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=s=>{this.handleMessage(s.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e,credentials:"include"});if(!t.ok)return null;let s=await t.json();return s.ticket??s.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var p=class extends u{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??T;this.http=new m({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new v({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new l(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new g(e.offlineQueue??!0),this.ws.on("status",s=>{switch(s){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",s=>{this.emit("reconnecting",s)}),this.ws.on("message",({channel:s,data:i})=>{this.handleRealtimeMessage(s,i)}),this.ws.on("presence:state",({channel:s,members:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:state",{conversationId:o,members:i})}),this.ws.on("presence:join",({channel:s,user:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:join",{userId:i.user_id,conversationId:o,userData:i.user_data})}),this.ws.on("presence:leave",({channel:s,userId:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:leave",{userId:i,conversationId:o})}),this.ws.on("presence:update",({channel:s,userId:i,status:o,userData:r})=>{let a=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:update",{userId:i,conversationId:a,status:o,userData:r})}),this.ws.on("error",({message:s})=>{this.emit("error",{code:"ws_error",message:s})})}get status(){return this.ws.getStatus()}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t=await this.http.post("/v1/chat/conversations",e);return t.data&&this.trackConversationType(t.data),t}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page));let s=t.toString(),i=await this.http.get(`/v1/chat/conversations${s?"?"+s:""}`);return i.data&&i.data.forEach(o=>this.trackConversationType(o)),i}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let s=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});return s.data?this.cache.addMessage(e,s.data):s.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text"),s}async getMessages(e,t){let s=new URLSearchParams;t?.limit&&s.set("limit",String(t.limit)),t?.before&&s.set("before",t.before),t?.after&&s.set("after",t.after);let i=s.toString(),o=await this.http.get(`/v1/chat/conversations/${e}/messages${i?"?"+i:""}`);return o.data?.messages&&!t?.before&&!t?.after&&this.cache.setMessages(e,o.data.messages),o}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}getCachedMessages(e){return this.cache.getMessages(e)}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let s=this.channelName(e);this.ws.joinPresence(s,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,s){let i=this.channelName(e);this.ws.send({type:"presence_update",channel:i,status:t,user_data:s})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),s=this.ws.subscribe(t);return this.conversationSubs.set(e,s),()=>{this.conversationSubs.delete(e),s()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(!e.startsWith("conversation:"))return;let s=e.replace(/^conversation:(?:lr:|bc:)?/,""),i=t;if(!i)return;let o=i.event??i.type,r=i.data??i;switch(o){case"new_message":{let a=r;this.cache.addMessage(s,a),this.emit("message",{message:a,conversationId:s});break}case"message_edited":{let a=r;this.cache.updateMessage(s,a),this.emit("message:updated",{message:a,conversationId:s});break}case"message_deleted":{let a=r.message_id??r.id;a&&(this.cache.removeMessage(s,a),this.emit("message:deleted",{messageId:a,conversationId:s}));break}case"user_typing":{let a=r.user_id;a&&this.emit("typing",{userId:a,conversationId:s});break}case"user_stopped_typing":{let a=r.user_id;a&&this.emit("typing:stop",{userId:a,conversationId:s});break}case"typing_batch":{let a=r.users??[];for(let d of a)this.emit("typing",{userId:d,conversationId:s});break}case"messages_read":{let a=r.user_id,d=r.last_read_at;a&&d&&this.emit("read",{userId:a,conversationId:s,lastReadAt:d});break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type})}};var _="0.0.1";var f={create(c){return new p(c)},version:_};var C=class extends HTMLElement{constructor(){super();this.client=null;this.unsub=null;this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["api-key","conversation-id","api-base-url","embed-token"]}connectedCallback(){this.initialize()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(){this.cleanup(),this.initialize()}initialize(){let e=this.getAttribute("api-key")??void 0,t=this.getAttribute("conversation-id"),s=this.getAttribute("api-base-url")??void 0,i=this.getAttribute("embed-token")??void 0;if(!e&&!i)return;let o={apiKey:e,embedToken:i,apiBaseUrl:s};this.client=new p(o),this.shadow.innerHTML=`
1
+ "use strict";var ScaleMuleChat=(()=>{var b=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var R=(c,n)=>{for(var e in n)b(c,e,{get:n[e],enumerable:!0})},x=(c,n,e,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of M(n))!A.call(c,s)&&s!==e&&b(c,s,{get:()=>n[s],enumerable:!(t=S(n,s))||t.enumerable});return c};var P=c=>x(b({},"__esModule",{value:!0}),c);var H={};R(H,{ChatClient:()=>d,create:()=>K,version:()=>W});var u=class{constructor(){this.listeners=new Map}on(n,e){return this.listeners.has(n)||this.listeners.set(n,new Set),this.listeners.get(n).add(e),()=>this.off(n,e)}off(n,e){this.listeners.get(n)?.delete(e)}once(n,e){let t=(s=>{this.off(n,t),e(s)});return this.on(n,t)}emit(n,...[e]){let t=this.listeners.get(n);if(t)for(let s of t)try{s(e)}catch(i){console.error(`[ScaleMuleChat] Error in ${String(n)} listener:`,i)}}removeAllListeners(n){n?this.listeners.delete(n):this.listeners.clear()}};var T="https://api.scalemule.com";var l=class{constructor(n,e){this.cache=new Map;this.maxMessages=n??200,this.maxConversations=e??50}getMessages(n){return this.cache.get(n)??[]}setMessages(n,e){this.cache.set(n,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(n,e){let t=this.cache.get(n)??[];t.some(s=>s.id===e.id)||(t.push(e),t.sort((s,i)=>s.created_at.localeCompare(i.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(n,t))}updateMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e.id);s>=0&&(t[s]=e)}removeMessage(n,e){let t=this.cache.get(n);if(!t)return;let s=t.findIndex(i=>i.id===e);s>=0&&t.splice(s,1)}clear(n){n?this.cache.delete(n):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let n=this.cache.keys().next().value;n&&this.cache.delete(n)}}};var _="scalemule_chat_offline_queue",g=class{constructor(n=!0){this.queue=[];this.enabled=n,this.enabled&&this.load()}enqueue(n,e,t="text",s){this.enabled&&(this.queue.push({conversationId:n,content:e,message_type:t,attachments:s,timestamp:Date.now()}),this.save())}drain(){let n=[...this.queue];return this.queue=[],this.save(),n}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(_,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let n=localStorage.getItem(_);n&&(this.queue=JSON.parse(n))}}catch{this.queue=[]}}};var m=class{constructor(n){this.baseUrl=n.baseUrl.replace(/\/$/,""),this.apiKey=n.apiKey,this.getToken=n.getToken,this.timeout=n.timeout??1e4}async get(n){return this.request("GET",n)}async post(n,e){return this.request("POST",n,e)}async patch(n,e){return this.request("PATCH",n,e)}async del(n){return this.request("DELETE",n)}async request(n,e,t){let s={"Content-Type":"application/json"};if(this.apiKey&&(s["x-api-key"]=this.apiKey),this.getToken){let r=await this.getToken();r&&(s.Authorization=`Bearer ${r}`)}let i=new AbortController,o=setTimeout(()=>i.abort(),this.timeout);try{let r=await fetch(`${this.baseUrl}${e}`,{method:n,headers:s,body:t?JSON.stringify(t):void 0,signal:i.signal,credentials:"include"});if(clearTimeout(o),r.status===204)return{data:null,error:null};let a=await r.json().catch(()=>null);return r.ok?{data:a?.data!==void 0?a.data:a,error:null}:{data:null,error:{code:a?.error?.code??a?.code??"unknown",message:a?.error?.message??a?.message??r.statusText,status:r.status,details:a?.error?.details??a?.details}}}catch(r){return clearTimeout(o),{data:null,error:{code:"network_error",message:r instanceof Error?r.message:"Network error",status:0}}}}};var v=class extends u{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`,this.wsBaseUrl=t.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=s=>{this.handleMessage(s.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e,credentials:"include"});if(!t.ok)return null;let s=await t.json();return s.ticket??s.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var d=class extends u{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??T;this.http=new m({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new v({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new l(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new g(e.offlineQueue??!0),this.ws.on("status",s=>{switch(s){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",s=>{this.emit("reconnecting",s)}),this.ws.on("message",({channel:s,data:i})=>{this.handleRealtimeMessage(s,i)}),this.ws.on("presence:state",({channel:s,members:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:state",{conversationId:o,members:i})}),this.ws.on("presence:join",({channel:s,user:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:join",{userId:i.user_id,conversationId:o,userData:i.user_data})}),this.ws.on("presence:leave",({channel:s,userId:i})=>{let o=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:leave",{userId:i,conversationId:o})}),this.ws.on("presence:update",({channel:s,userId:i,status:o,userData:r})=>{let a=s.replace(/^conversation:(?:lr:|bc:)?/,"");this.emit("presence:update",{userId:i,conversationId:a,status:o,userData:r})}),this.ws.on("error",({message:s})=>{this.emit("error",{code:"ws_error",message:s})})}get status(){return this.ws.getStatus()}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t={...e,conversation_type:e.conversation_type??"direct"},s=await this.http.post("/v1/chat/conversations",t);return s.data&&this.trackConversationType(s.data),s}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page)),e?.conversation_type&&t.set("conversation_type",e.conversation_type);let s=t.toString(),i=await this.http.get(`/v1/chat/conversations${s?"?"+s:""}`);return i.data&&i.data.forEach(o=>this.trackConversationType(o)),i}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let s=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});return s.data?this.cache.addMessage(e,s.data):s.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text"),s}async getMessages(e,t){let s=new URLSearchParams;t?.limit&&s.set("limit",String(t.limit)),t?.before&&s.set("before",t.before),t?.after&&s.set("after",t.after);let i=s.toString(),o=await this.http.get(`/v1/chat/conversations/${e}/messages${i?"?"+i:""}`);return o.data?.messages&&(t?.after||(o.data.messages=o.data.messages.slice().reverse()),!t?.before&&!t?.after&&this.cache.setMessages(e,o.data.messages)),o}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}getCachedMessages(e){return this.cache.getMessages(e)}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async removeReaction(e,t){return this.http.del(`/v1/chat/messages/${e}/reactions/${encodeURIComponent(t)}`)}async getUnreadTotal(){return this.http.get("/v1/chat/conversations/unread-total")}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let s=this.channelName(e);this.ws.joinPresence(s,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,s){let i=this.channelName(e);this.ws.send({type:"presence_update",channel:i,status:t,user_data:s})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}async findChannelBySessionId(e){let t=await this.http.get(`/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(e)}`);return t.error?.status===404?null:(t.data&&this.conversationTypes.set(t.data.id,t.data.channel_type),t.data)}async joinChannel(e){return this.http.post(`/v1/chat/channels/${e}/join`,{})}async createEphemeralChannel(e){let t=await this.http.post("/v1/chat/channels/ephemeral",e);return t.data&&this.conversationTypes.set(t.data.id,"ephemeral"),t}async createLargeRoom(e){let t=await this.http.post("/v1/chat/channels/large-room",e);return t.data&&this.conversationTypes.set(t.data.id,"large_room"),t}async getSubscriberCount(e){return(await this.http.get(`/v1/chat/conversations/${e}/subscriber-count`)).data?.count??0}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),s=this.ws.subscribe(t);return this.conversationSubs.set(e,s),()=>{this.conversationSubs.delete(e),s()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(e.startsWith("conversation:")){this.handleConversationMessage(e,t);return}if(e.startsWith("private:")){this.handlePrivateMessage(t);return}}handlePrivateMessage(e){let t=e;if(!t)return;let s=t.event??t.type,i=t.data??t;if(s==="new_message"){let o=i.conversation_id,r=i.id??i.message_id,a=i.sender_id,h=i.content??"";o&&this.emit("inbox:update",{conversationId:o,messageId:r,senderId:a,preview:h})}}handleConversationMessage(e,t){let s=e.replace(/^conversation:(?:lr:|bc:)?/,""),i=t;if(!i)return;let o=i.event??i.type,r=i.data??i;switch(o){case"new_message":{let a=r;this.cache.addMessage(s,a),this.emit("message",{message:a,conversationId:s});break}case"message_edited":{let a=r;this.cache.updateMessage(s,a),this.emit("message:updated",{message:a,conversationId:s});break}case"message_deleted":{let a=r.message_id??r.id;a&&(this.cache.removeMessage(s,a),this.emit("message:deleted",{messageId:a,conversationId:s}));break}case"user_typing":{let a=r.user_id;a&&this.emit("typing",{userId:a,conversationId:s});break}case"user_stopped_typing":{let a=r.user_id;a&&this.emit("typing:stop",{userId:a,conversationId:s});break}case"typing_batch":{let a=r.users??[];for(let h of a)this.emit("typing",{userId:h,conversationId:s});break}case"messages_read":{let a=r.user_id,h=r.last_read_at;a&&h&&this.emit("read",{userId:a,conversationId:s,lastReadAt:h});break}case"room_upgraded":{if(r.new_type==="large_room"){this.conversationTypes.set(s,"large_room");let h=this.conversationSubs.get(s);h&&(h(),this.conversationSubs.delete(s),this.subscribeToConversation(s)),this.emit("room_upgraded",{conversationId:s,newType:"large_room"})}break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type})}};var w="0.0.1";var f={create(c){return new d(c)},version:w};var C=class extends HTMLElement{constructor(){super();this.client=null;this.unsub=null;this.shadow=this.attachShadow({mode:"open"})}static get observedAttributes(){return["api-key","conversation-id","api-base-url","embed-token"]}connectedCallback(){this.initialize()}disconnectedCallback(){this.cleanup()}attributeChangedCallback(){this.cleanup(),this.initialize()}initialize(){let e=this.getAttribute("api-key")??void 0,t=this.getAttribute("conversation-id"),s=this.getAttribute("api-base-url")??void 0,i=this.getAttribute("embed-token")??void 0;if(!e&&!i)return;let o={apiKey:e,embedToken:i,apiBaseUrl:s};this.client=new d(o),this.shadow.innerHTML=`
2
2
  <style>
3
3
  :host { display: block; width: 100%; height: 100%; }
4
4
  .chat-container { width: 100%; height: 100%; display: flex; flex-direction: column; font-family: system-ui, sans-serif; }
@@ -15,4 +15,4 @@
15
15
  <button id="send">Send</button>
16
16
  </div>
17
17
  </div>
18
- `;let r=this.shadow.getElementById("messages"),a=this.shadow.getElementById("input"),d=this.shadow.getElementById("send");this.client.on("message",({message:h})=>{this.appendMessage(r,h),this.dispatchEvent(new CustomEvent("chat-message",{detail:h,composed:!0,bubbles:!0}))});let b=()=>{let h=a.value.trim();!h||!t||!this.client||(this.client.sendMessage(t,{content:h}),a.value="")};d.addEventListener("click",b),a.addEventListener("keydown",h=>{h.key==="Enter"&&b()});let E=this.client;t&&(this.client.getConversation(t).then(()=>{this.client===E&&(this.unsub=this.client.subscribeToConversation(t),this.client.connect())}),this.client.getMessages(t).then(h=>{if(h.data?.messages)for(let k of h.data.messages)this.appendMessage(r,k)}))}appendMessage(e,t){let s=document.createElement("div");s.className="message",s.textContent=t.content,e.appendChild(s),e.scrollTop=e.scrollHeight}cleanup(){this.unsub?.(),this.unsub=null,this.client?.destroy(),this.client=null,this.shadow.innerHTML=""}};typeof customElements<"u"&&!customElements.get("scalemule-chat")&&customElements.define("scalemule-chat",C);var $=f.create.bind(f),H=f.version;return P(W);})();
18
+ `;let r=this.shadow.getElementById("messages"),a=this.shadow.getElementById("input"),h=this.shadow.getElementById("send");this.client.on("message",({message:p})=>{this.appendMessage(r,p),this.dispatchEvent(new CustomEvent("chat-message",{detail:p,composed:!0,bubbles:!0}))});let y=()=>{let p=a.value.trim();!p||!t||!this.client||(this.client.sendMessage(t,{content:p}),a.value="")};h.addEventListener("click",y),a.addEventListener("keydown",p=>{p.key==="Enter"&&y()});let k=this.client;t&&(this.client.getConversation(t).then(()=>{this.client===k&&(this.unsub=this.client.subscribeToConversation(t),this.client.connect())}),this.client.getMessages(t).then(p=>{if(p.data?.messages)for(let E of p.data.messages)this.appendMessage(r,E)}))}appendMessage(e,t){let s=document.createElement("div");s.className="message",s.textContent=t.content,e.appendChild(s),e.scrollTop=e.scrollHeight}cleanup(){this.unsub?.(),this.unsub=null,this.client?.destroy(),this.client=null,this.shadow.innerHTML=""}};typeof customElements<"u"&&!customElements.get("scalemule-chat")&&customElements.define("scalemule-chat",C);var K=f.create.bind(f),W=f.version;return P(H);})();
@@ -521,7 +521,11 @@ var ChatClient = class extends EventEmitter {
521
521
  }
522
522
  // ============ Conversations ============
523
523
  async createConversation(options) {
524
- const result = await this.http.post("/v1/chat/conversations", options);
524
+ const body = {
525
+ ...options,
526
+ conversation_type: options.conversation_type ?? "direct"
527
+ };
528
+ const result = await this.http.post("/v1/chat/conversations", body);
525
529
  if (result.data) this.trackConversationType(result.data);
526
530
  return result;
527
531
  }
@@ -529,6 +533,7 @@ var ChatClient = class extends EventEmitter {
529
533
  const params = new URLSearchParams();
530
534
  if (options?.page) params.set("page", String(options.page));
531
535
  if (options?.per_page) params.set("per_page", String(options.per_page));
536
+ if (options?.conversation_type) params.set("conversation_type", options.conversation_type);
532
537
  const qs = params.toString();
533
538
  const result = await this.http.get(`/v1/chat/conversations${qs ? "?" + qs : ""}`);
534
539
  if (result.data) result.data.forEach((c) => this.trackConversationType(c));
@@ -571,6 +576,9 @@ var ChatClient = class extends EventEmitter {
571
576
  `/v1/chat/conversations/${conversationId}/messages${qs ? "?" + qs : ""}`
572
577
  );
573
578
  if (result.data?.messages) {
579
+ if (!options?.after) {
580
+ result.data.messages = result.data.messages.slice().reverse();
581
+ }
574
582
  if (!options?.before && !options?.after) {
575
583
  this.cache.setMessages(conversationId, result.data.messages);
576
584
  }
@@ -590,6 +598,13 @@ var ChatClient = class extends EventEmitter {
590
598
  async addReaction(messageId, emoji) {
591
599
  return this.http.post(`/v1/chat/messages/${messageId}/reactions`, { emoji });
592
600
  }
601
+ async removeReaction(messageId, emoji) {
602
+ return this.http.del(`/v1/chat/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
603
+ }
604
+ // ============ Unread Count ============
605
+ async getUnreadTotal() {
606
+ return this.http.get("/v1/chat/conversations/unread-total");
607
+ }
593
608
  // ============ Typing & Read Receipts ============
594
609
  async sendTyping(conversationId, isTyping = true) {
595
610
  await this.http.post(`/v1/chat/conversations/${conversationId}/typing`, { is_typing: isTyping });
@@ -634,6 +649,54 @@ var ChatClient = class extends EventEmitter {
634
649
  setConversationType(conversationId, type) {
635
650
  this.conversationTypes.set(conversationId, type);
636
651
  }
652
+ // ============ Channel Methods ============
653
+ /**
654
+ * Find an active ephemeral or large_room channel by linked session ID.
655
+ * Returns null (not an error) if no active channel exists.
656
+ */
657
+ async findChannelBySessionId(linkedSessionId) {
658
+ const result = await this.http.get(
659
+ `/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(linkedSessionId)}`
660
+ );
661
+ if (result.error?.status === 404) return null;
662
+ if (result.data) {
663
+ this.conversationTypes.set(result.data.id, result.data.channel_type);
664
+ }
665
+ return result.data;
666
+ }
667
+ /**
668
+ * Self-join an ephemeral or large_room channel. Idempotent.
669
+ */
670
+ async joinChannel(channelId) {
671
+ return this.http.post(`/v1/chat/channels/${channelId}/join`, {});
672
+ }
673
+ /**
674
+ * Create an ephemeral channel tied to a session (e.g., a video snap).
675
+ */
676
+ async createEphemeralChannel(options) {
677
+ const result = await this.http.post("/v1/chat/channels/ephemeral", options);
678
+ if (result.data) {
679
+ this.conversationTypes.set(result.data.id, "ephemeral");
680
+ }
681
+ return result;
682
+ }
683
+ /**
684
+ * Create a large room channel (high-concurrency, skips MySQL tracking in realtime).
685
+ */
686
+ async createLargeRoom(options) {
687
+ const result = await this.http.post("/v1/chat/channels/large-room", options);
688
+ if (result.data) {
689
+ this.conversationTypes.set(result.data.id, "large_room");
690
+ }
691
+ return result;
692
+ }
693
+ /**
694
+ * Get the global concurrent subscriber count for a conversation.
695
+ */
696
+ async getSubscriberCount(conversationId) {
697
+ const result = await this.http.get(`/v1/chat/conversations/${conversationId}/subscriber-count`);
698
+ return result.data?.count ?? 0;
699
+ }
637
700
  // ============ Realtime Subscriptions ============
638
701
  subscribeToConversation(conversationId) {
639
702
  if (this.conversationSubs.has(conversationId)) {
@@ -667,7 +730,34 @@ var ChatClient = class extends EventEmitter {
667
730
  }
668
731
  // ============ Private ============
669
732
  handleRealtimeMessage(channel, data) {
670
- if (!channel.startsWith("conversation:")) return;
733
+ if (channel.startsWith("conversation:")) {
734
+ this.handleConversationMessage(channel, data);
735
+ return;
736
+ }
737
+ if (channel.startsWith("private:")) {
738
+ this.handlePrivateMessage(data);
739
+ return;
740
+ }
741
+ }
742
+ handlePrivateMessage(data) {
743
+ const raw = data;
744
+ if (!raw) return;
745
+ const event = raw.event ?? raw.type;
746
+ const payload = raw.data ?? raw;
747
+ switch (event) {
748
+ case "new_message": {
749
+ const conversationId = payload.conversation_id;
750
+ const messageId = payload.id ?? payload.message_id;
751
+ const senderId = payload.sender_id;
752
+ const preview = payload.content ?? "";
753
+ if (conversationId) {
754
+ this.emit("inbox:update", { conversationId, messageId, senderId, preview });
755
+ }
756
+ break;
757
+ }
758
+ }
759
+ }
760
+ handleConversationMessage(channel, data) {
671
761
  const conversationId = channel.replace(/^conversation:(?:lr:|bc:)?/, "");
672
762
  const raw = data;
673
763
  if (!raw) return;
@@ -723,6 +813,20 @@ var ChatClient = class extends EventEmitter {
723
813
  }
724
814
  break;
725
815
  }
816
+ case "room_upgraded": {
817
+ const newType = payload.new_type;
818
+ if (newType === "large_room") {
819
+ this.conversationTypes.set(conversationId, "large_room");
820
+ const oldUnsub = this.conversationSubs.get(conversationId);
821
+ if (oldUnsub) {
822
+ oldUnsub();
823
+ this.conversationSubs.delete(conversationId);
824
+ this.subscribeToConversation(conversationId);
825
+ }
826
+ this.emit("room_upgraded", { conversationId, newType: "large_room" });
827
+ }
828
+ break;
829
+ }
726
830
  }
727
831
  }
728
832
  async flushOfflineQueue() {
@@ -523,7 +523,11 @@ var ChatClient = class extends EventEmitter {
523
523
  }
524
524
  // ============ Conversations ============
525
525
  async createConversation(options) {
526
- const result = await this.http.post("/v1/chat/conversations", options);
526
+ const body = {
527
+ ...options,
528
+ conversation_type: options.conversation_type ?? "direct"
529
+ };
530
+ const result = await this.http.post("/v1/chat/conversations", body);
527
531
  if (result.data) this.trackConversationType(result.data);
528
532
  return result;
529
533
  }
@@ -531,6 +535,7 @@ var ChatClient = class extends EventEmitter {
531
535
  const params = new URLSearchParams();
532
536
  if (options?.page) params.set("page", String(options.page));
533
537
  if (options?.per_page) params.set("per_page", String(options.per_page));
538
+ if (options?.conversation_type) params.set("conversation_type", options.conversation_type);
534
539
  const qs = params.toString();
535
540
  const result = await this.http.get(`/v1/chat/conversations${qs ? "?" + qs : ""}`);
536
541
  if (result.data) result.data.forEach((c) => this.trackConversationType(c));
@@ -573,6 +578,9 @@ var ChatClient = class extends EventEmitter {
573
578
  `/v1/chat/conversations/${conversationId}/messages${qs ? "?" + qs : ""}`
574
579
  );
575
580
  if (result.data?.messages) {
581
+ if (!options?.after) {
582
+ result.data.messages = result.data.messages.slice().reverse();
583
+ }
576
584
  if (!options?.before && !options?.after) {
577
585
  this.cache.setMessages(conversationId, result.data.messages);
578
586
  }
@@ -592,6 +600,13 @@ var ChatClient = class extends EventEmitter {
592
600
  async addReaction(messageId, emoji) {
593
601
  return this.http.post(`/v1/chat/messages/${messageId}/reactions`, { emoji });
594
602
  }
603
+ async removeReaction(messageId, emoji) {
604
+ return this.http.del(`/v1/chat/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
605
+ }
606
+ // ============ Unread Count ============
607
+ async getUnreadTotal() {
608
+ return this.http.get("/v1/chat/conversations/unread-total");
609
+ }
595
610
  // ============ Typing & Read Receipts ============
596
611
  async sendTyping(conversationId, isTyping = true) {
597
612
  await this.http.post(`/v1/chat/conversations/${conversationId}/typing`, { is_typing: isTyping });
@@ -636,6 +651,54 @@ var ChatClient = class extends EventEmitter {
636
651
  setConversationType(conversationId, type) {
637
652
  this.conversationTypes.set(conversationId, type);
638
653
  }
654
+ // ============ Channel Methods ============
655
+ /**
656
+ * Find an active ephemeral or large_room channel by linked session ID.
657
+ * Returns null (not an error) if no active channel exists.
658
+ */
659
+ async findChannelBySessionId(linkedSessionId) {
660
+ const result = await this.http.get(
661
+ `/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(linkedSessionId)}`
662
+ );
663
+ if (result.error?.status === 404) return null;
664
+ if (result.data) {
665
+ this.conversationTypes.set(result.data.id, result.data.channel_type);
666
+ }
667
+ return result.data;
668
+ }
669
+ /**
670
+ * Self-join an ephemeral or large_room channel. Idempotent.
671
+ */
672
+ async joinChannel(channelId) {
673
+ return this.http.post(`/v1/chat/channels/${channelId}/join`, {});
674
+ }
675
+ /**
676
+ * Create an ephemeral channel tied to a session (e.g., a video snap).
677
+ */
678
+ async createEphemeralChannel(options) {
679
+ const result = await this.http.post("/v1/chat/channels/ephemeral", options);
680
+ if (result.data) {
681
+ this.conversationTypes.set(result.data.id, "ephemeral");
682
+ }
683
+ return result;
684
+ }
685
+ /**
686
+ * Create a large room channel (high-concurrency, skips MySQL tracking in realtime).
687
+ */
688
+ async createLargeRoom(options) {
689
+ const result = await this.http.post("/v1/chat/channels/large-room", options);
690
+ if (result.data) {
691
+ this.conversationTypes.set(result.data.id, "large_room");
692
+ }
693
+ return result;
694
+ }
695
+ /**
696
+ * Get the global concurrent subscriber count for a conversation.
697
+ */
698
+ async getSubscriberCount(conversationId) {
699
+ const result = await this.http.get(`/v1/chat/conversations/${conversationId}/subscriber-count`);
700
+ return result.data?.count ?? 0;
701
+ }
639
702
  // ============ Realtime Subscriptions ============
640
703
  subscribeToConversation(conversationId) {
641
704
  if (this.conversationSubs.has(conversationId)) {
@@ -669,7 +732,34 @@ var ChatClient = class extends EventEmitter {
669
732
  }
670
733
  // ============ Private ============
671
734
  handleRealtimeMessage(channel, data) {
672
- if (!channel.startsWith("conversation:")) return;
735
+ if (channel.startsWith("conversation:")) {
736
+ this.handleConversationMessage(channel, data);
737
+ return;
738
+ }
739
+ if (channel.startsWith("private:")) {
740
+ this.handlePrivateMessage(data);
741
+ return;
742
+ }
743
+ }
744
+ handlePrivateMessage(data) {
745
+ const raw = data;
746
+ if (!raw) return;
747
+ const event = raw.event ?? raw.type;
748
+ const payload = raw.data ?? raw;
749
+ switch (event) {
750
+ case "new_message": {
751
+ const conversationId = payload.conversation_id;
752
+ const messageId = payload.id ?? payload.message_id;
753
+ const senderId = payload.sender_id;
754
+ const preview = payload.content ?? "";
755
+ if (conversationId) {
756
+ this.emit("inbox:update", { conversationId, messageId, senderId, preview });
757
+ }
758
+ break;
759
+ }
760
+ }
761
+ }
762
+ handleConversationMessage(channel, data) {
673
763
  const conversationId = channel.replace(/^conversation:(?:lr:|bc:)?/, "");
674
764
  const raw = data;
675
765
  if (!raw) return;
@@ -725,6 +815,20 @@ var ChatClient = class extends EventEmitter {
725
815
  }
726
816
  break;
727
817
  }
818
+ case "room_upgraded": {
819
+ const newType = payload.new_type;
820
+ if (newType === "large_room") {
821
+ this.conversationTypes.set(conversationId, "large_room");
822
+ const oldUnsub = this.conversationSubs.get(conversationId);
823
+ if (oldUnsub) {
824
+ oldUnsub();
825
+ this.conversationSubs.delete(conversationId);
826
+ this.subscribeToConversation(conversationId);
827
+ }
828
+ this.emit("room_upgraded", { conversationId, newType: "large_room" });
829
+ }
830
+ break;
831
+ }
728
832
  }
729
833
  }
730
834
  async flushOfflineQueue() {
package/dist/element.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNFOVLPF2_cjs = require('./chunk-NFOVLPF2.cjs');
3
+ var chunkWYFNZUAL_cjs = require('./chunk-WYFNZUAL.cjs');
4
4
 
5
5
  // src/element.ts
6
6
  var ScaleMuleChatElement = class extends HTMLElement {
@@ -34,7 +34,7 @@ var ScaleMuleChatElement = class extends HTMLElement {
34
34
  embedToken,
35
35
  apiBaseUrl
36
36
  };
37
- this.client = new chunkNFOVLPF2_cjs.ChatClient(config);
37
+ this.client = new chunkWYFNZUAL_cjs.ChatClient(config);
38
38
  this.shadow.innerHTML = `
39
39
  <style>
40
40
  :host { display: block; width: 100%; height: 100%; }
package/dist/element.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatClient } from './chunk-6QRI4CJZ.js';
1
+ import { ChatClient } from './chunk-FXO4ZYQB.js';
2
2
 
3
3
  // src/element.ts
4
4
  var ScaleMuleChatElement = class extends HTMLElement {
package/dist/iframe.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as ChatConfig } from './types-BwgD_Etd.cjs';
1
+ import { d as ChatConfig } from './types-FQcFhbJx.cjs';
2
2
 
3
3
  declare class ChatIframeController {
4
4
  private iframe;
package/dist/iframe.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as ChatConfig } from './types-BwgD_Etd.js';
1
+ import { d as ChatConfig } from './types-FQcFhbJx.js';
2
2
 
3
3
  declare class ChatIframeController {
4
4
  private iframe;
package/dist/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunkNFOVLPF2_cjs = require('./chunk-NFOVLPF2.cjs');
3
+ var chunkWYFNZUAL_cjs = require('./chunk-WYFNZUAL.cjs');
4
4
 
5
5
  // src/version.ts
6
6
  var CHAT_VERSION = "0.0.1";
7
7
 
8
8
  Object.defineProperty(exports, "ChatClient", {
9
9
  enumerable: true,
10
- get: function () { return chunkNFOVLPF2_cjs.ChatClient; }
10
+ get: function () { return chunkWYFNZUAL_cjs.ChatClient; }
11
11
  });
12
12
  exports.CHAT_VERSION = CHAT_VERSION;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- export { C as ChatClient } from './ChatClient-CXPywp1w.cjs';
2
- export { A as ApiError, a as ApiResponse, b as Attachment, C as ChannelSettings, c as ChatConfig, d as ChatEventMap, e as ChatMessage, f as ChatReaction, g as ConnectionStatus, h as Conversation, i as CreateConversationOptions, G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, P as Participant, j as PresenceMember, R as ReadStatus, S as SendMessageOptions } from './types-BwgD_Etd.cjs';
1
+ export { C as ChatClient } from './ChatClient-lwyQYmNf.cjs';
2
+ export { A as ApiError, a as ApiResponse, b as Attachment, C as ChannelSettings, c as ChannelWithSettings, d as ChatConfig, e as ChatEventMap, f as ChatMessage, g as ChatReaction, h as ConnectionStatus, i as Conversation, j as CreateConversationOptions, k as CreateEphemeralChannelOptions, l as CreateLargeRoomOptions, G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, P as Participant, m as PresenceMember, R as ReactionSummary, n as ReadStatus, S as SendMessageOptions, U as UnreadTotalResponse } from './types-FQcFhbJx.cjs';
3
3
 
4
4
  declare const CHAT_VERSION = "0.0.1";
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { C as ChatClient } from './ChatClient-DCid_mmU.js';
2
- export { A as ApiError, a as ApiResponse, b as Attachment, C as ChannelSettings, c as ChatConfig, d as ChatEventMap, e as ChatMessage, f as ChatReaction, g as ConnectionStatus, h as Conversation, i as CreateConversationOptions, G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, P as Participant, j as PresenceMember, R as ReadStatus, S as SendMessageOptions } from './types-BwgD_Etd.js';
1
+ export { C as ChatClient } from './ChatClient-BD0sMudg.js';
2
+ export { A as ApiError, a as ApiResponse, b as Attachment, C as ChannelSettings, c as ChannelWithSettings, d as ChatConfig, e as ChatEventMap, f as ChatMessage, g as ChatReaction, h as ConnectionStatus, i as Conversation, j as CreateConversationOptions, k as CreateEphemeralChannelOptions, l as CreateLargeRoomOptions, G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, P as Participant, m as PresenceMember, R as ReactionSummary, n as ReadStatus, S as SendMessageOptions, U as UnreadTotalResponse } from './types-FQcFhbJx.js';
3
3
 
4
4
  declare const CHAT_VERSION = "0.0.1";
5
5
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { ChatClient } from './chunk-6QRI4CJZ.js';
1
+ export { ChatClient } from './chunk-FXO4ZYQB.js';
2
2
 
3
3
  // src/version.ts
4
4
  var CHAT_VERSION = "0.0.1";
package/dist/react.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkNFOVLPF2_cjs = require('./chunk-NFOVLPF2.cjs');
3
+ var chunkWYFNZUAL_cjs = require('./chunk-WYFNZUAL.cjs');
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
@@ -11,7 +11,7 @@ function useChatContext() {
11
11
  return ctx;
12
12
  }
13
13
  function ChatProvider({ config, children }) {
14
- const [client] = react.useState(() => new chunkNFOVLPF2_cjs.ChatClient(config));
14
+ const [client] = react.useState(() => new chunkWYFNZUAL_cjs.ChatClient(config));
15
15
  react.useEffect(() => {
16
16
  return () => {
17
17
  client.destroy();
@@ -230,13 +230,82 @@ function useTyping(conversationId) {
230
230
  );
231
231
  return { typingUsers, sendTyping };
232
232
  }
233
+ function useConversations(options) {
234
+ const { client } = useChatContext();
235
+ const [conversations, setConversations] = react.useState([]);
236
+ const [isLoading, setIsLoading] = react.useState(true);
237
+ const refreshTimer = react.useRef(null);
238
+ const fetchConversations = react.useCallback(async () => {
239
+ const result = await client.listConversations({
240
+ conversation_type: options?.conversationType
241
+ });
242
+ if (result.data) {
243
+ setConversations(result.data);
244
+ }
245
+ setIsLoading(false);
246
+ }, [client, options?.conversationType]);
247
+ react.useEffect(() => {
248
+ fetchConversations();
249
+ }, [fetchConversations]);
250
+ react.useEffect(() => {
251
+ return client.on("inbox:update", () => {
252
+ if (refreshTimer.current) clearTimeout(refreshTimer.current);
253
+ refreshTimer.current = setTimeout(() => {
254
+ fetchConversations();
255
+ }, 500);
256
+ });
257
+ }, [client, fetchConversations]);
258
+ react.useEffect(() => {
259
+ return client.on("read", ({ conversationId }) => {
260
+ setConversations(
261
+ (prev) => prev.map((c) => c.id === conversationId ? { ...c, unread_count: 0 } : c)
262
+ );
263
+ });
264
+ }, [client]);
265
+ react.useEffect(() => {
266
+ return () => {
267
+ if (refreshTimer.current) clearTimeout(refreshTimer.current);
268
+ };
269
+ }, []);
270
+ return { conversations, isLoading, refresh: fetchConversations };
271
+ }
272
+ function useUnreadCount() {
273
+ const { client } = useChatContext();
274
+ const [totalUnread, setTotalUnread] = react.useState(0);
275
+ react.useEffect(() => {
276
+ (async () => {
277
+ const result = await client.getUnreadTotal();
278
+ if (result.data) {
279
+ setTotalUnread(result.data.unread_messages);
280
+ }
281
+ })();
282
+ }, [client]);
283
+ react.useEffect(() => {
284
+ return client.on("inbox:update", () => {
285
+ setTotalUnread((prev) => prev + 1);
286
+ });
287
+ }, [client]);
288
+ react.useEffect(() => {
289
+ return client.on("read", () => {
290
+ (async () => {
291
+ const result = await client.getUnreadTotal();
292
+ if (result.data) {
293
+ setTotalUnread(result.data.unread_messages);
294
+ }
295
+ })();
296
+ });
297
+ }, [client]);
298
+ return { totalUnread };
299
+ }
233
300
 
234
301
  Object.defineProperty(exports, "ChatClient", {
235
302
  enumerable: true,
236
- get: function () { return chunkNFOVLPF2_cjs.ChatClient; }
303
+ get: function () { return chunkWYFNZUAL_cjs.ChatClient; }
237
304
  });
238
305
  exports.ChatProvider = ChatProvider;
239
306
  exports.useChat = useChat;
240
307
  exports.useConnection = useConnection;
308
+ exports.useConversations = useConversations;
241
309
  exports.usePresence = usePresence;
242
310
  exports.useTyping = useTyping;
311
+ exports.useUnreadCount = useUnreadCount;
package/dist/react.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { c as ChatConfig, e as ChatMessage, S as SendMessageOptions, a as ApiResponse, g as ConnectionStatus } from './types-BwgD_Etd.cjs';
3
- export { h as Conversation, G as GetMessagesOptions, M as MessagesResponse } from './types-BwgD_Etd.cjs';
4
- export { C as ChatClient } from './ChatClient-CXPywp1w.cjs';
2
+ import { d as ChatConfig, f as ChatMessage, S as SendMessageOptions, a as ApiResponse, h as ConnectionStatus, i as Conversation } from './types-FQcFhbJx.cjs';
3
+ export { G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, R as ReactionSummary, U as UnreadTotalResponse } from './types-FQcFhbJx.cjs';
4
+ export { C as ChatClient } from './ChatClient-lwyQYmNf.cjs';
5
5
 
6
6
  interface ChatProviderProps {
7
7
  config: ChatConfig;
@@ -33,5 +33,15 @@ declare function useTyping(conversationId?: string): {
33
33
  typingUsers: string[];
34
34
  sendTyping: (isTyping?: boolean) => void;
35
35
  };
36
+ declare function useConversations(options?: {
37
+ conversationType?: string;
38
+ }): {
39
+ conversations: Conversation[];
40
+ isLoading: boolean;
41
+ refresh: () => Promise<void>;
42
+ };
43
+ declare function useUnreadCount(): {
44
+ totalUnread: number;
45
+ };
36
46
 
37
- export { ApiResponse, ChatConfig, ChatMessage, ChatProvider, ConnectionStatus, SendMessageOptions, useChat, useConnection, usePresence, useTyping };
47
+ export { ApiResponse, ChatConfig, ChatMessage, ChatProvider, ConnectionStatus, Conversation, SendMessageOptions, useChat, useConnection, useConversations, usePresence, useTyping, useUnreadCount };
package/dist/react.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { c as ChatConfig, e as ChatMessage, S as SendMessageOptions, a as ApiResponse, g as ConnectionStatus } from './types-BwgD_Etd.js';
3
- export { h as Conversation, G as GetMessagesOptions, M as MessagesResponse } from './types-BwgD_Etd.js';
4
- export { C as ChatClient } from './ChatClient-DCid_mmU.js';
2
+ import { d as ChatConfig, f as ChatMessage, S as SendMessageOptions, a as ApiResponse, h as ConnectionStatus, i as Conversation } from './types-FQcFhbJx.js';
3
+ export { G as GetMessagesOptions, L as ListConversationsOptions, M as MessagesResponse, R as ReactionSummary, U as UnreadTotalResponse } from './types-FQcFhbJx.js';
4
+ export { C as ChatClient } from './ChatClient-BD0sMudg.js';
5
5
 
6
6
  interface ChatProviderProps {
7
7
  config: ChatConfig;
@@ -33,5 +33,15 @@ declare function useTyping(conversationId?: string): {
33
33
  typingUsers: string[];
34
34
  sendTyping: (isTyping?: boolean) => void;
35
35
  };
36
+ declare function useConversations(options?: {
37
+ conversationType?: string;
38
+ }): {
39
+ conversations: Conversation[];
40
+ isLoading: boolean;
41
+ refresh: () => Promise<void>;
42
+ };
43
+ declare function useUnreadCount(): {
44
+ totalUnread: number;
45
+ };
36
46
 
37
- export { ApiResponse, ChatConfig, ChatMessage, ChatProvider, ConnectionStatus, SendMessageOptions, useChat, useConnection, usePresence, useTyping };
47
+ export { ApiResponse, ChatConfig, ChatMessage, ChatProvider, ConnectionStatus, Conversation, SendMessageOptions, useChat, useConnection, useConversations, usePresence, useTyping, useUnreadCount };
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ChatClient } from './chunk-6QRI4CJZ.js';
2
- export { ChatClient } from './chunk-6QRI4CJZ.js';
1
+ import { ChatClient } from './chunk-FXO4ZYQB.js';
2
+ export { ChatClient } from './chunk-FXO4ZYQB.js';
3
3
  import { createContext, useState, useEffect, useCallback, useRef, useContext } from 'react';
4
4
  import { jsx } from 'react/jsx-runtime';
5
5
 
@@ -229,5 +229,72 @@ function useTyping(conversationId) {
229
229
  );
230
230
  return { typingUsers, sendTyping };
231
231
  }
232
+ function useConversations(options) {
233
+ const { client } = useChatContext();
234
+ const [conversations, setConversations] = useState([]);
235
+ const [isLoading, setIsLoading] = useState(true);
236
+ const refreshTimer = useRef(null);
237
+ const fetchConversations = useCallback(async () => {
238
+ const result = await client.listConversations({
239
+ conversation_type: options?.conversationType
240
+ });
241
+ if (result.data) {
242
+ setConversations(result.data);
243
+ }
244
+ setIsLoading(false);
245
+ }, [client, options?.conversationType]);
246
+ useEffect(() => {
247
+ fetchConversations();
248
+ }, [fetchConversations]);
249
+ useEffect(() => {
250
+ return client.on("inbox:update", () => {
251
+ if (refreshTimer.current) clearTimeout(refreshTimer.current);
252
+ refreshTimer.current = setTimeout(() => {
253
+ fetchConversations();
254
+ }, 500);
255
+ });
256
+ }, [client, fetchConversations]);
257
+ useEffect(() => {
258
+ return client.on("read", ({ conversationId }) => {
259
+ setConversations(
260
+ (prev) => prev.map((c) => c.id === conversationId ? { ...c, unread_count: 0 } : c)
261
+ );
262
+ });
263
+ }, [client]);
264
+ useEffect(() => {
265
+ return () => {
266
+ if (refreshTimer.current) clearTimeout(refreshTimer.current);
267
+ };
268
+ }, []);
269
+ return { conversations, isLoading, refresh: fetchConversations };
270
+ }
271
+ function useUnreadCount() {
272
+ const { client } = useChatContext();
273
+ const [totalUnread, setTotalUnread] = useState(0);
274
+ useEffect(() => {
275
+ (async () => {
276
+ const result = await client.getUnreadTotal();
277
+ if (result.data) {
278
+ setTotalUnread(result.data.unread_messages);
279
+ }
280
+ })();
281
+ }, [client]);
282
+ useEffect(() => {
283
+ return client.on("inbox:update", () => {
284
+ setTotalUnread((prev) => prev + 1);
285
+ });
286
+ }, [client]);
287
+ useEffect(() => {
288
+ return client.on("read", () => {
289
+ (async () => {
290
+ const result = await client.getUnreadTotal();
291
+ if (result.data) {
292
+ setTotalUnread(result.data.unread_messages);
293
+ }
294
+ })();
295
+ });
296
+ }, [client]);
297
+ return { totalUnread };
298
+ }
232
299
 
233
- export { ChatProvider, useChat, useConnection, usePresence, useTyping };
300
+ export { ChatProvider, useChat, useConnection, useConversations, usePresence, useTyping, useUnreadCount };
@@ -36,6 +36,9 @@ interface Conversation {
36
36
  participant_count?: number;
37
37
  last_message_at?: string;
38
38
  unread_count?: number;
39
+ last_message_preview?: string;
40
+ last_message_sender_id?: string;
41
+ counterparty_user_id?: string;
39
42
  created_at: string;
40
43
  participants?: Participant[];
41
44
  }
@@ -52,9 +55,15 @@ interface ChatMessage {
52
55
  sender_type?: string;
53
56
  sender_agent_model?: string;
54
57
  attachments?: Attachment[];
58
+ reactions?: ReactionSummary[];
55
59
  is_edited: boolean;
56
60
  created_at: string;
57
61
  }
62
+ interface ReactionSummary {
63
+ emoji: string;
64
+ count: number;
65
+ user_ids: string[];
66
+ }
58
67
  interface Attachment {
59
68
  file_id: string;
60
69
  file_name: string;
@@ -85,6 +94,26 @@ interface ChannelSettings {
85
94
  max_participants?: number;
86
95
  slow_mode_seconds?: number;
87
96
  }
97
+ interface ChannelWithSettings {
98
+ id: string;
99
+ channel_type: string;
100
+ name?: string;
101
+ created_at: string;
102
+ linked_session_id?: string;
103
+ expires_at?: string;
104
+ participant_count: number;
105
+ }
106
+ interface CreateEphemeralChannelOptions {
107
+ linked_session_id: string;
108
+ name?: string;
109
+ ttl_minutes?: number;
110
+ }
111
+ interface CreateLargeRoomOptions {
112
+ name: string;
113
+ linked_session_id?: string;
114
+ max_participants?: number;
115
+ slow_mode_seconds?: number;
116
+ }
88
117
  interface ChatEventMap {
89
118
  connected: void;
90
119
  disconnected: void;
@@ -139,10 +168,20 @@ interface ChatEventMap {
139
168
  reaction: ChatReaction;
140
169
  conversationId: string;
141
170
  };
171
+ 'room_upgraded': {
172
+ conversationId: string;
173
+ newType: 'large_room';
174
+ };
142
175
  'delivery': {
143
176
  messageId: string;
144
177
  status: 'sent' | 'delivered' | 'read';
145
178
  };
179
+ 'inbox:update': {
180
+ conversationId: string;
181
+ messageId: string;
182
+ senderId: string;
183
+ preview: string;
184
+ };
146
185
  'error': {
147
186
  code: string;
148
187
  message: string;
@@ -157,6 +196,11 @@ interface SendMessageOptions {
157
196
  interface ListConversationsOptions {
158
197
  page?: number;
159
198
  per_page?: number;
199
+ conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
200
+ }
201
+ interface UnreadTotalResponse {
202
+ unread_conversations: number;
203
+ unread_messages: number;
160
204
  }
161
205
  interface GetMessagesOptions {
162
206
  limit?: number;
@@ -175,4 +219,4 @@ interface MessagesResponse {
175
219
  newest_id?: string;
176
220
  }
177
221
 
178
- export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReadStatus as R, SendMessageOptions as S, ApiResponse as a, Attachment as b, ChatConfig as c, ChatEventMap as d, ChatMessage as e, ChatReaction as f, ConnectionStatus as g, Conversation as h, CreateConversationOptions as i, PresenceMember as j };
222
+ export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ApiResponse as a, Attachment as b, ChannelWithSettings as c, ChatConfig as d, ChatEventMap as e, ChatMessage as f, ChatReaction as g, ConnectionStatus as h, Conversation as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
@@ -36,6 +36,9 @@ interface Conversation {
36
36
  participant_count?: number;
37
37
  last_message_at?: string;
38
38
  unread_count?: number;
39
+ last_message_preview?: string;
40
+ last_message_sender_id?: string;
41
+ counterparty_user_id?: string;
39
42
  created_at: string;
40
43
  participants?: Participant[];
41
44
  }
@@ -52,9 +55,15 @@ interface ChatMessage {
52
55
  sender_type?: string;
53
56
  sender_agent_model?: string;
54
57
  attachments?: Attachment[];
58
+ reactions?: ReactionSummary[];
55
59
  is_edited: boolean;
56
60
  created_at: string;
57
61
  }
62
+ interface ReactionSummary {
63
+ emoji: string;
64
+ count: number;
65
+ user_ids: string[];
66
+ }
58
67
  interface Attachment {
59
68
  file_id: string;
60
69
  file_name: string;
@@ -85,6 +94,26 @@ interface ChannelSettings {
85
94
  max_participants?: number;
86
95
  slow_mode_seconds?: number;
87
96
  }
97
+ interface ChannelWithSettings {
98
+ id: string;
99
+ channel_type: string;
100
+ name?: string;
101
+ created_at: string;
102
+ linked_session_id?: string;
103
+ expires_at?: string;
104
+ participant_count: number;
105
+ }
106
+ interface CreateEphemeralChannelOptions {
107
+ linked_session_id: string;
108
+ name?: string;
109
+ ttl_minutes?: number;
110
+ }
111
+ interface CreateLargeRoomOptions {
112
+ name: string;
113
+ linked_session_id?: string;
114
+ max_participants?: number;
115
+ slow_mode_seconds?: number;
116
+ }
88
117
  interface ChatEventMap {
89
118
  connected: void;
90
119
  disconnected: void;
@@ -139,10 +168,20 @@ interface ChatEventMap {
139
168
  reaction: ChatReaction;
140
169
  conversationId: string;
141
170
  };
171
+ 'room_upgraded': {
172
+ conversationId: string;
173
+ newType: 'large_room';
174
+ };
142
175
  'delivery': {
143
176
  messageId: string;
144
177
  status: 'sent' | 'delivered' | 'read';
145
178
  };
179
+ 'inbox:update': {
180
+ conversationId: string;
181
+ messageId: string;
182
+ senderId: string;
183
+ preview: string;
184
+ };
146
185
  'error': {
147
186
  code: string;
148
187
  message: string;
@@ -157,6 +196,11 @@ interface SendMessageOptions {
157
196
  interface ListConversationsOptions {
158
197
  page?: number;
159
198
  per_page?: number;
199
+ conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
200
+ }
201
+ interface UnreadTotalResponse {
202
+ unread_conversations: number;
203
+ unread_messages: number;
160
204
  }
161
205
  interface GetMessagesOptions {
162
206
  limit?: number;
@@ -175,4 +219,4 @@ interface MessagesResponse {
175
219
  newest_id?: string;
176
220
  }
177
221
 
178
- export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReadStatus as R, SendMessageOptions as S, ApiResponse as a, Attachment as b, ChatConfig as c, ChatEventMap as d, ChatMessage as e, ChatReaction as f, ConnectionStatus as g, Conversation as h, CreateConversationOptions as i, PresenceMember as j };
222
+ export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ApiResponse as a, Attachment as b, ChannelWithSettings as c, ChatConfig as d, ChatEventMap as e, ChatMessage as f, ChatReaction as g, ConnectionStatus as h, Conversation as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/chat",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "ScaleMule standalone chat SDK — real-time messaging, presence, typing indicators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",