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.
Files changed (147) hide show
  1. package/dist/components/BlockList/index.d.ts +1 -0
  2. package/dist/components/BlockList/index.d.ts.map +1 -0
  3. package/dist/components/BlockList/index.js +55 -28
  4. package/dist/components/BlockList/index.js.map +1 -0
  5. package/dist/components/CallScreen/index.d.ts +3 -0
  6. package/dist/components/CallScreen/index.d.ts.map +1 -0
  7. package/dist/components/CallScreen/index.js +107 -29
  8. package/dist/components/CallScreen/index.js.map +1 -0
  9. package/dist/components/ChatScreen/index.d.ts +1 -0
  10. package/dist/components/ChatScreen/index.d.ts.map +1 -0
  11. package/dist/components/ChatScreen/index.js +493 -294
  12. package/dist/components/ChatScreen/index.js.map +1 -0
  13. package/dist/components/ChatWidget.d.ts +1 -0
  14. package/dist/components/ChatWidget.d.ts.map +1 -0
  15. package/dist/components/ChatWidget.js +359 -250
  16. package/dist/components/ChatWidget.js.map +1 -0
  17. package/dist/components/EmojiPicker/index.d.ts +1 -0
  18. package/dist/components/EmojiPicker/index.d.ts.map +1 -0
  19. package/dist/components/EmojiPicker/index.js +19 -7
  20. package/dist/components/EmojiPicker/index.js.map +1 -0
  21. package/dist/components/ErrorBoundary/index.d.ts +20 -0
  22. package/dist/components/ErrorBoundary/index.d.ts.map +1 -0
  23. package/dist/components/ErrorBoundary/index.js +76 -0
  24. package/dist/components/ErrorBoundary/index.js.map +1 -0
  25. package/dist/components/HomeScreen/index.d.ts +1 -0
  26. package/dist/components/HomeScreen/index.d.ts.map +1 -0
  27. package/dist/components/HomeScreen/index.js +236 -158
  28. package/dist/components/HomeScreen/index.js.map +1 -0
  29. package/dist/components/MaintenanceView/index.d.ts +1 -0
  30. package/dist/components/MaintenanceView/index.d.ts.map +1 -0
  31. package/dist/components/MaintenanceView/index.js +28 -12
  32. package/dist/components/MaintenanceView/index.js.map +1 -0
  33. package/dist/components/MiniCallBar/index.d.ts +15 -0
  34. package/dist/components/MiniCallBar/index.d.ts.map +1 -0
  35. package/dist/components/MiniCallBar/index.js +116 -0
  36. package/dist/components/MiniCallBar/index.js.map +1 -0
  37. package/dist/components/PermissionsGateScreen/index.d.ts +1 -0
  38. package/dist/components/PermissionsGateScreen/index.d.ts.map +1 -0
  39. package/dist/components/PermissionsGateScreen/index.js +82 -28
  40. package/dist/components/PermissionsGateScreen/index.js.map +1 -0
  41. package/dist/components/RecentChatsScreen/index.d.ts +1 -0
  42. package/dist/components/RecentChatsScreen/index.d.ts.map +1 -0
  43. package/dist/components/RecentChatsScreen/index.js +79 -19
  44. package/dist/components/RecentChatsScreen/index.js.map +1 -0
  45. package/dist/components/SlideNavMenu.d.ts +1 -0
  46. package/dist/components/SlideNavMenu.d.ts.map +1 -0
  47. package/dist/components/SlideNavMenu.js +82 -63
  48. package/dist/components/SlideNavMenu.js.map +1 -0
  49. package/dist/components/Tabs/BottomTabs.d.ts +1 -0
  50. package/dist/components/Tabs/BottomTabs.d.ts.map +1 -0
  51. package/dist/components/Tabs/BottomTabs.js +34 -19
  52. package/dist/components/Tabs/BottomTabs.js.map +1 -0
  53. package/dist/components/TicketDetailScreen/index.d.ts +1 -0
  54. package/dist/components/TicketDetailScreen/index.d.ts.map +1 -0
  55. package/dist/components/TicketDetailScreen/index.js +66 -27
  56. package/dist/components/TicketDetailScreen/index.js.map +1 -0
  57. package/dist/components/TicketFormScreen/index.d.ts +1 -0
  58. package/dist/components/TicketFormScreen/index.d.ts.map +1 -0
  59. package/dist/components/TicketFormScreen/index.js +99 -49
  60. package/dist/components/TicketFormScreen/index.js.map +1 -0
  61. package/dist/components/TicketScreen/index.d.ts +1 -0
  62. package/dist/components/TicketScreen/index.d.ts.map +1 -0
  63. package/dist/components/TicketScreen/index.js +95 -26
  64. package/dist/components/TicketScreen/index.js.map +1 -0
  65. package/dist/components/UserListScreen/index.d.ts +1 -0
  66. package/dist/components/UserListScreen/index.d.ts.map +1 -0
  67. package/dist/components/UserListScreen/index.js +127 -53
  68. package/dist/components/UserListScreen/index.js.map +1 -0
  69. package/dist/components/ViewerBlockedScreen/index.d.ts +1 -0
  70. package/dist/components/ViewerBlockedScreen/index.d.ts.map +1 -0
  71. package/dist/components/ViewerBlockedScreen/index.js +113 -61
  72. package/dist/components/ViewerBlockedScreen/index.js.map +1 -0
  73. package/dist/config/index.d.ts +1 -0
  74. package/dist/config/index.d.ts.map +1 -0
  75. package/dist/config/index.js +7 -2
  76. package/dist/config/index.js.map +1 -0
  77. package/dist/hooks/useChat.d.ts +9 -1
  78. package/dist/hooks/useChat.d.ts.map +1 -0
  79. package/dist/hooks/useChat.js +60 -18
  80. package/dist/hooks/useChat.js.map +1 -0
  81. package/dist/hooks/useRemoteConfig.d.ts +1 -0
  82. package/dist/hooks/useRemoteConfig.d.ts.map +1 -0
  83. package/dist/hooks/useRemoteConfig.js +12 -8
  84. package/dist/hooks/useRemoteConfig.js.map +1 -0
  85. package/dist/hooks/useSocket.d.ts +40 -0
  86. package/dist/hooks/useSocket.d.ts.map +1 -0
  87. package/dist/hooks/useSocket.js +190 -0
  88. package/dist/hooks/useSocket.js.map +1 -0
  89. package/dist/hooks/useWebRTC.d.ts +10 -2
  90. package/dist/hooks/useWebRTC.d.ts.map +1 -0
  91. package/dist/hooks/useWebRTC.js +101 -69
  92. package/dist/hooks/useWebRTC.js.map +1 -0
  93. package/dist/index.d.ts +6 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +67 -21
  96. package/dist/index.js.map +1 -0
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +3 -1
  100. package/dist/types/index.js.map +1 -0
  101. package/dist/utils/chat.d.ts +1 -0
  102. package/dist/utils/chat.d.ts.map +1 -0
  103. package/dist/utils/chat.js +17 -7
  104. package/dist/utils/chat.js.map +1 -0
  105. package/dist/utils/fileName.d.ts +1 -0
  106. package/dist/utils/fileName.d.ts.map +1 -0
  107. package/dist/utils/fileName.js +5 -1
  108. package/dist/utils/fileName.js.map +1 -0
  109. package/dist/utils/messageSound.d.ts +1 -0
  110. package/dist/utils/messageSound.d.ts.map +1 -0
  111. package/dist/utils/messageSound.js +9 -3
  112. package/dist/utils/messageSound.js.map +1 -0
  113. package/dist/utils/presenceStatus.d.ts +1 -0
  114. package/dist/utils/presenceStatus.d.ts.map +1 -0
  115. package/dist/utils/presenceStatus.js +11 -4
  116. package/dist/utils/presenceStatus.js.map +1 -0
  117. package/dist/utils/privacyConsent.d.ts +1 -0
  118. package/dist/utils/privacyConsent.d.ts.map +1 -0
  119. package/dist/utils/privacyConsent.js +9 -3
  120. package/dist/utils/privacyConsent.js.map +1 -0
  121. package/dist/utils/reenableRequest.d.ts +1 -0
  122. package/dist/utils/reenableRequest.d.ts.map +1 -0
  123. package/dist/utils/reenableRequest.js +5 -1
  124. package/dist/utils/reenableRequest.js.map +1 -0
  125. package/dist/utils/theme.d.ts +1 -0
  126. package/dist/utils/theme.d.ts.map +1 -0
  127. package/dist/utils/theme.js +10 -4
  128. package/dist/utils/theme.js.map +1 -0
  129. package/dist/utils/widgetPermissions.d.ts +1 -0
  130. package/dist/utils/widgetPermissions.d.ts.map +1 -0
  131. package/dist/utils/widgetPermissions.js +13 -5
  132. package/dist/utils/widgetPermissions.js.map +1 -0
  133. package/dist/utils/widgetSession.d.ts +1 -0
  134. package/dist/utils/widgetSession.d.ts.map +1 -0
  135. package/dist/utils/widgetSession.js +9 -3
  136. package/dist/utils/widgetSession.js.map +1 -0
  137. package/package.json +3 -3
  138. package/src/components/CallScreen/index.tsx +23 -1
  139. package/src/components/ChatScreen/index.tsx +2 -1
  140. package/src/components/ChatWidget.tsx +314 -263
  141. package/src/components/ErrorBoundary/index.tsx +62 -0
  142. package/src/components/HomeScreen/index.tsx +0 -3
  143. package/src/components/MiniCallBar/index.tsx +150 -0
  144. package/src/hooks/useChat.ts +59 -12
  145. package/src/hooks/useSocket.ts +228 -0
  146. package/src/hooks/useWebRTC.ts +99 -64
  147. 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 { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
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
- import { HomeScreen } from './HomeScreen';
14
- import { UserListScreen } from './UserListScreen';
15
- import { ChatScreen } from './ChatScreen';
16
- import { RecentChatsScreen } from './RecentChatsScreen';
17
- import { TicketScreen } from './TicketScreen';
18
- import { TicketDetailScreen } from './TicketDetailScreen';
19
- import { TicketFormScreen } from './TicketFormScreen';
20
- import { BlockListScreen } from './BlockList';
21
- import { CallScreen } from './CallScreen';
22
- import { MaintenanceView } from './MaintenanceView';
23
- import { BottomTabs } from './Tabs/BottomTabs';
24
- import { ViewerBlockedScreen } from './ViewerBlockedScreen';
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 ? { primaryColor: data.widget.primaryColor, buttonLabel: data.widget.buttonLabel, buttonPosition: data.widget.buttonPosition } : undefined,
42
- localTheme
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
- /* Drawer open state */
46
- const [isOpen, setIsOpen] = useState(false);
47
- const [closing, setClosing] = useState(false); // for slide-out animation
48
-
49
- /* Navigation */
50
- const [activeTab, setActiveTab] = useState<BottomTab>('home');
51
- const [screen, setScreen] = useState<Screen>('home');
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
- /** Stagger list animation only when opening from home burger menu */
57
- const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
58
- /** Microphone, geolocation, and screen capture granted for this tab */
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
- /* App state */
62
- const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
63
- const [recentChats, setRecentChats] = useState<RecentChat[]>([]);
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 = viewer?.projectId?.trim();
85
+ const pid = viewer?.projectId?.trim();
72
86
  const devs = data.developers ?? [];
73
- const usr = pid ? (data.users ?? []).filter(u => u.project === pid) : (data.users ?? []);
74
- const all = [...devs, ...usr];
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
- /* Chat hook */
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, togglePause, reportChat, clearChat, setMessages,
96
- } = useChat();
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
- /* WebRTC hook */
99
- const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
196
+ useEffect(() => {
197
+ if (!callInProgress) setCallMinimized(false);
198
+ }, [callInProgress]);
100
199
 
