ajaxter-chat 3.1.0 → 3.2.0

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,56 +1,117 @@
1
- import { LocalEnvConfig, RemoteChatData } from '../types';
1
+ import { LocalEnvConfig, RemoteChatData, AjaxterConfigResponse, WidgetConfig } from '../types';
2
2
 
3
- /**
4
- * Default JSON endpoint. Override with `REACT_APP_CHAT_CONFIG_URL` / `NEXT_PUBLIC_CHAT_CONFIG_URL`.
5
- * If the remote host does not send CORS headers, set this to a same-origin path (e.g. `/api/chat-config`)
6
- * and proxy the JSON from your server — see `examples/next-app-router-chat-proxy.ts`.
7
- */
8
- const DEFAULT_CHAT_DATA_BASE = 'https://window.mscorpres.com/TEST/chatData.json';
9
- const DEMO_API_KEY = 'demo1234';
10
- const DEMO_WIDGET_ID = 'demo';
3
+ const DEFAULT_SOCKET_URL = 'http://localhost:3005';
4
+ const DEFAULT_CONFIG_URL = 'http://localhost:5000/api/chat-config';
5
+ const DEMO_API_KEY = 'demo1234';
6
+ const DEMO_WIDGET_ID = 'demo';
11
7
 
12
8
  function getEnv(key: string): string | undefined {
13
9
  if (typeof process !== 'undefined' && process.env) {
14
10
  return (
15
11
  process.env[`NEXT_PUBLIC_${key}`] ??
16
- process.env[`REACT_APP_${key}`] ??
17
- process.env[key] ??
12
+ process.env[`REACT_APP_${key}`] ??
13
+ process.env[key] ??
18
14
  undefined
19
15
  );
20
16
  }
21
17
  return undefined;
22
18
  }
23
19
 
