ajaxter-chat 3.0.17 → 3.1.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 +350 -255
- 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 +1 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -2
- 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 +12 -8
- package/dist/hooks/useRemoteConfig.js.map +1 -0
- package/dist/hooks/useSocket.d.ts +40 -0
- package/dist/hooks/useSocket.d.ts.map +1 -0
- package/dist/hooks/useSocket.js +190 -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 +6 -0
- 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 +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -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 +3 -3
- package/src/components/ChatWidget.tsx +291 -269
- package/src/components/ErrorBoundary/index.tsx +62 -0
- package/src/hooks/useChat.ts +59 -12
- package/src/hooks/useSocket.ts +228 -0
- package/src/hooks/useWebRTC.ts +99 -64
- package/src/index.ts +7 -2
|
@@ -1,80 +1,91 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ChatWidgetProps, BottomTab, Screen, UserListContext,
|
|
6
|
+
ChatUser, Ticket, RecentChat, ChatMessage,
|
|
7
|
+
} from '../types';
|
|
5
8
|
import { loadLocalConfig } from '../config';
|
|
6
9
|
import { mergeTheme } from '../utils/theme';
|
|
7
10
|
import { useRemoteConfig } from '../hooks/useRemoteConfig';
|
|
8
11
|
import { useChat } from '../hooks/useChat';
|
|
9
12
|
import { useWebRTC } from '../hooks/useWebRTC';
|
|
13
|
+
import { useSocket } from '../hooks/useSocket';
|
|
10
14
|
import { saveSession, loadSession } from '../utils/widgetSession';
|
|
11
15
|
import { playMessageSound, getMessageSoundEnabled, setMessageSoundEnabled } from '../utils/messageSound';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
16
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
17
|
+
|
|
18
|
+
import { HomeScreen } from './HomeScreen';
|
|
19
|
+
import { UserListScreen } from './UserListScreen';
|
|
20
|
+
import { ChatScreen } from './ChatScreen';
|
|
21
|
+
import { RecentChatsScreen } from './RecentChatsScreen';
|
|
22
|
+
import { TicketScreen } from './TicketScreen';
|
|
23
|
+
import { TicketDetailScreen } from './TicketDetailScreen';
|
|
24
|
+
import { TicketFormScreen } from './TicketFormScreen';
|
|
25
|
+
import { BlockListScreen } from './BlockList';
|
|
26
|
+
import { CallScreen } from './CallScreen';
|
|
27
|
+
import { MiniCallBar } from './MiniCallBar';
|
|
28
|
+
import { MaintenanceView } from './MaintenanceView';
|
|
29
|
+
import { BottomTabs } from './Tabs/BottomTabs';
|
|
30
|
+
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
26
31
|
import { PermissionsGateScreen } from './PermissionsGateScreen';
|
|
27
32
|
import { hasStoredPermissionsGrant } from '../utils/widgetPermissions';
|
|
28
33
|
|
|
34
|
+
/** WebSocket server URL — override via env NEXT_PUBLIC_SOCKET_URL / REACT_APP_SOCKET_URL */
|
|
35
|
+
function getSocketUrl(): string {
|
|
36
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
37
|
+
return (
|
|
38
|
+
process.env['NEXT_PUBLIC_SOCKET_URL'] ??
|
|
39
|
+
process.env['REACT_APP_SOCKET_URL'] ??
|
|
40
|
+
'http://localhost:3005'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return 'http://localhost:3005';
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewer }) => {
|
|
30
47
|
/* SSR guard */
|
|
31
48
|
const [mounted, setMounted] = useState(false);
|
|
32
49
|
useEffect(() => { setMounted(true); }, []);
|
|
33
50
|
|
|
34
|
-
/* Env config */
|
|
35
51
|
const { apiKey, widgetId } = loadLocalConfig();
|
|
36
|
-
|
|
37
|
-
/* Remote config */
|
|
38
52
|
const { data, loading: cfgLoading, error: cfgError } = useRemoteConfig(apiKey, widgetId);
|
|
39
53
|
|
|
40
|
-
/* Merged theme — remote config overrides defaults, local prop overrides both */
|
|
41
54
|
const theme = mergeTheme(
|
|
42
|
-
data?.widget ? {
|
|
43
|
-
|
|
55
|
+
data?.widget ? {
|
|
56
|
+
primaryColor: data.widget.primaryColor,
|
|
57
|
+
buttonLabel: data.widget.buttonLabel,
|
|
58
|
+
buttonPosition: data.widget.buttonPosition,
|
|
59
|
+
} : undefined,
|
|
60
|
+
localTheme,
|
|
44
61
|
);
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
const [
|
|
48
|
-
const [
|
|
49
|
-
|
|
50
|
-
const [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const [activeTab, setActiveTab] = useState<BottomTab>('home');
|
|
54
|
-
const [screen, setScreen] = useState<Screen>('home');
|
|
55
|
-
const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
|
|
56
|
-
const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
|
|
63
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
64
|
+
const [closing, setClosing] = useState(false);
|
|
65
|
+
const [callMinimized, setCallMinimized] = useState(false);
|
|
66
|
+
const [activeTab, setActiveTab] = useState<BottomTab>('home');
|
|
67
|
+
const [screen, setScreen] = useState<Screen>('home');
|
|
68
|
+
const [userListCtx, setUserListCtx] = useState<UserListContext>('support');
|
|
69
|
+
const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
|
|
57
70
|
const [viewingTicketId, setViewingTicketId] = useState<string | null>(null);
|
|
58
71
|
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
59
|
-
|
|
60
|
-
const [
|
|
61
|
-
|
|
62
|
-
const [permissionsOk, setPermissionsOk] = useState(false);
|
|
72
|
+
const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
|
|
73
|
+
const [permissionsOk, setPermissionsOk] = useState(false);
|
|
74
|
+
const [socketStatus, setSocketStatus] = useState<'connecting'|'connected'|'disconnected'|'error'>('disconnected');
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
const [
|
|
66
|
-
const [
|
|
67
|
-
const [blockedUids, setBlockedUids] = useState<string[]>(data?.blockedUsers ?? []);
|
|
76
|
+
const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
|
|
77
|
+
const [recentChats, setRecentChats] = useState<RecentChat[]>([]);
|
|
78
|
+
const [blockedUids, setBlockedUids] = useState<string[]>(data?.blockedUsers ?? []);
|
|
68
79
|
|
|
69
80
|
/* Sync remote data into local state once loaded */
|
|
70
81
|
useEffect(() => {
|
|
71
82
|
if (data) {
|
|
72
83
|
setTickets(data.sampleTickets);
|
|
73
84
|
setBlockedUids(data.blockedUsers);
|
|
74
|
-
const pid
|
|
85
|
+
const pid = viewer?.projectId?.trim();
|
|
75
86
|
const devs = data.developers ?? [];
|
|
76
|
-
const usr
|
|
77
|
-
const all
|
|
87
|
+
const usr = pid ? (data.users ?? []).filter(u => u.project === pid) : (data.users ?? []);
|
|
88
|
+
const all = [...devs, ...usr];
|
|
78
89
|
const recents: RecentChat[] = Object.entries(data.sampleChats).map(([uid, msgs]) => {
|
|
79
90
|
const user = all.find(u => u.uid === uid);
|
|
80
91
|
if (!user || msgs.length === 0) return null;
|
|
@@ -92,14 +103,92 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
92
103
|
}
|
|
93
104
|
}, [data, viewer?.projectId]);
|
|
94
105
|
|
|
95
|
-
/*
|
|
106
|
+
/* ── Socket setup ───────────────────────────────────────────────────── */
|
|
107
|
+
const viewerUidForSocket = (viewer?.uid ?? data?.widget?.viewerUid ?? '').trim();
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
joinRoom, leaveRoom,
|
|
111
|
+
emitMessage, emitPauseToggle, emitReport,
|
|
112
|
+
emitBlock, emitUnblock, emitTransfer,
|
|
113
|
+
emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
|
|
114
|
+
emitAddParticipant,
|
|
115
|
+
status: _socketStatus,
|
|
116
|
+
} = useSocket({
|
|
117
|
+
widgetId,
|
|
118
|
+
viewerUid: viewerUidForSocket,
|
|
119
|
+
serverUrl: getSocketUrl(),
|
|
120
|
+
onMessage: (msg) => {
|
|
121
|
+
receiveMessage(msg);
|
|
122
|
+
if (messageSoundEnabled && msg.senderId !== 'me') playMessageSound();
|
|
123
|
+
// Update recent chats last message
|
|
124
|
+
setRecentChats(prev => prev.map(r =>
|
|
125
|
+
r.user.uid === msg.senderId || r.user.uid === msg.receiverId
|
|
126
|
+
? { ...r, lastMessage: msg.text, lastTime: msg.timestamp, unread: r.unread + 1 }
|
|
127
|
+
: r
|
|
128
|
+
));
|
|
129
|
+
},
|
|
130
|
+
onMessageAck: ({ messageId, status }) => {
|
|
131
|
+
updateMessageStatus(messageId, status);
|
|
132
|
+
},
|
|
133
|
+
onChatPaused: (_roomId, paused) => {
|
|
134
|
+
// Sync pause state from remote
|
|
135
|
+
setRecentChats(prev => prev.map(r =>
|
|
136
|
+
activeUser && r.user.uid === activeUser.uid ? { ...r, isPaused: paused } : r
|
|
137
|
+
));
|
|
138
|
+
},
|
|
139
|
+
onCallOffer: async (offer, fromUid, callId) => {
|
|
140
|
+
// Find peer user
|
|
141
|
+
const allUsers = [...(data?.developers ?? []), ...(data?.users ?? [])];
|
|
142
|
+
const peer = allUsers.find(u => u.uid === fromUid);
|
|
143
|
+
if (peer) {
|
|
144
|
+
await acceptCall(offer, peer, callId);
|
|
145
|
+
setScreen('call');
|
|
146
|
+
setIsOpen(true);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
onCallAnswer: async (answer, _fromUid, _callId) => {
|
|
150
|
+
await addIceCandidate({ sdpMid: '0', sdpMLineIndex: 0, candidate: '' });
|
|
151
|
+
// WebRTC: set remote description
|
|
152
|
+
void answer;
|
|
153
|
+
},
|
|
154
|
+
onIceCandidate: async (candidate) => {
|
|
155
|
+
await addIceCandidate(candidate);
|
|
156
|
+
},
|
|
157
|
+
onCallEnd: () => { _endCall(); },
|
|
158
|
+
onError: (code, message) => {
|
|
159
|
+
console.error('[ChatWidget] Socket error:', code, message);
|
|
160
|
+
},
|
|
161
|
+
onUserStatus: (uid, status) => {
|
|
162
|
+
setRecentChats(prev => prev.map(r =>
|
|
163
|
+
r.user.uid === uid ? { ...r, user: { ...r.user, status } } : r
|
|
164
|
+
));
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
useEffect(() => { setSocketStatus(_socketStatus); }, [_socketStatus]);
|
|
169
|
+
|
|
170
|
+
/* ── Chat hook ──────────────────────────────────────────────────────── */
|
|
96
171
|
const {
|
|
97
172
|
messages, activeUser, isPaused, isReported,
|
|
98
|
-
selectUser, sendMessage,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
173
|
+
selectUser, sendMessage, receiveMessage, updateMessageStatus,
|
|
174
|
+
togglePause: _togglePause, reportChat: _reportChat, clearChat, setMessages,
|
|
175
|
+
} = useChat([], {
|
|
176
|
+
onEmitMessage: (msg) => { emitMessage(msg); },
|
|
177
|
+
onEmitPause: (roomId, targetUid, paused) => { emitPauseToggle(roomId, targetUid, paused); },
|
|
178
|
+
onEmitReport: (roomId) => { emitReport(roomId); },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/* ── WebRTC hook ────────────────────────────────────────────────────── */
|
|
182
|
+
const {
|
|
183
|
+
session: callSession, localVideoRef, remoteVideoRef,
|
|
184
|
+
startCall: _startCall, acceptCall, addIceCandidate, endCall: _endCall,
|
|
185
|
+
toggleMute, toggleCamera,
|
|
186
|
+
} = useWebRTC({
|
|
187
|
+
onOfferReady: (offer, toUid, callId) => { emitCallOffer(offer, toUid, callId); },
|
|
188
|
+
onAnswerReady: (answer, toUid, callId) => { emitCallAnswer(answer, toUid, callId); },
|
|
189
|
+
onIceCandidateReady: (candidate, toUid) => { emitIceCandidate(candidate, toUid); },
|
|
190
|
+
onCallEnded: (callId, toUid) => { emitCallEnd(callId, toUid); },
|
|
191
|
+
});
|
|
103
192
|
|
|
104
193
|
const callInProgress =
|
|
105
194
|
callSession.state === 'calling' || callSession.state === 'connected';
|
|
@@ -108,7 +197,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
108
197
|
if (!callInProgress) setCallMinimized(false);
|
|
109
198
|
}, [callInProgress]);
|
|
110
199
|
|
|
111
|
-
/* ── Drawer open/close
|
|
200
|
+
/* ── Drawer open/close ──────────────────────────────────────────────── */
|
|
112
201
|
const openDrawer = () => {
|
|
113
202
|
setClosing(false);
|
|
114
203
|
setIsOpen(true);
|
|
@@ -119,23 +208,16 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
119
208
|
const w = data?.widget;
|
|
120
209
|
if (!w) return;
|
|
121
210
|
saveSession(w.id, {
|
|
122
|
-
screen,
|
|
123
|
-
activeTab,
|
|
124
|
-
userListCtx,
|
|
211
|
+
screen, activeTab, userListCtx,
|
|
125
212
|
activeUserUid: activeUser?.uid ?? null,
|
|
126
|
-
messages,
|
|
127
|
-
viewingTicketId,
|
|
128
|
-
chatReturnCtx,
|
|
213
|
+
messages, viewingTicketId, chatReturnCtx,
|
|
129
214
|
});
|
|
130
215
|
}, [data?.widget, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx]);
|
|
131
216
|
|
|
132
217
|
const closeDrawer = useCallback(() => {
|
|
133
218
|
persistWidgetState();
|
|
134
219
|
setClosing(true);
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
setIsOpen(false);
|
|
137
|
-
setClosing(false);
|
|
138
|
-
}, 300);
|
|
220
|
+
setTimeout(() => { setIsOpen(false); setClosing(false); }, 300);
|
|
139
221
|
}, [persistWidgetState]);
|
|
140
222
|
|
|
141
223
|
useEffect(() => {
|
|
@@ -171,7 +253,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
171
253
|
setViewingTicketId(p.viewingTicketId ?? null);
|
|
172
254
|
setChatReturnCtx(p.chatReturnCtx ?? 'conversation');
|
|
173
255
|
if (p.activeUserUid) {
|
|
174
|
-
const pid
|
|
256
|
+
const pid = viewer?.projectId?.trim();
|
|
175
257
|
const pool = pid
|
|
176
258
|
? [...data.developers, ...data.users].filter(u => u.project === pid)
|
|
177
259
|
: [...data.developers, ...data.users];
|
|
@@ -181,11 +263,14 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
181
263
|
? p.messages
|
|
182
264
|
: (data.sampleChats[u.uid] ?? []);
|
|
183
265
|
selectUser(u, hist);
|
|
266
|
+
// Rejoin socket room
|
|
267
|
+
const roomId = [u.uid, viewerUidForSocket].sort().join('_');
|
|
268
|
+
joinRoom(roomId);
|
|
184
269
|
}
|
|
185
270
|
}
|
|
186
271
|
}
|
|
187
272
|
restoredRef.current = true;
|
|
188
|
-
}, [data, selectUser, clearChat, viewer?.projectId, viewer?.uid]);
|
|
273
|
+
}, [data, selectUser, clearChat, viewer?.projectId, viewer?.uid, viewerUidForSocket, joinRoom]);
|
|
189
274
|
|
|
190
275
|
useEffect(() => {
|
|
191
276
|
if (!data?.widget) return;
|
|
@@ -209,20 +294,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
209
294
|
}, [data?.widget?.id, screen, activeTab, userListCtx, activeUser?.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
|
|
210
295
|
|
|
211
296
|
const incomingSoundRef = useRef(0);
|
|
212
|
-
useEffect(() => {
|
|
213
|
-
incomingSoundRef.current = messages.length;
|
|
214
|
-
}, [activeUser?.uid]);
|
|
215
|
-
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
if (!messageSoundEnabled || !activeUser || !data?.widget) return;
|
|
218
|
-
if (messages.length < incomingSoundRef.current) {
|
|
219
|
-
incomingSoundRef.current = messages.length;
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const added = messages.slice(incomingSoundRef.current);
|
|
223
|
-
incomingSoundRef.current = messages.length;
|
|
224
|
-
if (added.some(m => m.senderId !== 'me')) playMessageSound();
|
|
225
|
-
}, [messages, messageSoundEnabled, activeUser, data?.widget]);
|
|
297
|
+
useEffect(() => { incomingSoundRef.current = messages.length; }, [activeUser?.uid]);
|
|
226
298
|
|
|
227
299
|
const toggleMessageSound = useCallback((enabled: boolean) => {
|
|
228
300
|
const w = data?.widget;
|
|
@@ -231,28 +303,18 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
231
303
|
setMessageSoundEnabledState(enabled);
|
|
232
304
|
}, [data?.widget]);
|
|
233
305
|
|
|
234
|
-
/* ── Navigation
|
|
306
|
+
/* ── Navigation ─────────────────────────────────────────────────────── */
|
|
235
307
|
const handleCardClick = useCallback((ctx: UserListContext | 'ticket', options?: { fromMenu?: boolean }) => {
|
|
236
308
|
setListEntranceAnimation(!!options?.fromMenu);
|
|
237
|
-
if (ctx === 'ticket') {
|
|
238
|
-
|
|
239
|
-
setScreen('tickets');
|
|
240
|
-
} else {
|
|
241
|
-
setUserListCtx(ctx as UserListContext);
|
|
242
|
-
setScreen('user-list');
|
|
243
|
-
}
|
|
309
|
+
if (ctx === 'ticket') { setActiveTab('tickets'); setScreen('tickets'); }
|
|
310
|
+
else { setUserListCtx(ctx as UserListContext); setScreen('user-list'); }
|
|
244
311
|
}, []);
|
|
245
312
|
|
|
246
313
|
const handleNavFromMenu = useCallback((ctx: UserListContext | 'ticket') => {
|
|
247
314
|
setListEntranceAnimation(false);
|
|
248
315
|
clearChat();
|
|
249
|
-
if (ctx === 'ticket') {
|
|
250
|
-
|
|
251
|
-
setScreen('tickets');
|
|
252
|
-
} else {
|
|
253
|
-
setUserListCtx(ctx);
|
|
254
|
-
setScreen('user-list');
|
|
255
|
-
}
|
|
316
|
+
if (ctx === 'ticket') { setActiveTab('tickets'); setScreen('tickets'); }
|
|
317
|
+
else { setUserListCtx(ctx); setScreen('user-list'); }
|
|
256
318
|
}, [clearChat]);
|
|
257
319
|
|
|
258
320
|
const listCtxForUser = useCallback((user: ChatUser, viewerIsDev: boolean): UserListContext => {
|
|
@@ -266,19 +328,26 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
266
328
|
const history = data?.sampleChats[user.uid] ?? [];
|
|
267
329
|
selectUser(user, history);
|
|
268
330
|
setScreen('chat');
|
|
331
|
+
// Join socket room for real-time messages
|
|
332
|
+
const roomId = [user.uid, viewerUidForSocket].sort().join('_');
|
|
333
|
+
joinRoom(roomId);
|
|
269
334
|
setRecentChats(prev => {
|
|
270
335
|
const exists = prev.find(r => r.user.uid === user.uid);
|
|
271
|
-
if (exists) return prev;
|
|
336
|
+
if (exists) return prev.map(r => r.user.uid === user.uid ? { ...r, unread: 0 } : r);
|
|
272
337
|
return [{ id: `rc_${user.uid}`, user, lastMessage: '', lastTime: new Date().toISOString(), unread: 0, isPaused: false }, ...prev];
|
|
273
338
|
});
|
|
274
|
-
}, [data, selectUser, userListCtx]);
|
|
339
|
+
}, [data, selectUser, userListCtx, viewerUidForSocket, joinRoom]);
|
|
275
340
|
|
|
276
341
|
const handleBackFromChat = useCallback(() => {
|
|
277
342
|
setListEntranceAnimation(false);
|
|
343
|
+
if (activeUser) {
|
|
344
|
+
const roomId = [activeUser.uid, viewerUidForSocket].sort().join('_');
|
|
345
|
+
leaveRoom(roomId);
|
|
346
|
+
}
|
|
278
347
|
clearChat();
|
|
279
348
|
setUserListCtx(chatReturnCtx);
|
|
280
349
|
setScreen('user-list');
|
|
281
|
-
}, [clearChat, chatReturnCtx]);
|
|
350
|
+
}, [clearChat, chatReturnCtx, activeUser, viewerUidForSocket, leaveRoom]);
|
|
282
351
|
|
|
283
352
|
const handleOpenTicket = useCallback((id: string) => {
|
|
284
353
|
setListEntranceAnimation(false);
|
|
@@ -299,20 +368,22 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
299
368
|
return () => window.clearTimeout(t);
|
|
300
369
|
}, [listEntranceAnimation]);
|
|
301
370
|
|
|
302
|
-
/* ── Block/Unblock
|
|
371
|
+
/* ── Block/Unblock ──────────────────────────────────────────────────── */
|
|
303
372
|
const handleBlock = useCallback(() => {
|
|
304
373
|
if (!activeUser) return;
|
|
305
374
|
setBlockedUids(prev => [...prev, activeUser.uid]);
|
|
375
|
+
emitBlock(activeUser.uid);
|
|
306
376
|
clearChat();
|
|
307
377
|
setScreen('block-list');
|
|
308
378
|
setActiveTab('home');
|
|
309
|
-
}, [activeUser, clearChat]);
|
|
379
|
+
}, [activeUser, clearChat, emitBlock]);
|
|
310
380
|
|
|
311
381
|
const handleUnblock = useCallback((uid: string) => {
|
|
312
382
|
setBlockedUids(prev => prev.filter(id => id !== uid));
|
|
313
|
-
|
|
383
|
+
emitUnblock(uid);
|
|
384
|
+
}, [emitUnblock]);
|
|
314
385
|
|
|
315
|
-
/* ── Tickets
|
|
386
|
+
/* ── Tickets ────────────────────────────────────────────────────────── */
|
|
316
387
|
const handleRaiseTicket = useCallback((title: string, desc: string, priority: Ticket['priority']) => {
|
|
317
388
|
const t: Ticket = {
|
|
318
389
|
id: `TKT-${String(Date.now()).slice(-4)}`,
|
|
@@ -327,40 +398,40 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
327
398
|
setActiveTab('tickets');
|
|
328
399
|
}, []);
|
|
329
400
|
|
|
330
|
-
/* ── Pause sync back into recent chats
|
|
401
|
+
/* ── Pause sync back into recent chats ─────────────────────────────── */
|
|
331
402
|
const handleTogglePause = useCallback(() => {
|
|
332
|
-
|
|
403
|
+
_togglePause();
|
|
333
404
|
if (activeUser) {
|
|
334
|
-
setRecentChats(prev => prev.map(r =>
|
|
405
|
+
setRecentChats(prev => prev.map(r =>
|
|
406
|
+
r.user.uid === activeUser.uid ? { ...r, isPaused: !isPaused } : r
|
|
407
|
+
));
|
|
335
408
|
}
|
|
336
|
-
}, [
|
|
409
|
+
}, [_togglePause, activeUser, isPaused]);
|
|
337
410
|
|
|
338
|
-
/* ── Call
|
|
411
|
+
/* ── Call ───────────────────────────────────────────────────────────── */
|
|
339
412
|
const handleStartCall = useCallback((withVideo: boolean) => {
|
|
340
413
|
if (!activeUser) return;
|
|
341
|
-
|
|
414
|
+
_startCall(activeUser, withVideo);
|
|
342
415
|
setScreen('call');
|
|
343
|
-
}, [activeUser,
|
|
416
|
+
}, [activeUser, _startCall]);
|
|
344
417
|
|
|
345
418
|
const handleEndCall = useCallback(() => {
|
|
346
|
-
|
|
419
|
+
_endCall();
|
|
347
420
|
setCallMinimized(false);
|
|
348
421
|
setScreen('chat');
|
|
349
|
-
}, [
|
|
422
|
+
}, [_endCall]);
|
|
350
423
|
|
|
351
424
|
const minimizeCall = useCallback(() => {
|
|
352
425
|
setCallMinimized(true);
|
|
353
426
|
closeDrawer();
|
|
354
427
|
}, [closeDrawer]);
|
|
355
428
|
|
|
356
|
-
/* ── Derived
|
|
357
|
-
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
358
|
-
|
|
429
|
+
/* ── Derived config (must be declared before callbacks that use it) ── */
|
|
359
430
|
const widgetConfig = useMemo(() => {
|
|
360
431
|
if (!data?.widget) return undefined;
|
|
361
432
|
const w = { ...data.widget };
|
|
362
433
|
if (viewer) {
|
|
363
|
-
w.viewerUid
|
|
434
|
+
w.viewerUid = viewer.uid;
|
|
364
435
|
w.viewerName = viewer.name;
|
|
365
436
|
w.viewerType = viewer.type;
|
|
366
437
|
if (viewer.projectId?.trim()) w.viewerProjectId = viewer.projectId.trim();
|
|
@@ -368,16 +439,42 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
368
439
|
return w;
|
|
369
440
|
}, [data?.widget, viewer]);
|
|
370
441
|
|
|
371
|
-
|
|
442
|
+
/* ── Transfer (developer feature) ──────────────────────────────────── */
|
|
443
|
+
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
444
|
+
if (!activeUser || !widgetConfig) return;
|
|
445
|
+
const agent = widgetConfig.viewerName?.trim() || 'Agent';
|
|
446
|
+
const roomId = [activeUser.uid, viewerUidForSocket].sort().join('_');
|
|
447
|
+
const transferNote: ChatMessage = {
|
|
448
|
+
id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
449
|
+
senderId: 'me',
|
|
450
|
+
receiverId: dev.uid,
|
|
451
|
+
text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
|
|
452
|
+
timestamp: new Date().toISOString(),
|
|
453
|
+
type: 'text',
|
|
454
|
+
status: 'sent',
|
|
455
|
+
};
|
|
456
|
+
emitTransfer(roomId, dev.uid, transferNote.text);
|
|
457
|
+
selectUser(dev, [...messages, transferNote]);
|
|
458
|
+
}, [activeUser, messages, selectUser, widgetConfig, viewerUidForSocket, emitTransfer]);
|
|
459
|
+
|
|
460
|
+
/* ── Add participant (developer feature) ────────────────────────────── */
|
|
461
|
+
const handleAddParticipant = useCallback((uid: string) => {
|
|
462
|
+
if (!activeUser) return;
|
|
463
|
+
const roomId = [activeUser.uid, viewerUidForSocket].sort().join('_');
|
|
464
|
+
emitAddParticipant(roomId, uid);
|
|
465
|
+
}, [activeUser, viewerUidForSocket, emitAddParticipant]);
|
|
466
|
+
|
|
467
|
+
/* ── Derived ────────────────────────────────────────────────────────── */
|
|
468
|
+
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
469
|
+
|
|
470
|
+
const primaryColor = theme.primaryColor;
|
|
372
471
|
|
|
373
|
-
/** All developers are listed; only end-`user` rows are filtered by `viewer.projectId`. */
|
|
374
472
|
const allUsers = useMemo(() => {
|
|
375
473
|
if (!data) return [];
|
|
376
|
-
const pid
|
|
474
|
+
const pid = viewer?.projectId?.trim();
|
|
377
475
|
const devs = data.developers ?? [];
|
|
378
476
|
if (!pid) return [...devs, ...data.users];
|
|
379
|
-
|
|
380
|
-
return [...devs, ...usersInProject];
|
|
477
|
+
return [...devs, ...data.users.filter(u => u.project === pid)];
|
|
381
478
|
}, [data, viewer?.projectId]);
|
|
382
479
|
|
|
383
480
|
const effectiveViewerBlocked = useMemo(() => {
|
|
@@ -389,18 +486,15 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
389
486
|
return rec?.viewerBlocked === true;
|
|
390
487
|
}, [widgetConfig, viewer?.uid, data]);
|
|
391
488
|
|
|
392
|
-
const viewerIsDev
|
|
393
|
-
const viewerUid
|
|
489
|
+
const viewerIsDev = widgetConfig?.viewerType === 'developer';
|
|
490
|
+
const viewerUid = widgetConfig?.viewerUid;
|
|
394
491
|
|
|
395
492
|
const filteredUsers = screen === 'user-list'
|
|
396
493
|
? allUsers.filter(u => {
|
|
397
494
|
if (userListCtx === 'support') {
|
|
398
|
-
|
|
399
|
-
return u.type === 'developer';
|
|
400
|
-
}
|
|
401
|
-
if (viewerIsDev) {
|
|
402
|
-
return u.type === 'developer' && u.uid !== viewerUid;
|
|
495
|
+
return viewerIsDev ? u.type === 'user' : u.type === 'developer';
|
|
403
496
|
}
|
|
497
|
+
if (viewerIsDev) return u.type === 'developer' && u.uid !== viewerUid;
|
|
404
498
|
return u.type === 'user';
|
|
405
499
|
})
|
|
406
500
|
: [];
|
|
@@ -409,60 +503,30 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
409
503
|
() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid),
|
|
410
504
|
[allUsers, viewerUid],
|
|
411
505
|
);
|
|
412
|
-
const blockedUsers
|
|
506
|
+
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
413
507
|
|
|
414
508
|
const totalUnread = useMemo(
|
|
415
509
|
() => recentChats.reduce((sum, c) => sum + Math.max(0, c.unread ?? 0), 0),
|
|
416
510
|
[recentChats],
|
|
417
511
|
);
|
|
418
512
|
|
|
419
|
-
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
420
|
-
if (!activeUser || !widgetConfig) return;
|
|
421
|
-
const agent = widgetConfig.viewerName?.trim() || 'Agent';
|
|
422
|
-
const transferNote: ChatMessage = {
|
|
423
|
-
id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
424
|
-
senderId: 'me',
|
|
425
|
-
receiverId: dev.uid,
|
|
426
|
-
text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
|
|
427
|
-
timestamp: new Date().toISOString(),
|
|
428
|
-
type: 'text',
|
|
429
|
-
status: 'sent',
|
|
430
|
-
};
|
|
431
|
-
selectUser(dev, [...messages, transferNote]);
|
|
432
|
-
}, [activeUser, messages, selectUser, widgetConfig]);
|
|
433
|
-
|
|
434
|
-
/* Position */
|
|
435
513
|
const posStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
|
|
436
514
|
? { left: 24, right: 'auto' }
|
|
437
515
|
: { right: 24, left: 'auto' };
|
|
438
516
|
|
|
439
|
-
/* No radius on top-left / bottom-left; left-docked panel keeps inner TR/BR curve */
|
|
440
517
|
const drawerPosStyle: React.CSSProperties =
|
|
441
518
|
theme.buttonPosition === 'bottom-left'
|
|
442
|
-
? {
|
|
443
|
-
|
|
444
|
-
borderTopLeftRadius: 0,
|
|
445
|
-
borderBottomLeftRadius: 0,
|
|
446
|
-
borderTopRightRadius: 16,
|
|
447
|
-
borderBottomRightRadius: 16,
|
|
448
|
-
}
|
|
449
|
-
: {
|
|
450
|
-
right: 0,
|
|
451
|
-
borderTopLeftRadius: 0,
|
|
452
|
-
borderBottomLeftRadius: 0,
|
|
453
|
-
};
|
|
519
|
+
? { left: 0, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, borderTopRightRadius: 16, borderBottomRightRadius: 16 }
|
|
520
|
+
: { right: 0, borderTopLeftRadius: 0, borderBottomLeftRadius: 0 };
|
|
454
521
|
|
|
455
|
-
/* ── Don't render until mounted (SSR safe) ──────────────────────────── */
|
|
456
522
|
if (!mounted) return null;
|
|
457
523
|
|
|
458
524
|
return (
|
|
459
|
-
|
|
525
|
+
<ErrorBoundary primaryColor={primaryColor}>
|
|
460
526
|
{/* ── Global styles ── */}
|
|
461
527
|
<style>{`
|
|
462
528
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
|
|
463
|
-
|
|
464
529
|
.cw-root * { box-sizing: border-box; font-family: 'DM Sans', 'Segoe UI', sans-serif; }
|
|
465
|
-
|
|
466
530
|
@keyframes cw-slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
467
531
|
@keyframes cw-slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
|
|
468
532
|
@keyframes cw-slideInLeft { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
@@ -471,25 +535,30 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
471
535
|
@keyframes cw-slideIn { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); } }
|
|
472
536
|
@keyframes cw-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
473
537
|
@keyframes cw-btnPop { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
|
474
|
-
|
|
538
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
475
539
|
.cw-scroll::-webkit-scrollbar { width: 4px; }
|
|
476
540
|
.cw-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
477
541
|
.cw-scroll::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 4px; }
|
|
478
|
-
|
|
479
542
|
.cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
|
|
480
543
|
.cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
|
|
481
|
-
|
|
482
|
-
.cw-drawer-panel {
|
|
483
|
-
width: 30%;
|
|
484
|
-
max-width: 100vw;
|
|
485
|
-
min-width: 0;
|
|
486
|
-
}
|
|
487
|
-
@media (max-width: 1024px) {
|
|
488
|
-
.cw-drawer-panel { width: 100%; }
|
|
489
|
-
}
|
|
544
|
+
.cw-drawer-panel { width: 30%; max-width: 100vw; min-width: 0; }
|
|
545
|
+
@media (max-width: 1024px) { .cw-drawer-panel { width: 100%; } }
|
|
490
546
|
`}</style>
|
|
491
547
|
|
|
492
|
-
{/* ──
|
|
548
|
+
{/* ── Socket status dot (subtle, only visible when not connected) ── */}
|
|
549
|
+
{socketStatus !== 'connected' && socketStatus !== 'disconnected' && (
|
|
550
|
+
<div style={{
|
|
551
|
+
position: 'fixed', bottom: 80, ...posStyle, zIndex: 9996,
|
|
552
|
+
background: socketStatus === 'connecting' ? '#f59e0b' : '#ef4444',
|
|
553
|
+
color: '#fff', fontSize: 11, fontWeight: 700,
|
|
554
|
+
padding: '3px 10px', borderRadius: 20,
|
|
555
|
+
animation: 'cw-fadeUp 0.3s ease',
|
|
556
|
+
}}>
|
|
557
|
+
{socketStatus === 'connecting' ? '● Connecting…' : '● Offline'}
|
|
558
|
+
</div>
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{/* ── Minimized call bar ── */}
|
|
493
562
|
{!isOpen && callMinimized && callInProgress && callSession.peer && (
|
|
494
563
|
<MiniCallBar
|
|
495
564
|
session={callSession}
|
|
@@ -500,7 +569,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
500
569
|
/>
|
|
501
570
|
)}
|
|
502
571
|
|
|
503
|
-
{/* ── Floating Button
|
|
572
|
+
{/* ── Floating Button ── */}
|
|
504
573
|
{!isOpen && (
|
|
505
574
|
<button
|
|
506
575
|
className="cw-root"
|
|
@@ -509,12 +578,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
509
578
|
aria-label={totalUnread > 0 ? `${theme.buttonLabel}, ${totalUnread} unread` : theme.buttonLabel}
|
|
510
579
|
title={totalUnread > 0 ? `${totalUnread} unread message${totalUnread === 1 ? '' : 's'}` : theme.buttonLabel}
|
|
511
580
|
style={{
|
|
512
|
-
position: 'fixed', bottom: 24, zIndex: 9999,
|
|
513
|
-
...posStyle,
|
|
581
|
+
position: 'fixed', bottom: 24, zIndex: 9999, ...posStyle,
|
|
514
582
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
515
583
|
padding: '13px 22px',
|
|
516
|
-
backgroundColor: theme.buttonColor,
|
|
517
|
-
color: theme.buttonTextColor,
|
|
584
|
+
backgroundColor: theme.buttonColor, color: theme.buttonTextColor,
|
|
518
585
|
border: 'none', borderRadius: 50,
|
|
519
586
|
cursor: 'pointer', fontSize: 15, fontWeight: 700,
|
|
520
587
|
boxShadow: `0 8px 28px ${theme.buttonColor}55`,
|
|
@@ -536,25 +603,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
536
603
|
stroke={theme.buttonTextColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
537
604
|
</svg>
|
|
538
605
|
{totalUnread > 0 && (
|
|
539
|
-
<span
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
height: 20,
|
|
546
|
-
padding: '0 5px',
|
|
547
|
-
borderRadius: 999,
|
|
548
|
-
background: '#ef4444',
|
|
549
|
-
color: '#fff',
|
|
550
|
-
fontSize: 11,
|
|
551
|
-
fontWeight: 800,
|
|
552
|
-
lineHeight: '20px',
|
|
553
|
-
textAlign: 'center',
|
|
554
|
-
border: '2px solid #fff',
|
|
555
|
-
boxSizing: 'border-box',
|
|
556
|
-
}}
|
|
557
|
-
>
|
|
606
|
+
<span style={{
|
|
607
|
+
position: 'absolute', top: -8, right: -10,
|
|
608
|
+
minWidth: 20, height: 20, padding: '0 5px', borderRadius: 999,
|
|
609
|
+
background: '#ef4444', color: '#fff', fontSize: 11, fontWeight: 800,
|
|
610
|
+
lineHeight: '20px', textAlign: 'center', border: '2px solid #fff', boxSizing: 'border-box',
|
|
611
|
+
}}>
|
|
558
612
|
{totalUnread > 99 ? '99+' : totalUnread}
|
|
559
613
|
</span>
|
|
560
614
|
)}
|
|
@@ -563,7 +617,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
563
617
|
</button>
|
|
564
618
|
)}
|
|
565
619
|
|
|
566
|
-
{/* ── Backdrop
|
|
620
|
+
{/* ── Backdrop ── */}
|
|
567
621
|
{isOpen && (
|
|
568
622
|
<div
|
|
569
623
|
aria-hidden
|
|
@@ -576,59 +630,47 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
576
630
|
/>
|
|
577
631
|
)}
|
|
578
632
|
|
|
579
|
-
{/* ── Drawer
|
|
633
|
+
{/* ── Drawer ── */}
|
|
580
634
|
{isOpen && (
|
|
581
635
|
<div
|
|
582
636
|
className={`cw-root cw-drawer-panel ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
|
|
583
637
|
style={{
|
|
584
|
-
position: 'fixed',
|
|
585
|
-
top: 0,
|
|
586
|
-
bottom: 0,
|
|
587
|
-
...drawerPosStyle,
|
|
588
|
-
zIndex: 9998,
|
|
638
|
+
position: 'fixed', top: 0, bottom: 0, ...drawerPosStyle, zIndex: 9998,
|
|
589
639
|
backgroundColor: '#fff',
|
|
590
640
|
boxShadow: theme.buttonPosition === 'bottom-left'
|
|
591
641
|
? '4px 0 40px rgba(0,0,0,0.18)'
|
|
592
642
|
: '-4px 0 40px rgba(0,0,0,0.18)',
|
|
593
|
-
display: 'flex',
|
|
594
|
-
flexDirection: 'column',
|
|
595
|
-
overflow: 'hidden',
|
|
643
|
+
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
596
644
|
}}
|
|
597
645
|
>
|
|
598
|
-
{/*
|
|
646
|
+
{/* Loading */}
|
|
599
647
|
{cfgLoading && (
|
|
600
|
-
<div style={{ display:
|
|
601
|
-
<div style={{
|
|
602
|
-
|
|
603
|
-
border: `3px solid ${primaryColor}30`,
|
|
604
|
-
borderTopColor: primaryColor,
|
|
605
|
-
animation: 'spin 0.8s linear infinite',
|
|
606
|
-
}} />
|
|
607
|
-
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
608
|
-
<p style={{ fontSize: 14, color: '#7b8fa1' }}>Loading chat…</p>
|
|
648
|
+
<div style={{ display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',height:'100%',gap:16 }}>
|
|
649
|
+
<div style={{ width:40,height:40,borderRadius:'50%',border:`3px solid ${primaryColor}30`,borderTopColor:primaryColor,animation:'spin 0.8s linear infinite' }} />
|
|
650
|
+
<p style={{ fontSize:14,color:'#7b8fa1' }}>Loading chat…</p>
|
|
609
651
|
</div>
|
|
610
652
|
)}
|
|
611
653
|
|
|
612
|
-
{/*
|
|
654
|
+
{/* Error */}
|
|
613
655
|
{cfgError && !cfgLoading && (
|
|
614
|
-
<div style={{ display:
|
|
615
|
-
<div style={{ fontSize:
|
|
616
|
-
<p style={{ fontWeight:
|
|
617
|
-
<p style={{ fontSize:
|
|
618
|
-
<button onClick={closeDrawer} style={{ padding:
|
|
656
|
+
<div style={{ display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',height:'100%',gap:12,padding:32,textAlign:'center' }}>
|
|
657
|
+
<div style={{ fontSize:40 }}>⚠️</div>
|
|
658
|
+
<p style={{ fontWeight:700,color:'#1a2332' }}>Could not load chat configuration</p>
|
|
659
|
+
<p style={{ fontSize:13,color:'#7b8fa1',lineHeight:1.6 }}>{cfgError}</p>
|
|
660
|
+
<button onClick={closeDrawer} style={{ padding:'9px 20px',borderRadius:10,border:'none',background:primaryColor,color:'#fff',cursor:'pointer',fontWeight:700 }}>Close</button>
|
|
619
661
|
</div>
|
|
620
662
|
)}
|
|
621
663
|
|
|
622
|
-
{/*
|
|
664
|
+
{/* Main content */}
|
|
623
665
|
{!cfgLoading && !cfgError && widgetConfig && (
|
|
624
|
-
|
|
625
|
-
{/*
|
|
666
|
+
<ErrorBoundary primaryColor={primaryColor}>
|
|
667
|
+
{/* Close button */}
|
|
626
668
|
{screen !== 'chat' && screen !== 'call' && !effectiveViewerBlocked && (
|
|
627
669
|
<div style={{
|
|
628
|
-
position:
|
|
670
|
+
position:'absolute',top:12,
|
|
629
671
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
630
|
-
left: theme.buttonPosition === 'bottom-left' ? 12
|
|
631
|
-
zIndex:
|
|
672
|
+
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
673
|
+
zIndex:20,display:'flex',gap:6,
|
|
632
674
|
}}>
|
|
633
675
|
<CornerBtn onClick={closeDrawer} title="Close">
|
|
634
676
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
|
@@ -638,12 +680,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
638
680
|
</div>
|
|
639
681
|
)}
|
|
640
682
|
|
|
641
|
-
{
|
|
642
|
-
{widgetConfig.status === 'MAINTENANCE' && (
|
|
643
|
-
<MaintenanceView primaryColor={primaryColor} />
|
|
644
|
-
)}
|
|
683
|
+
{widgetConfig.status === 'MAINTENANCE' && <MaintenanceView primaryColor={primaryColor} />}
|
|
645
684
|
|
|
646
|
-
{/* ── DISABLED ── */}
|
|
647
685
|
{widgetConfig.status === 'DISABLE' && (
|
|
648
686
|
<div style={{ display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',height:'100%',padding:32,textAlign:'center',gap:12 }}>
|
|
649
687
|
<div style={{ fontSize:40 }}>🔒</div>
|
|
@@ -652,12 +690,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
652
690
|
</div>
|
|
653
691
|
)}
|
|
654
692
|
|
|
655
|
-
{/* ── ACTIVE: viewer spam-blocked (no chat/tickets UI) ── */}
|
|
656
693
|
{widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (
|
|
657
694
|
<ViewerBlockedScreen config={widgetConfig} apiKey={apiKey} onClose={closeDrawer} />
|
|
658
695
|
)}
|
|
659
696
|
|
|
660
|
-
{/* ── ACTIVE: microphone, location, screen share required ── */}
|
|
661
697
|
{widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (
|
|
662
698
|
<PermissionsGateScreen
|
|
663
699
|
primaryColor={primaryColor}
|
|
@@ -666,14 +702,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
666
702
|
/>
|
|
667
703
|
)}
|
|
668
704
|
|
|
669
|
-
{/* ── ACTIVE ── */}
|
|
670
705
|
{widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (
|
|
671
|
-
<div className="cw-scroll" style={{ flex:
|
|
706
|
+
<div className="cw-scroll" style={{ flex:1,display:'flex',flexDirection:'column',overflow:'hidden' }}>
|
|
672
707
|
|
|
673
708
|
{screen === 'home' && (
|
|
674
709
|
<HomeScreen
|
|
675
|
-
config={widgetConfig}
|
|
676
|
-
apiKey={apiKey}
|
|
710
|
+
config={widgetConfig} apiKey={apiKey}
|
|
677
711
|
onNavigate={handleCardClick}
|
|
678
712
|
onOpenTicket={handleOpenTicket}
|
|
679
713
|
tickets={tickets}
|
|
@@ -706,7 +740,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
706
740
|
onBack={handleBackFromChat}
|
|
707
741
|
onClose={closeDrawer}
|
|
708
742
|
onTogglePause={handleTogglePause}
|
|
709
|
-
onReport={
|
|
743
|
+
onReport={_reportChat}
|
|
710
744
|
onBlock={handleBlock}
|
|
711
745
|
onStartCall={handleStartCall}
|
|
712
746
|
onNavAction={handleNavFromMenu}
|
|
@@ -761,18 +795,16 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
761
795
|
/>
|
|
762
796
|
)}
|
|
763
797
|
|
|
764
|
-
{screen === 'ticket-detail' && viewingTicketId && (
|
|
765
|
-
(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
})()
|
|
775
|
-
)}
|
|
798
|
+
{screen === 'ticket-detail' && viewingTicketId && (() => {
|
|
799
|
+
const t = tickets.find(x => x.id === viewingTicketId);
|
|
800
|
+
return t ? (
|
|
801
|
+
<TicketDetailScreen
|
|
802
|
+
ticket={t}
|
|
803
|
+
config={widgetConfig}
|
|
804
|
+
onBack={() => { setViewingTicketId(null); setScreen('tickets'); }}
|
|
805
|
+
/>
|
|
806
|
+
) : null;
|
|
807
|
+
})()}
|
|
776
808
|
|
|
777
809
|
{screen === 'block-list' && (
|
|
778
810
|
<BlockListScreen
|
|
@@ -785,38 +817,28 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
785
817
|
</div>
|
|
786
818
|
)}
|
|
787
819
|
|
|
788
|
-
{/*
|
|
820
|
+
{/* Bottom Tabs */}
|
|
789
821
|
{widgetConfig.status === 'ACTIVE' &&
|
|
790
|
-
!effectiveViewerBlocked &&
|
|
791
|
-
|
|
792
|
-
screen !== '
|
|
793
|
-
screen !== '
|
|
794
|
-
|
|
795
|
-
screen !== 'block-list' &&
|
|
796
|
-
screen !== 'ticket-detail' &&
|
|
797
|
-
screen !== 'ticket-new' && (
|
|
798
|
-
<BottomTabs
|
|
799
|
-
active={activeTab}
|
|
800
|
-
onChange={handleTabChange}
|
|
801
|
-
primaryColor={primaryColor}
|
|
802
|
-
/>
|
|
822
|
+
!effectiveViewerBlocked && permissionsOk &&
|
|
823
|
+
screen !== 'chat' && screen !== 'call' &&
|
|
824
|
+
screen !== 'user-list' && screen !== 'block-list' &&
|
|
825
|
+
screen !== 'ticket-detail' && screen !== 'ticket-new' && (
|
|
826
|
+
<BottomTabs active={activeTab} onChange={handleTabChange} primaryColor={primaryColor} />
|
|
803
827
|
)}
|
|
804
|
-
|
|
828
|
+
</ErrorBoundary>
|
|
805
829
|
)}
|
|
806
830
|
</div>
|
|
807
831
|
)}
|
|
808
|
-
|
|
832
|
+
</ErrorBoundary>
|
|
809
833
|
);
|
|
810
834
|
};
|
|
811
835
|
|
|
812
836
|
export default ChatWidget;
|
|
813
837
|
|
|
814
|
-
/* ── Tiny corner button ────────────────────────────────────────────────────── */
|
|
815
838
|
const CornerBtn: React.FC<{ onClick: () => void; title: string; children: React.ReactNode }> = ({ onClick, title, children }) => (
|
|
816
839
|
<button onClick={onClick} title={title} style={{
|
|
817
|
-
width:
|
|
818
|
-
|
|
819
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
|
840
|
+
width:26,height:26,borderRadius:'50%',background:'rgba(0,0,0,0.25)',border:'none',
|
|
841
|
+
display:'flex',alignItems:'center',justifyContent:'center',cursor:'pointer',
|
|
820
842
|
}}>
|
|
821
843
|
{children}
|
|
822
844
|
</button>
|