101
- /* ── Drawer open/close with slide animation ───────────────────────────── */
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 = viewer?.projectId?.trim();
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
- setActiveTab('tickets');
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
- setActiveTab('tickets');
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
- togglePause();
403
+ _togglePause();
322
404
  if (activeUser) {
323
- setRecentChats(prev => prev.map(r => r.user.uid === activeUser.uid ? { ...r, isPaused: !isPaused } : r));
405
+ setRecentChats(prev => prev.map(r =>
406
+ r.user.uid === activeUser.uid ? { ...r, isPaused: !isPaused } : r
407
+ ));
324
408
  }
325
- }, [togglePause, activeUser, isPaused]);
409
+ }, [_togglePause, activeUser, isPaused]);
326
410
 
327
- /* ── Call ────────────────────────────────────────────────────────────── */
411
+ /* ── Call ───────────────────────────────────────────────────────────── */
328
412
  const handleStartCall = useCallback((withVideo: boolean) => {
329
413
  if (!activeUser) return;
330
- startCall(activeUser, withVideo);
414
+ _startCall(activeUser, withVideo);
331
415
  setScreen('call');
332
- }, [activeUser, startCall]);
416
+ }, [activeUser, _startCall]);
333
417
 
334
418
  const handleEndCall = useCallback(() => {
335
- endCall();
419
+ _endCall();
420
+ setCallMinimized(false);
336
421
  setScreen('chat');
337
- }, [endCall]);
422
+ }, [_endCall]);
338
423
 
