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.
- package/dist/components/ChatWidget.d.ts.map +1 -1
- package/dist/components/ChatWidget.js +302 -265
- package/dist/components/ChatWidget.js.map +1 -1
- package/dist/config/index.d.ts +6 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +68 -22
- package/dist/config/index.js.map +1 -1
- package/dist/hooks/useRemoteConfig.d.ts.map +1 -1
- package/dist/hooks/useRemoteConfig.js +10 -7
- package/dist/hooks/useRemoteConfig.js.map +1 -1
- package/dist/hooks/useSocket.d.ts +23 -4
- package/dist/hooks/useSocket.d.ts.map +1 -1
- package/dist/hooks/useSocket.js +170 -157
- package/dist/hooks/useSocket.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +128 -48
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +8 -4
- package/src/components/ChatWidget.tsx +513 -514
- package/src/config/index.ts +87 -26
- package/src/hooks/useRemoteConfig.ts +8 -3
- package/src/hooks/useSocket.ts +187 -166
- package/src/index.ts +9 -3
- package/src/types/index.ts +177 -143
package/src/config/index.ts
CHANGED
|
@@ -1,56 +1,117 @@
|
|
|
1
|
-
import { LocalEnvConfig, RemoteChatData } from '../types';
|
|
1
|
+
import { LocalEnvConfig, RemoteChatData, AjaxterConfigResponse, WidgetConfig } from '../types';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
25
|
-
return
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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 =
|
|
37
|
-
const url
|
|
38
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
17
|
+
.then(d => {
|
|
18
|
+
if (!cancelled) { setData(d); setLoading(false); }
|
|
19
|
+
})
|
|
15
20
|
.catch(e => {
|
|
16
21
|
if (!cancelled) {
|
|
17
|
-
|
|
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
|
|
package/src/hooks/useSocket.ts
CHANGED
|
@@ -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:
|
|
15
|
-
viewerUid:
|
|
16
|
-
/**
|
|
24
|
+
widgetId: string;
|
|
25
|
+
viewerUid: string;
|
|
26
|
+
/** Socket.IO server URL — defaults to http://localhost:3005 */
|
|
17
27
|
serverUrl?: string;
|
|
18
|
-
onMessage?:
|
|
19
|
-
onMessageAck?:
|
|
20
|
-
onChatPaused?:
|
|
21
|
-
onCallOffer?:
|
|
22
|
-
onCallAnswer?:
|
|
23
|
-
onIceCandidate?:
|
|
24
|
-
onCallEnd?:
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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,
|
|
80
|
+
widgetId, viewerUid,
|
|
81
|
+
serverUrl = DEFAULT_SOCKET_URL,
|
|
36
82
|
onMessage, onMessageAck, onChatPaused,
|
|
37
83
|
onCallOffer, onCallAnswer, onIceCandidate, onCallEnd,
|
|
38
|
-
|
|
84
|
+
onTypingStart, onTypingStop, onUserStatus,
|
|
85
|
+
onTicketCreated, onTicketUpdated, onTransferAssigned, onError,
|
|
39
86
|
} = opts;
|
|
40
87
|
|
|
41
88
|
const [status, setStatus] = useState<SocketStatus>('disconnected');
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
if (!
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
}, [
|
|
189
|
+
currentRoom.current = roomId;
|
|
190
|
+
emit('join', { roomId, widgetId });
|
|
191
|
+
}, [emit, widgetId]);
|
|
162
192
|
|
|
163
193
|
const leaveRoom = useCallback((roomId: string) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}, [
|
|
194
|
+
if (currentRoom.current === roomId) currentRoom.current = null;
|
|
195
|
+
emit('leave', { roomId });
|
|
196
|
+
}, [emit]);
|
|
167
197
|
|
|
168
|
-
const emitMessage
|
|
169
|
-
|
|
170
|
-
}, [
|
|
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
|
|
173
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
}, [send, viewerUid]);
|
|
206
|
+
const emitReport = useCallback((roomId: string, reason?: string) =>
|
|
207
|
+
emit('chat:report', { roomId, reason }), [emit]);
|
|
179
208
|
|
|
180
|
-
const
|
|
181
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
}, [send, viewerUid]);
|
|
212
|
+
const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) =>
|
|
213
|
+
emit('transfer', { fromRoomId, toUserId, note }), [emit]);
|
|
187
214
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
}, [send]);
|
|
215
|
+
const emitAddParticipant = useCallback((roomId: string, uid: string) =>
|
|
216
|
+
emit('room:add-participant', { roomId, uid }), [emit]);
|
|
191
217
|
|
|
192
|
-
const
|
|
193
|
-
|
|
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
|
|
197
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
}, [send, viewerUid]);
|
|
224
|
+
const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) =>
|
|
225
|
+
emit('ice-candidate', { candidate, to: toUid }), [emit]);
|
|
203
226
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
}, [send]);
|
|
227
|
+
const emitCallEnd = useCallback((callId: string, toUid?: string) =>
|
|
228
|
+
emit('call-end', { callId, to: toUid }), [emit]);
|
|
207
229
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
}, [send]);
|
|
230
|
+
const emitPresence = useCallback((presenceStatus: 'ACTIVE'|'AWAY'|'DND') =>
|
|
231
|
+
emit('presence:update', { status: presenceStatus }), [emit]);
|
|
211
232
|
|
|
212
|
-
const
|
|
213
|
-
|
|
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
|
|
217
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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';
|