ajaxter-chat 3.0.17 → 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/BlockList/index.d.ts +1 -0
- package/dist/components/BlockList/index.d.ts.map +1 -0
- package/dist/components/BlockList/index.js +55 -28
- package/dist/components/BlockList/index.js.map +1 -0
- package/dist/components/CallScreen/index.d.ts +1 -0
- package/dist/components/CallScreen/index.d.ts.map +1 -0
- package/dist/components/CallScreen/index.js +107 -39
- package/dist/components/CallScreen/index.js.map +1 -0
- package/dist/components/ChatScreen/index.d.ts +1 -0
- package/dist/components/ChatScreen/index.d.ts.map +1 -0
- package/dist/components/ChatScreen/index.js +493 -294
- package/dist/components/ChatScreen/index.js.map +1 -0
- package/dist/components/ChatWidget.d.ts +1 -0
- package/dist/components/ChatWidget.d.ts.map +1 -0
- package/dist/components/ChatWidget.js +499 -367
- package/dist/components/ChatWidget.js.map +1 -0
- package/dist/components/EmojiPicker/index.d.ts +1 -0
- package/dist/components/EmojiPicker/index.d.ts.map +1 -0
- package/dist/components/EmojiPicker/index.js +19 -7
- package/dist/components/EmojiPicker/index.js.map +1 -0
- package/dist/components/ErrorBoundary/index.d.ts +20 -0
- package/dist/components/ErrorBoundary/index.d.ts.map +1 -0
- package/dist/components/ErrorBoundary/index.js +76 -0
- package/dist/components/ErrorBoundary/index.js.map +1 -0
- package/dist/components/HomeScreen/index.d.ts +1 -0
- package/dist/components/HomeScreen/index.d.ts.map +1 -0
- package/dist/components/HomeScreen/index.js +236 -158
- package/dist/components/HomeScreen/index.js.map +1 -0
- package/dist/components/MaintenanceView/index.d.ts +1 -0
- package/dist/components/MaintenanceView/index.d.ts.map +1 -0
- package/dist/components/MaintenanceView/index.js +28 -12
- package/dist/components/MaintenanceView/index.js.map +1 -0
- package/dist/components/MiniCallBar/index.d.ts +1 -0
- package/dist/components/MiniCallBar/index.d.ts.map +1 -0
- package/dist/components/MiniCallBar/index.js +85 -37
- package/dist/components/MiniCallBar/index.js.map +1 -0
- package/dist/components/PermissionsGateScreen/index.d.ts +1 -0
- package/dist/components/PermissionsGateScreen/index.d.ts.map +1 -0
- package/dist/components/PermissionsGateScreen/index.js +82 -28
- package/dist/components/PermissionsGateScreen/index.js.map +1 -0
- package/dist/components/RecentChatsScreen/index.d.ts +1 -0
- package/dist/components/RecentChatsScreen/index.d.ts.map +1 -0
- package/dist/components/RecentChatsScreen/index.js +79 -19
- package/dist/components/RecentChatsScreen/index.js.map +1 -0
- package/dist/components/SlideNavMenu.d.ts +1 -0
- package/dist/components/SlideNavMenu.d.ts.map +1 -0
- package/dist/components/SlideNavMenu.js +82 -63
- package/dist/components/SlideNavMenu.js.map +1 -0
- package/dist/components/Tabs/BottomTabs.d.ts +1 -0
- package/dist/components/Tabs/BottomTabs.d.ts.map +1 -0
- package/dist/components/Tabs/BottomTabs.js +34 -19
- package/dist/components/Tabs/BottomTabs.js.map +1 -0
- package/dist/components/TicketDetailScreen/index.d.ts +1 -0
- package/dist/components/TicketDetailScreen/index.d.ts.map +1 -0
- package/dist/components/TicketDetailScreen/index.js +66 -27
- package/dist/components/TicketDetailScreen/index.js.map +1 -0
- package/dist/components/TicketFormScreen/index.d.ts +1 -0
- package/dist/components/TicketFormScreen/index.d.ts.map +1 -0
- package/dist/components/TicketFormScreen/index.js +99 -49
- package/dist/components/TicketFormScreen/index.js.map +1 -0
- package/dist/components/TicketScreen/index.d.ts +1 -0
- package/dist/components/TicketScreen/index.d.ts.map +1 -0
- package/dist/components/TicketScreen/index.js +95 -26
- package/dist/components/TicketScreen/index.js.map +1 -0
- package/dist/components/UserListScreen/index.d.ts +1 -0
- package/dist/components/UserListScreen/index.d.ts.map +1 -0
- package/dist/components/UserListScreen/index.js +127 -53
- package/dist/components/UserListScreen/index.js.map +1 -0
- package/dist/components/ViewerBlockedScreen/index.d.ts +1 -0
- package/dist/components/ViewerBlockedScreen/index.d.ts.map +1 -0
- package/dist/components/ViewerBlockedScreen/index.js +113 -61
- package/dist/components/ViewerBlockedScreen/index.js.map +1 -0
- package/dist/config/index.d.ts +7 -3
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +73 -22
- package/dist/config/index.js.map +1 -0
- package/dist/hooks/useChat.d.ts +9 -1
- package/dist/hooks/useChat.d.ts.map +1 -0
- package/dist/hooks/useChat.js +60 -18
- package/dist/hooks/useChat.js.map +1 -0
- package/dist/hooks/useRemoteConfig.d.ts +1 -0
- package/dist/hooks/useRemoteConfig.d.ts.map +1 -0
- package/dist/hooks/useRemoteConfig.js +22 -15
- package/dist/hooks/useRemoteConfig.js.map +1 -0
- package/dist/hooks/useSocket.d.ts +59 -0
- package/dist/hooks/useSocket.d.ts.map +1 -0
- package/dist/hooks/useSocket.js +203 -0
- package/dist/hooks/useSocket.js.map +1 -0
- package/dist/hooks/useWebRTC.d.ts +10 -2
- package/dist/hooks/useWebRTC.d.ts.map +1 -0
- package/dist/hooks/useWebRTC.js +101 -69
- package/dist/hooks/useWebRTC.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -21
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +129 -48
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -1
- package/dist/types/index.js.map +1 -0
- package/dist/utils/chat.d.ts +1 -0
- package/dist/utils/chat.d.ts.map +1 -0
- package/dist/utils/chat.js +17 -7
- package/dist/utils/chat.js.map +1 -0
- package/dist/utils/fileName.d.ts +1 -0
- package/dist/utils/fileName.d.ts.map +1 -0
- package/dist/utils/fileName.js +5 -1
- package/dist/utils/fileName.js.map +1 -0
- package/dist/utils/messageSound.d.ts +1 -0
- package/dist/utils/messageSound.d.ts.map +1 -0
- package/dist/utils/messageSound.js +9 -3
- package/dist/utils/messageSound.js.map +1 -0
- package/dist/utils/presenceStatus.d.ts +1 -0
- package/dist/utils/presenceStatus.d.ts.map +1 -0
- package/dist/utils/presenceStatus.js +11 -4
- package/dist/utils/presenceStatus.js.map +1 -0
- package/dist/utils/privacyConsent.d.ts +1 -0
- package/dist/utils/privacyConsent.d.ts.map +1 -0
- package/dist/utils/privacyConsent.js +9 -3
- package/dist/utils/privacyConsent.js.map +1 -0
- package/dist/utils/reenableRequest.d.ts +1 -0
- package/dist/utils/reenableRequest.d.ts.map +1 -0
- package/dist/utils/reenableRequest.js +5 -1
- package/dist/utils/reenableRequest.js.map +1 -0
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.d.ts.map +1 -0
- package/dist/utils/theme.js +10 -4
- package/dist/utils/theme.js.map +1 -0
- package/dist/utils/widgetPermissions.d.ts +1 -0
- package/dist/utils/widgetPermissions.d.ts.map +1 -0
- package/dist/utils/widgetPermissions.js +13 -5
- package/dist/utils/widgetPermissions.js.map +1 -0
- package/dist/utils/widgetSession.d.ts +1 -0
- package/dist/utils/widgetSession.d.ts.map +1 -0
- package/dist/utils/widgetSession.js +9 -3
- package/dist/utils/widgetSession.js.map +1 -0
- package/package.json +8 -4
- package/src/components/ChatWidget.tsx +643 -622
- package/src/components/ErrorBoundary/index.tsx +62 -0
- package/src/config/index.ts +87 -26
- package/src/hooks/useChat.ts +59 -12
- package/src/hooks/useRemoteConfig.ts +8 -3
- package/src/hooks/useSocket.ts +249 -0
- package/src/hooks/useWebRTC.ts +99 -64
- package/src/index.ts +14 -3
- package/src/types/index.ts +177 -143
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
|
+
primaryColor?: string;
|
|
7
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
16
|
+
constructor(props: Props) {
|
|
17
|
+
super(props);
|
|
18
|
+
this.state = { hasError: false, error: null };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
26
|
+
console.error('[ChatWidget] Uncaught error:', error, info.componentStack);
|
|
27
|
+
this.props.onError?.(error, info);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
handleReset = () => {
|
|
31
|
+
this.setState({ hasError: false, error: null });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
if (this.props.fallback) return this.props.fallback;
|
|
37
|
+
const color = this.props.primaryColor ?? '#2563EB';
|
|
38
|
+
return (
|
|
39
|
+
<div style={{
|
|
40
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
41
|
+
justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12,
|
|
42
|
+
}}>
|
|
43
|
+
<div style={{ fontSize: 40 }}>⚠️</div>
|
|
44
|
+
<p style={{ fontWeight: 700, color: '#1a2332', margin: 0 }}>Something went wrong</p>
|
|
45
|
+
<p style={{ fontSize: 13, color: '#7b8fa1', lineHeight: 1.6, margin: 0 }}>
|
|
46
|
+
{this.state.error?.message ?? 'An unexpected error occurred'}
|
|
47
|
+
</p>
|
|
48
|
+
<button
|
|
49
|
+
onClick={this.handleReset}
|
|
50
|
+
style={{
|
|
51
|
+
padding: '9px 20px', borderRadius: 10, border: 'none',
|
|
52
|
+
background: color, color: '#fff', cursor: 'pointer', fontWeight: 700, fontSize: 14,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
Try Again
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return this.props.children;
|
|
61
|
+
}
|
|
62
|
+
}
|
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
|
}
|
package/src/hooks/useChat.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { ChatMessage, ChatUser } from '../types';
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface UseChatOptions {
|
|
5
|
+
onEmitMessage?: (msg: ChatMessage) => void;
|
|
6
|
+
onEmitPause?: (roomId: string, targetUid: string, paused: boolean) => void;
|
|
7
|
+
onEmitReport?: (roomId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useChat(initialMessages: ChatMessage[] = [], opts: UseChatOptions = {}) {
|
|
5
11
|
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
|
6
12
|
const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
|
|
7
13
|
const [isPaused, setIsPaused] = useState(false);
|
|
8
14
|
const [isReported, setIsReported] = useState(false);
|
|
15
|
+
const activeUserRef = useRef<ChatUser | null>(null);
|
|
16
|
+
const isPausedRef = useRef(false);
|
|
17
|
+
|
|
18
|
+
const { onEmitMessage, onEmitPause, onEmitReport } = opts;
|
|
9
19
|
|
|
10
20
|
const selectUser = useCallback((user: ChatUser, history: ChatMessage[] = []) => {
|
|
11
21
|
setActiveUser(user);
|
|
22
|
+
activeUserRef.current = user;
|
|
12
23
|
setMessages(history);
|
|
13
24
|
setIsPaused(false);
|
|
25
|
+
isPausedRef.current = false;
|
|
14
26
|
setIsReported(false);
|
|
15
|
-
// TODO: socket.emit('join', { roomId: user.uid });
|
|
16
|
-
// TODO: socket.on('message', msg => setMessages(prev => [...prev, msg]));
|
|
17
27
|
}, []);
|
|
18
28
|
|
|
19
29
|
const sendMessage = useCallback((
|
|
@@ -21,11 +31,12 @@ export function useChat(initialMessages: ChatMessage[] = []) {
|
|
|
21
31
|
type: ChatMessage['type'] = 'text',
|
|
22
32
|
extra: Partial<ChatMessage> = {}
|
|
23
33
|
) => {
|
|
24
|
-
|
|
34
|
+
const user = activeUserRef.current;
|
|
35
|
+
if (!user || isPausedRef.current) return;
|
|
25
36
|
const msg: ChatMessage = {
|
|
26
37
|
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
27
38
|
senderId: 'me',
|
|
28
|
-
receiverId:
|
|
39
|
+
receiverId: user.uid,
|
|
29
40
|
text,
|
|
30
41
|
timestamp: new Date().toISOString(),
|
|
31
42
|
type,
|
|
@@ -33,16 +44,52 @@ export function useChat(initialMessages: ChatMessage[] = []) {
|
|
|
33
44
|
...extra,
|
|
34
45
|
};
|
|
35
46
|
setMessages(prev => [...prev, msg]);
|
|
36
|
-
|
|
37
|
-
}, [
|
|
47
|
+
onEmitMessage?.(msg);
|
|
48
|
+
}, [onEmitMessage]);
|
|
49
|
+
|
|
50
|
+
const receiveMessage = useCallback((msg: ChatMessage) => {
|
|
51
|
+
setMessages(prev => {
|
|
52
|
+
if (prev.some(m => m.id === msg.id)) return prev;
|
|
53
|
+
return [...prev, msg];
|
|
54
|
+
});
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const updateMessageStatus = useCallback((messageId: string, status: ChatMessage['status']) => {
|
|
58
|
+
setMessages(prev => prev.map(m => m.id === messageId ? { ...m, status } : m));
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const togglePause = useCallback(() => {
|
|
62
|
+
const user = activeUserRef.current;
|
|
63
|
+
if (!user) return;
|
|
64
|
+
const next = !isPausedRef.current;
|
|
65
|
+
setIsPaused(next);
|
|
66
|
+
isPausedRef.current = next;
|
|
67
|
+
const roomId = [user.uid, 'me'].sort().join('_');
|
|
68
|
+
onEmitPause?.(roomId, user.uid, next);
|
|
69
|
+
}, [onEmitPause]);
|
|
70
|
+
|
|
71
|
+
const reportChat = useCallback(() => {
|
|
72
|
+
const user = activeUserRef.current;
|
|
73
|
+
setIsReported(true);
|
|
74
|
+
if (user) {
|
|
75
|
+
const roomId = [user.uid, 'me'].sort().join('_');
|
|
76
|
+
onEmitReport?.(roomId);
|
|
77
|
+
}
|
|
78
|
+
}, [onEmitReport]);
|
|
38
79
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
const clearChat = useCallback(() => {
|
|
81
|
+
setMessages([]);
|
|
82
|
+
setActiveUser(null);
|
|
83
|
+
activeUserRef.current = null;
|
|
84
|
+
setIsPaused(false);
|
|
85
|
+
isPausedRef.current = false;
|
|
86
|
+
setIsReported(false);
|
|
87
|
+
}, []);
|
|
42
88
|
|
|
43
89
|
return {
|
|
44
90
|
messages, activeUser, isPaused, isReported,
|
|
45
|
-
selectUser, sendMessage,
|
|
91
|
+
selectUser, sendMessage, receiveMessage, updateMessageStatus,
|
|
92
|
+
togglePause, reportChat, clearChat,
|
|
46
93
|
setMessages,
|
|
47
94
|
};
|
|
48
95
|
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use client';
|
|
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
|
+
|
|
13
|
+
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
14
|
+
import { ChatMessage } from '../types';
|
|
15
|
+
|
|
16
|
+
export type SocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
17
|
+
|
|
18
|
+
export interface SocketMessageAck {
|
|
19
|
+
messageId: string;
|
|
20
|
+
status: 'delivered' | 'read';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseSocketOptions {
|
|
24
|
+
widgetId: string;
|
|
25
|
+
viewerUid: string;
|
|
26
|
+
/** Socket.IO server URL — defaults to http://localhost:3005 */
|
|
27
|
+
serverUrl?: string;
|
|
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
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_SOCKET_URL = 'http://localhost:3005';
|
|
77
|
+
|
|
78
|
+
export function useSocket(opts: UseSocketOptions) {
|
|
79
|
+
const {
|
|
80
|
+
widgetId, viewerUid,
|
|
81
|
+
serverUrl = DEFAULT_SOCKET_URL,
|
|
82
|
+
onMessage, onMessageAck, onChatPaused,
|
|
83
|
+
onCallOffer, onCallAnswer, onIceCandidate, onCallEnd,
|
|
84
|
+
onTypingStart, onTypingStop, onUserStatus,
|
|
85
|
+
onTicketCreated, onTicketUpdated, onTransferAssigned, onError,
|
|
86
|
+
} = opts;
|
|
87
|
+
|
|
88
|
+
const [status, setStatus] = useState<SocketStatus>('disconnected');
|
|
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);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!widgetId || !viewerUid) return;
|
|
102
|
+
mountedRef.current = true;
|
|
103
|
+
setStatus('connecting');
|
|
104
|
+
|
|
105
|
+
createSocket(serverUrl, { widgetId, uid: viewerUid }).then(socket => {
|
|
106
|
+
if (!mountedRef.current) { socket.disconnect(); return; }
|
|
107
|
+
socketRef.current = socket;
|
|
108
|
+
|
|
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 });
|
|
115
|
+
}
|
|
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
|
+
});
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
mountedRef.current = false;
|
|
180
|
+
socketRef.current?.disconnect();
|
|
181
|
+
socketRef.current = null;
|
|
182
|
+
};
|
|
183
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
184
|
+
}, [widgetId, viewerUid, serverUrl]);
|
|
185
|
+
|
|
186
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
const joinRoom = useCallback((roomId: string) => {
|
|
189
|
+
currentRoom.current = roomId;
|
|
190
|
+
emit('join', { roomId, widgetId });
|
|
191
|
+
}, [emit, widgetId]);
|
|
192
|
+
|
|
193
|
+
const leaveRoom = useCallback((roomId: string) => {
|
|
194
|
+
if (currentRoom.current === roomId) currentRoom.current = null;
|
|
195
|
+
emit('leave', { roomId });
|
|
196
|
+
}, [emit]);
|
|
197
|
+
|
|
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]);
|
|
202
|
+
|
|
203
|
+
const emitPauseToggle = useCallback((roomId: string, targetUserId: string, paused: boolean) =>
|
|
204
|
+
emit('chat:pause', { roomId, targetUserId, paused }), [emit]);
|
|
205
|
+
|
|
206
|
+
const emitReport = useCallback((roomId: string, reason?: string) =>
|
|
207
|
+
emit('chat:report', { roomId, reason }), [emit]);
|
|
208
|
+
|
|
209
|
+
const emitBlock = useCallback((blockedUid: string) => emit('user:block', { blockedUid }), [emit]);
|
|
210
|
+
const emitUnblock = useCallback((blockedUid: string) => emit('user:unblock', { blockedUid }), [emit]);
|
|
211
|
+
|
|
212
|
+
const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) =>
|
|
213
|
+
emit('transfer', { fromRoomId, toUserId, note }), [emit]);
|
|
214
|
+
|
|
215
|
+
const emitAddParticipant = useCallback((roomId: string, uid: string) =>
|
|
216
|
+
emit('room:add-participant', { roomId, uid }), [emit]);
|
|
217
|
+
|
|
218
|
+
const emitCallOffer = useCallback((offer: RTCSessionDescriptionInit, toUid: string, callId: string, hasVideo?: boolean) =>
|
|
219
|
+
emit('call-offer', { offer, to: toUid, callId, hasVideo }), [emit]);
|
|
220
|
+
|
|
221
|
+
const emitCallAnswer = useCallback((answer: RTCSessionDescriptionInit, toUid: string, callId: string) =>
|
|
222
|
+
emit('call-answer', { answer, to: toUid, callId }), [emit]);
|
|
223
|
+
|
|
224
|
+
const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) =>
|
|
225
|
+
emit('ice-candidate', { candidate, to: toUid }), [emit]);
|
|
226
|
+
|
|
227
|
+
const emitCallEnd = useCallback((callId: string, toUid?: string) =>
|
|
228
|
+
emit('call-end', { callId, to: toUid }), [emit]);
|
|
229
|
+
|
|
230
|
+
const emitPresence = useCallback((presenceStatus: 'ACTIVE'|'AWAY'|'DND') =>
|
|
231
|
+
emit('presence:update', { status: presenceStatus }), [emit]);
|
|
232
|
+
|
|
233
|
+
const emitTicketCreate = useCallback((payload: { title: string; description: string; priority: string; createdBy?: string }) =>
|
|
234
|
+
emit('ticket:create', payload), [emit]);
|
|
235
|
+
|
|
236
|
+
const emitTicketUpdate = useCallback((payload: { ticketId: string; status?: string; priority?: string; assignedTo?: string }) =>
|
|
237
|
+
emit('ticket:update', payload), [emit]);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
status,
|
|
241
|
+
joinRoom, leaveRoom,
|
|
242
|
+
emitMessage, emitTypingStart, emitTypingStop, emitMessageRead,
|
|
243
|
+
emitPauseToggle, emitReport, emitBlock, emitUnblock, emitTransfer,
|
|
244
|
+
emitAddParticipant,
|
|
245
|
+
emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
|
|
246
|
+
emitPresence,
|
|
247
|
+
emitTicketCreate, emitTicketUpdate,
|
|
248
|
+
};
|
|
249
|
+
}
|