24
- function getChatDataBaseUrl(): string {
25
- return getEnv('CHAT_CONFIG_URL')?.trim() || DEFAULT_CHAT_DATA_BASE;
20
+ export function loadLocalConfig(): LocalEnvConfig {
21
+ return {
22
+ apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
23
+ widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
24
+ socketUrl: getEnv('SOCKET_URL') ?? DEFAULT_SOCKET_URL,
25
+ };
26
+ }
27
+
28
+ function getConfigUrl(): string {
29
+ return getEnv('CHAT_CONFIG_URL')?.trim() ?? DEFAULT_CONFIG_URL;
26
30
  }
27
31
 
28
32
  /**
29
- * Loads remote widget config once via GET. Uses query params `key` and `widget` so the
30
- * server can validate the request without custom headers (avoids CORS preflight failures).
33
+ * Fetches widget config ONCE from the server.
34
+ * URL: GET /api/chat-config?key=<apiKey>&widget=<widgetId>
35
+ *
36
+ * Returns RemoteChatData (the shape the widget components expect),
37
+ * translated from the Ajaxter server response format.
31
38
  */
32
39
  export async function fetchRemoteChatData(
33
40
  apiKey: string,
34
41
  widgetId: string
35
42
  ): Promise<RemoteChatData> {
36
- const base = getChatDataBaseUrl();
37
- const url = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
38
- url.searchParams.set('key', apiKey);
43
+ const base = getConfigUrl();
44
+ const url = new URL(
45
+ base,
46
+ typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
47
+ );
48
+ url.searchParams.set('key', apiKey);
39
49
  url.searchParams.set('widget', widgetId);
40
50
 
41
51
  const res = await fetch(url.toString(), {
42
- method: 'GET',
52
+ method: 'GET',
53
+ headers: { Accept: 'application/json' },
43
54
  credentials: 'omit',
44
55
  mode: 'cors',
45
- headers: { Accept: 'application/json' },
46
56
  });
47
- if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
48
- return res.json() as Promise<RemoteChatData>;
49
- }
50
57
 
51
- export function loadLocalConfig(): LocalEnvConfig {
58
+ if (!res.ok) throw new Error(`Chat config fetch failed: ${res.status}`);
59
+
60
+ const json = await res.json() as AjaxterConfigResponse;
61
+
62
+ if (!json.ok || !json.data) {
63
+ throw new Error('Invalid config response from server');
64
+ }
65
+
66
+ const { data } = json;
67
+ const internal = data._internal;
68
+ const ws = data.widget;
69
+
70
+ // Map Ajaxter server response → WidgetConfig used by components
71
+ const widgetConfig: WidgetConfig = {
72
+ id: internal.id,
73
+ apiKey: internal.apiKey,
74
+ status: internal.status,
75
+ chatType: internal.chatType,
76
+ displayMode: internal.displayMode,
77
+ primaryColor: internal.primaryColor,
78
+ buttonLabel: internal.buttonLabel,
79
+
80
+ // Button position from visibility config
81
+ buttonPosition: (ws.visibility?.mobile?.position === 'bottom-left'
82
+ ? 'bottom-left'
83
+ : 'bottom-right') as 'bottom-right' | 'bottom-left',
84
+
85
+ // Welcome text from states.online header
86
+ welcomeTitle: data.branding?.name ?? internal.brandName ?? 'Hello there 👋',
87
+ welcomeSubtitle: (ws.states as Record<string, { header?: Array<{content?: {value?: string}}> }> | undefined)
88
+ ?.online?.header?.[0]?.content?.value
89
+ ?? 'How can we help you today?',
90
+
91
+ brandName: internal.brandName ?? undefined,
92
+ supportPhone: internal.supportPhone ?? undefined,
93
+ privacyPolicyUrl: internal.privacyPolicyUrl ?? undefined,
94
+ showPrivacyNotice: internal.showPrivacyNotice,
95
+
96
+ allowVoiceMessage: internal.allowVoiceMessage,
97
+ allowAttachment: internal.allowAttachment,
98
+ allowEmoji: internal.allowEmoji,
99
+ allowWebCall: internal.allowWebCall,
100
+ maxEmojiCount: internal.maxEmojiCount,
101
+ allowTranscriptDownload: internal.allowTranscript,
102
+ allowReport: internal.allowReport,
103
+ allowBlock: internal.allowBlock,
104
+
105
+ // Full Ajaxter settings stored for display-mode rendering
106
+ ajaxterSettings: ws,
107
+ };
108
+
52
109
  return {
53
- apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
54
- widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
110
+ widget: widgetConfig,
111
+ developers: data.developers ?? [],
112
+ users: data.users ?? [],
113
+ sampleChats: data.sampleChats ?? {},
114
+ sampleTickets: data.sampleTickets ?? [],
115
+ blockedUsers: data.blockedUsers ?? [],
55
116
  };
56
117
  }
@@ -8,17 +8,22 @@ export function useRemoteConfig(apiKey: string, widgetId: string) {
8
8
  const [error, setError] = useState<string | null>(null);
9
9
 
10
10
  useEffect(() => {
11
+ if (!apiKey || !widgetId) return;
11
12
  let cancelled = false;
12
13
  setLoading(true);
14
+ setError(null);
15
+
13
16
  fetchRemoteChatData(apiKey, widgetId)
14
- .then(d => { if (!cancelled) { setData(d); setError(null); setLoading(false); } })
17
+ .then(d => {
18
+ if (!cancelled) { setData(d); setLoading(false); }
19
+ })
15
20
  .catch(e => {
16
21
  if (!cancelled) {
17
- const msg = e instanceof Error ? e.message : String(e);
18
- setError(msg || 'Network error while loading configuration');
22
+ setError(e instanceof Error ? e.message : 'Failed to load config');
19
23
  setLoading(false);
20
24
  }
21
25
  });
26
+
22
27
  return () => { cancelled = true; };
23
28
  }, [apiKey, widgetId]);
24
29
 
@@ -1,5 +1,15 @@
1
1
  'use client';
2
2
 
3
+ /**
4
+ * useSocket — wraps socket.io-client for the ajaxter-chat npm package.
5
+ *
6
+ * The npm package fetches widget config ONCE via HTTP (GET /api/chat-config),
7
+ * then all real-time events (messages, calls, typing, presence…) flow through
8
+ * this Socket.IO connection.
9
+ *
10
+ * Install peer dep in consumer app: npm install socket.io-client
11
+ */
12
+
3
13
  import { useEffect, useRef, useCallback, useState } from 'react';
4
14
  import { ChatMessage } from '../types';
5
15
 
@@ -11,218 +21,229 @@ export interface SocketMessageAck {
11
21
  }
12
22
 
13
23
  export interface UseSocketOptions {
14
- widgetId: string;
15
- viewerUid: string;
16
- /** WebSocket server URL — defaults to http://localhost:3005 */
24
+ widgetId: string;
25
+ viewerUid: string;
26
+ /** Socket.IO server URL — defaults to http://localhost:3005 */
17
27
  serverUrl?: string;
18
- onMessage?: (msg: ChatMessage) => void;
19
- onMessageAck?: (ack: SocketMessageAck) => void;
20
- onChatPaused?: (roomId: string, paused: boolean) => void;
21
- onCallOffer?: (offer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
22
- onCallAnswer?: (answer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
23
- onIceCandidate?: (candidate: RTCIceCandidateInit, fromUid: string) => void;
24
- onCallEnd?: (callId: string) => void;
25
- onError?: (code: string, message: string) => void;
26
- onUserStatus?: (uid: string, status: 'online' | 'away' | 'offline') => void;
28
+ onMessage?: (msg: ChatMessage) => void;
29
+ onMessageAck?: (ack: SocketMessageAck) => void;
30
+ onChatPaused?: (roomId: string, paused: boolean) => void;
31
+ onCallOffer?: (offer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
32
+ onCallAnswer?: (answer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
33
+ onIceCandidate?:(candidate: RTCIceCandidateInit, fromUid: string) => void;
34
+ onCallEnd?: (callId: string) => void;
35
+ onTypingStart?: (roomId: string, userId: string) => void;
36
+ onTypingStop?: (roomId: string, userId: string) => void;
37
+ onUserStatus?: (uid: string, status: 'online' | 'away' | 'offline') => void;
38
+ onTicketCreated?: (ticket: unknown) => void;
39
+ onTicketUpdated?: (ticket: unknown) => void;
40
+ onTransferAssigned?: (payload: unknown) => void;
41
+ onError?: (code: string, message: string) => void;
42
+ }
43
+
44
+ // Lazy-load socket.io-client so the package stays light when not connected
45
+ type SocketIO = {
46
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
47
+ off: (event: string, cb?: (...args: unknown[]) => void) => void;
48
+ emit: (event: string, data?: unknown) => void;
49
+ disconnect: () => void;
50
+ connected: boolean;
51
+ };
52
+
53
+ async function createSocket(url: string, query: Record<string, string>): Promise<SocketIO> {
54
+ // Dynamic import so bundle is not broken when socket.io-client is absent
55
+ // Consumer app must have socket.io-client installed
56
+ let io: (url: string, opts: unknown) => SocketIO;
57
+ try {
58
+ const mod = await import('socket.io-client' as string);
59
+ io = (mod.default || mod) as (url: string, opts: unknown) => SocketIO;
60
+ } catch {
61
+ throw new Error(
62
+ '[ajaxter-chat] socket.io-client not found. ' +
63
+ 'Run: npm install socket.io-client'
64
+ );
65
+ }
66
+ return io(url, {
67
+ query,
68
+ transports: ['websocket', 'polling'],
69
+ reconnection: true,
70
+ reconnectionDelay: 2000,
71
+ reconnectionAttempts: 10,
72
+ autoConnect: true,
73
+ });
27
74
  }
28
75
 
29
- const SOCKET_URL = 'http://localhost:3005';
30
- const RECONNECT_DELAY = 3000;
31
- const MAX_RECONNECT = 5;
76
+ const DEFAULT_SOCKET_URL = 'http://localhost:3005';
32
77
 
33
78
  export function useSocket(opts: UseSocketOptions) {
34
79
  const {
35
- widgetId, viewerUid, serverUrl = SOCKET_URL,
80
+ widgetId, viewerUid,
81
+ serverUrl = DEFAULT_SOCKET_URL,
36
82
  onMessage, onMessageAck, onChatPaused,
37
83
  onCallOffer, onCallAnswer, onIceCandidate, onCallEnd,
38
- onError, onUserStatus,
84
+ onTypingStart, onTypingStop, onUserStatus,
85
+ onTicketCreated, onTicketUpdated, onTransferAssigned, onError,
39
86
  } = opts;
40
87
 
41
88
  const [status, setStatus] = useState<SocketStatus>('disconnected');
42
- const wsRef = useRef<WebSocket | null>(null);
43
- const reconnectCount = useRef(0);
44
- const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
45
- const currentRoomRef = useRef<string | null>(null);
46
- const mountedRef = useRef(true);
47
-
48
- const send = useCallback((event: string, payload: unknown) => {
49
- if (wsRef.current?.readyState === WebSocket.OPEN) {
50
- wsRef.current.send(JSON.stringify({ event, payload }));
89
+ const socketRef = useRef<SocketIO | null>(null);
90
+ const mountedRef = useRef(true);
91
+ const currentRoom = useRef<string | null>(null);
92
+
93
+ // Stable emit helper
94
+ const emit = useCallback((event: string, data?: unknown) => {
95
+ if (socketRef.current?.connected) {
96
+ socketRef.current.emit(event, data);
51
97
  }
52
98
  }, []);
53
99
 
54
- const connect = useCallback(() => {
55
- if (!mountedRef.current) return;
56
- if (wsRef.current) {
57
- wsRef.current.onclose = null;
58
- wsRef.current.close();
59
- }
100
+ useEffect(() => {
101
+ if (!widgetId || !viewerUid) return;
102
+ mountedRef.current = true;
60
103
  setStatus('connecting');
61
104
 
62
- const url = `${serverUrl}?widgetId=${encodeURIComponent(widgetId)}&uid=${encodeURIComponent(viewerUid)}`;
63
- let ws: WebSocket;
64
- try {
65
- ws = new WebSocket(url);
66
- } catch (e) {
67
- setStatus('error');
68
- return;
69
- }
70
- wsRef.current = ws;
71
-
72
- ws.onopen = () => {
73
- if (!mountedRef.current) { ws.close(); return; }
74
- reconnectCount.current = 0;
75
- setStatus('connected');
76
- // Re-join current room if we were in one
77
- if (currentRoomRef.current) {
78
- send('join', { roomId: currentRoomRef.current, widgetId });
79
- }
80
- };
105
+ createSocket(serverUrl, { widgetId, uid: viewerUid }).then(socket => {
106
+ if (!mountedRef.current) { socket.disconnect(); return; }
107
+ socketRef.current = socket;
81
108
 
82
- ws.onmessage = (event) => {
83
- let parsed: { event: string; payload: unknown };
84
- try { parsed = JSON.parse(event.data as string); }
85
- catch { return; }
86
- const { event: ev, payload } = parsed;
87
- switch (ev) {
88
- case 'message':
89
- onMessage?.(payload as ChatMessage);
90
- break;
91
- case 'message:ack':
92
- onMessageAck?.(payload as SocketMessageAck);
93
- break;
94
- case 'chat:paused': {
95
- const p = payload as { roomId: string; paused: boolean };
96
- onChatPaused?.(p.roomId, p.paused);
97
- break;
98
- }
99
- case 'call-offer': {
100
- const p = payload as { offer: RTCSessionDescriptionInit; from: string; callId: string };
101
- onCallOffer?.(p.offer, p.from, p.callId);
102
- break;
103
- }
104
- case 'call-answer': {
105
- const p = payload as { answer: RTCSessionDescriptionInit; from: string; callId: string };
106
- onCallAnswer?.(p.answer, p.from, p.callId);
107
- break;
108
- }
109
- case 'ice-candidate': {
110
- const p = payload as { candidate: RTCIceCandidateInit; from: string };
111
- onIceCandidate?.(p.candidate, p.from);
112
- break;
113
- }
114
- case 'call-end': {
115
- const p = payload as { callId: string };
116
- onCallEnd?.(p.callId);
117
- break;
109
+ socket.on('connect', () => {
110
+ if (!mountedRef.current) return;
111
+ setStatus('connected');
112
+ // Rejoin current room after reconnect
113
+ if (currentRoom.current) {
114
+ socket.emit('join', { roomId: currentRoom.current, widgetId });
118
115
  }
119
- case 'user:status': {
120
- const p = payload as { uid: string; status: 'online' | 'away' | 'offline' };
121
- onUserStatus?.(p.uid, p.status);
122
- break;
123
- }
124
- case 'error': {
125
- const p = payload as { code: string; message: string };
126
- onError?.(p.code, p.message);
127
- break;
128
- }
129
- }
130
- };
131
-
132
- ws.onerror = () => {
133
- if (!mountedRef.current) return;
134
- setStatus('error');
135
- };
136
-
137
- ws.onclose = () => {
138
- if (!mountedRef.current) return;
139
- setStatus('disconnected');
140
- if (reconnectCount.current < MAX_RECONNECT) {
141
- reconnectCount.current++;
142
- reconnectTimer.current = setTimeout(connect, RECONNECT_DELAY * reconnectCount.current);
143
- }
144
- };
145
- }, [widgetId, viewerUid, serverUrl, send, onMessage, onMessageAck, onChatPaused, onCallOffer, onCallAnswer, onIceCandidate, onCallEnd, onError, onUserStatus]);
116
+ });
117
+
118
+ socket.on('disconnect', () => {
119
+ if (mountedRef.current) setStatus('disconnected');
120
+ });
121
+
122
+ socket.on('connect_error', () => {
123
+ if (mountedRef.current) setStatus('error');
124
+ });
125
+
126
+ socket.on('message', (msg: unknown) => onMessage?.(msg as ChatMessage));
127
+ socket.on('message:ack', (ack: unknown) => onMessageAck?.(ack as SocketMessageAck));
128
+
129
+ socket.on('chat:paused', (p: unknown) => {
130
+ const { roomId, paused } = p as { roomId: string; paused: boolean };
131
+ onChatPaused?.(roomId, paused);
132
+ });
133
+
134
+ socket.on('call-offer', (p: unknown) => {
135
+ const { offer, from, callId } = p as { offer: RTCSessionDescriptionInit; from: string; callId: string };
136
+ onCallOffer?.(offer, from, callId);
137
+ });
138
+ socket.on('call-answer', (p: unknown) => {
139
+ const { answer, from, callId } = p as { answer: RTCSessionDescriptionInit; from: string; callId: string };
140
+ onCallAnswer?.(answer, from, callId);
141
+ });
142
+ socket.on('ice-candidate', (p: unknown) => {
143
+ const { candidate, from } = p as { candidate: RTCIceCandidateInit; from: string };
144
+ onIceCandidate?.(candidate, from);
145
+ });
146
+ socket.on('call-end', (p: unknown) => {
147
+ const { callId } = p as { callId: string };
148
+ onCallEnd?.(callId);
149
+ });
150
+
151
+ socket.on('typing_start', (p: unknown) => {
152
+ const { roomId, userId } = p as { roomId: string; userId: string };
153
+ onTypingStart?.(roomId, userId);
154
+ });
155
+ socket.on('typing_stop', (p: unknown) => {
156
+ const { roomId, userId } = p as { roomId: string; userId: string };
157
+ onTypingStop?.(roomId, userId);
158
+ });
159
+
160
+ socket.on('user:status', (p: unknown) => {
161
+ const { uid, status: s } = p as { uid: string; status: 'online'|'away'|'offline' };
162
+ onUserStatus?.(uid, s);
163
+ });
164
+
165
+ socket.on('ticket:created', onTicketCreated as (...args: unknown[]) => void);
166
+ socket.on('ticket:updated', onTicketUpdated as (...args: unknown[]) => void);
167
+ socket.on('transfer:assigned', onTransferAssigned as (...args: unknown[]) => void);
168
+
169
+ socket.on('error', (p: unknown) => {
170
+ const { code, message } = p as { code: string; message: string };
171
+ onError?.(code, message);
172
+ });
173
+ }).catch(err => {
174
+ console.error('[ajaxter-chat] Socket init error:', err.message);
175
+ if (mountedRef.current) setStatus('error');
176
+ });
146
177
 
147
- useEffect(() => {
148
- mountedRef.current = true;
149
- if (widgetId && viewerUid) connect();
150
178
  return () => {
151
179
  mountedRef.current = false;
152
- if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
153
- wsRef.current?.close();
180
+ socketRef.current?.disconnect();
181
+ socketRef.current = null;
154
182
  };
155
183
  // eslint-disable-next-line react-hooks/exhaustive-deps
156
184
  }, [widgetId, viewerUid, serverUrl]);
157
185
 
186
+ // ── Public API ────────────────────────────────────────────────────────────
187
+
158
188
  const joinRoom = useCallback((roomId: string) => {
159
- currentRoomRef.current = roomId;
160
- send('join', { roomId, widgetId });
161
- }, [send, widgetId]);
189
+ currentRoom.current = roomId;
190
+ emit('join', { roomId, widgetId });
191
+ }, [emit, widgetId]);
162
192
 
163
193
  const leaveRoom = useCallback((roomId: string) => {
164
- currentRoomRef.current = null;
165
- send('leave', { roomId });
166
- }, [send]);
194
+ if (currentRoom.current === roomId) currentRoom.current = null;
195
+ emit('leave', { roomId });
196
+ }, [emit]);
167
197
 
168
- const emitMessage = useCallback((msg: ChatMessage) => {
169
- send('message', msg);
170
- }, [send]);
198
+ const emitMessage = useCallback((msg: ChatMessage) => emit('message', msg), [emit]);
199
+ const emitTypingStart = useCallback((roomId: string) => emit('typing_start', { roomId }), [emit]);
200
+ const emitTypingStop = useCallback((roomId: string) => emit('typing_stop', { roomId }), [emit]);
201
+ const emitMessageRead = useCallback((roomId: string, messageId: string) => emit('message:read', { roomId, messageId }), [emit]);
171
202
 
172
- const emitTypingStart = useCallback((roomId: string) => {
173
- send('typing_start', { roomId, userId: viewerUid });
174
- }, [send, viewerUid]);
203
+ const emitPauseToggle = useCallback((roomId: string, targetUserId: string, paused: boolean) =>
204
+ emit('chat:pause', { roomId, targetUserId, paused }), [emit]);
175
205
 
176
- const emitTypingStop = useCallback((roomId: string) => {
177
- send('typing_stop', { roomId, userId: viewerUid });
178
- }, [send, viewerUid]);
206
+ const emitReport = useCallback((roomId: string, reason?: string) =>
207
+ emit('chat:report', { roomId, reason }), [emit]);
179
208
 
180
- const emitPauseToggle = useCallback((roomId: string, targetUserId: string, paused: boolean) => {
181
- send('chat:pause', { roomId, targetUserId, paused });
182
- }, [send]);
209
+ const emitBlock = useCallback((blockedUid: string) => emit('user:block', { blockedUid }), [emit]);
210
+ const emitUnblock = useCallback((blockedUid: string) => emit('user:unblock', { blockedUid }), [emit]);
183
211
 
184
- const emitReport = useCallback((roomId: string, reason?: string) => {
185
- send('chat:report', { roomId, reporterId: viewerUid, reason });
186
- }, [send, viewerUid]);
212
+ const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) =>
213
+ emit('transfer', { fromRoomId, toUserId, note }), [emit]);
187
214
 
188
- const emitBlock = useCallback((blockedUid: string) => {
189
- send('user:block', { blockedUid });
190
- }, [send]);
215
+ const emitAddParticipant = useCallback((roomId: string, uid: string) =>
216
+ emit('room:add-participant', { roomId, uid }), [emit]);
191
217
 
192
- const emitUnblock = useCallback((blockedUid: string) => {
193
- send('user:unblock', { blockedUid });
194
- }, [send]);
218
+ const emitCallOffer = useCallback((offer: RTCSessionDescriptionInit, toUid: string, callId: string, hasVideo?: boolean) =>
219
+ emit('call-offer', { offer, to: toUid, callId, hasVideo }), [emit]);
195
220
 
196
- const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) => {
197
- send('transfer', { fromRoomId, toUserId, note });
198
- }, [send]);
221
+ const emitCallAnswer = useCallback((answer: RTCSessionDescriptionInit, toUid: string, callId: string) =>
222
+ emit('call-answer', { answer, to: toUid, callId }), [emit]);
199
223
 
200
- const emitCallOffer = useCallback((offer: RTCSessionDescriptionInit, toUid: string, callId: string) => {
201
- send('call-offer', { offer, to: toUid, from: viewerUid, callId });
202
- }, [send, viewerUid]);
224
+ const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) =>
225
+ emit('ice-candidate', { candidate, to: toUid }), [emit]);
203
226
 
204
- const emitCallAnswer = useCallback((answer: RTCSessionDescriptionInit, toUid: string, callId: string) => {
205
- send('call-answer', { answer, to: toUid, callId });
206
- }, [send]);
227
+ const emitCallEnd = useCallback((callId: string, toUid?: string) =>
228
+ emit('call-end', { callId, to: toUid }), [emit]);
207
229
 
208
- const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) => {
209
- send('ice-candidate', { candidate, to: toUid });
210
- }, [send]);
230
+ const emitPresence = useCallback((presenceStatus: 'ACTIVE'|'AWAY'|'DND') =>
231
+ emit('presence:update', { status: presenceStatus }), [emit]);
211
232
 
212
- const emitCallEnd = useCallback((callId: string, toUid?: string) => {
213
- send('call-end', { callId, to: toUid });
214
- }, [send]);
233
+ const emitTicketCreate = useCallback((payload: { title: string; description: string; priority: string; createdBy?: string }) =>
234
+ emit('ticket:create', payload), [emit]);
215
235
 
216
- const emitAddParticipant = useCallback((roomId: string, uid: string) => {
217
- send('room:add-participant', { roomId, uid });
218
- }, [send]);
236
+ const emitTicketUpdate = useCallback((payload: { ticketId: string; status?: string; priority?: string; assignedTo?: string }) =>
237
+ emit('ticket:update', payload), [emit]);
219
238
 
220
239
  return {
221
240
  status,
222
241
  joinRoom, leaveRoom,
223
- emitMessage, emitTypingStart, emitTypingStop,
242
+ emitMessage, emitTypingStart, emitTypingStop, emitMessageRead,
224
243
  emitPauseToggle, emitReport, emitBlock, emitUnblock, emitTransfer,
225
- emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
226
244
  emitAddParticipant,
245
+ emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
246
+ emitPresence,
247
+ emitTicketCreate, emitTicketUpdate,
227
248
  };
228
249
  }
