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