339
- /* ── Derived ─────────────────────────────────────────────────────────── */
340
- const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
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 = viewer.uid;
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
- const primaryColor = theme.primaryColor;
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 = viewer?.projectId?.trim();
474
+ const pid = viewer?.projectId?.trim();
360
475
  const devs = data.developers ?? [];
361
476
  if (!pid) return [...devs, ...data.users];
362
- const usersInProject = data.users.filter(u => u.project === pid);
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 = widgetConfig?.viewerType === 'developer';
376
- const viewerUid = widgetConfig?.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
- if (viewerIsDev) return u.type === 'user';
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 = allUsers.filter(u => blockedUids.includes(u.uid));
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
- left: 0,
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
- {/* ── Floating Button (unread badge + tooltip when closed) ── */}
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
- style={{
513
- position: 'absolute',
514
- top: -8,
515
- right: -10,
516
- minWidth: 20,
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 (visual only — does not close widget on click) ── */}
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 / Slider ── */}
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
- {/* ── Loading state ── */}
646
+ {/* Loading */}
571
647
  {cfgLoading && (
572
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
573
- <div style={{
574
- width: 40, height: 40, borderRadius: '50%',
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
- {/* ── Error state ── */}
654
+ {/* Error */}
585
655
  {cfgError && !cfgLoading && (
586
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }}>
587
- <div style={{ fontSize: 40 }}>⚠️</div>
588
- <p style={{ fontWeight: 700, color: '#1a2332' }}>Could not load chat configuration</p>
589
- <p style={{ fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }}>{cfgError}</p>
590
- <button onClick={closeDrawer} style={{ padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }}>Close</button>
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
- {/* ── Main content ── */}
664
+ {/* Main content */}
595
665
  {!cfgLoading && !cfgError && widgetConfig && (
596
- <>
597
- {/* Resize + Close controls — hidden on blocked screen (Close is in-panel) */}
666
+ <ErrorBoundary primaryColor={primaryColor}>
667
+ {/* Close button */}
598
668
  {screen !== 'chat' && screen !== 'call' && !effectiveViewerBlocked && (
599
669
  <div style={{
600
- position: 'absolute', top: 12,
670
+ position:'absolute',top:12,
601
671
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
602
- left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
603
- zIndex: 20, display: 'flex', gap: 6,
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
- {/* ── MAINTENANCE ── */}
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: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
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={reportChat}
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
- const t = tickets.find(x => x.id === viewingTicketId);
738
- return t ? (
739
- <TicketDetailScreen
740
- ticket={t}
741
- config={widgetConfig}
742
- onBack={() => { setViewingTicketId(null); setScreen('tickets'); }}
743
- />
744
- ) : null;
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
- {/* ── Bottom Tabs (hidden during chat/call/user-list/block-list) ── */}
820
+ {/* Bottom Tabs */}
760
821
  {widgetConfig.status === 'ACTIVE' &&
761
- !effectiveViewerBlocked &&
762
- permissionsOk &&
763
- screen !== 'chat' &&
764
- screen !== 'call' &&
765
- screen !== 'user-list' &&
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: 26, height: 26, borderRadius: '50%',
789
- background: 'rgba(0,0,0,0.25)', border: 'none',
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>