package/src/index.ts CHANGED
@@ -27,13 +27,19 @@ export type { PresenceSyncPayload } from './utils/presenceStatus';
27
27
  export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
28
28
 
29
29
  export type {
30
+ // Core widget
30
31
  ChatWidgetProps, ChatWidgetTheme, ChatWidgetViewer,
32
+ // Config shapes
31
33
  WidgetConfig, RemoteChatData,
34
+ AjaxterConfigResponse, AjaxterInternalConfig, AjaxterWidgetSettings,
35
+ AjaxterWidgetTheme, AjaxterWidgetFeatures, AjaxterMinimized,
36
+ // Data models
32
37
  ChatUser, ChatMessage, Ticket, RecentChat,
33
38
  CallSession, CallState,
34
- ChatStatus, ChatType, UserType, OnlineStatus,
35
- Screen, BottomTab, UserListContext, MessageType,
36
- LocalEnvConfig, PresenceStatus,
39
+ // Enums / literals
40
+ ChatStatus, ChatType, DisplayMode, UserType, OnlineStatus,
41
+ Screen, BottomTab, UserListContext, MessageType, PresenceStatus,
42
+ LocalEnvConfig,
37
43
  } from './types';
38
44
 
39
45
  export type { UseSocketOptions, SocketStatus, SocketMessageAck } from './hooks/useSocket';