ajaxter-chat 3.0.14 → 3.0.15

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/README.md CHANGED
Binary file
@@ -23,7 +23,7 @@ import { ViewerBlockedScreen } from './ViewerBlockedScreen';
23
23
  import { PermissionsGateScreen } from './PermissionsGateScreen';
24
24
  import { hasStoredPermissionsGrant } from '../utils/widgetPermissions';
25
25
  export const ChatWidget = ({ theme: localTheme, viewer }) => {
26
- var _a, _b, _c, _d, _e, _f;
26
+ var _a, _b, _c, _d, _e;
27
27
  /* SSR guard */
28
28
  const [mounted, setMounted] = useState(false);
29
29
  useEffect(() => { setMounted(true); }, []);
@@ -53,13 +53,14 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
53
53
  const [blockedUids, setBlockedUids] = useState((_b = data === null || data === void 0 ? void 0 : data.blockedUsers) !== null && _b !== void 0 ? _b : []);
54
54
  /* Sync remote data into local state once loaded */
55
55
  useEffect(() => {
56
- var _a, _b, _c;
56
+ var _a, _b, _c, _d;
57
57
  if (data) {
58
58
  setTickets(data.sampleTickets);
59
59
  setBlockedUids(data.blockedUsers);
60
60
  const pid = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim();
61
- const inProject = (u) => !pid || u.project === pid;
62
- const all = [...((_b = data.developers) !== null && _b !== void 0 ? _b : []), ...((_c = data.users) !== null && _c !== void 0 ? _c : [])].filter(inProject);
61
+ const devs = (_b = data.developers) !== null && _b !== void 0 ? _b : [];
62
+ const usr = pid ? ((_c = data.users) !== null && _c !== void 0 ? _c : []).filter(u => u.project === pid) : ((_d = data.users) !== null && _d !== void 0 ? _d : []);
63
+ const all = [...devs, ...usr];
63
64
  const recents = Object.entries(data.sampleChats).map(([uid, msgs]) => {
64
65
  const user = all.find(u => u.uid === uid);
65
66
  if (!user || msgs.length === 0)
@@ -118,12 +119,18 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
118
119
  }, [(_c = data === null || data === void 0 ? void 0 : data.widget) === null || _c === void 0 ? void 0 : _c.id]);
119
120
  const restoredRef = useRef(false);
120
121
  useEffect(() => {
121
- var _a, _b, _c, _d;
122
+ var _a, _b, _c, _d, _e, _f;
122
123
  if (!(data === null || data === void 0 ? void 0 : data.widget) || restoredRef.current)
123
124
  return;
124
125
  const w = data.widget;
125
126
  setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
126
- if (w.viewerBlocked) {
127
+ const uidForBlock = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : w.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
128
+ let viewerIsBlocked = w.viewerBlocked === true;
129
+ if (!viewerIsBlocked && uidForBlock) {
130
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uidForBlock);
131
+ viewerIsBlocked = (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
132
+ }
133
+ if (viewerIsBlocked) {
127
134
  clearChat();
128
135
  setScreen('home');
129
136
  setActiveTab('home');
@@ -136,10 +143,10 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
136
143
  setScreen(p.screen);
137
144
  setActiveTab(p.activeTab);
138
145
  setUserListCtx(p.userListCtx);
139
- setViewingTicketId((_a = p.viewingTicketId) !== null && _a !== void 0 ? _a : null);
140
- setChatReturnCtx((_b = p.chatReturnCtx) !== null && _b !== void 0 ? _b : 'conversation');
146
+ setViewingTicketId((_c = p.viewingTicketId) !== null && _c !== void 0 ? _c : null);
147
+ setChatReturnCtx((_d = p.chatReturnCtx) !== null && _d !== void 0 ? _d : 'conversation');
141
148
  if (p.activeUserUid) {
142
- const pid = (_c = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _c === void 0 ? void 0 : _c.trim();
149
+ const pid = (_e = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _e === void 0 ? void 0 : _e.trim();
143
150
  const pool = pid
144
151
  ? [...data.developers, ...data.users].filter(u => u.project === pid)
145
152
  : [...data.developers, ...data.users];
@@ -147,27 +154,36 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
147
154
  if (u) {
148
155
  const hist = Array.isArray(p.messages) && p.messages.length
149
156
  ? p.messages
150
- : ((_d = data.sampleChats[u.uid]) !== null && _d !== void 0 ? _d : []);
157
+ : ((_f = data.sampleChats[u.uid]) !== null && _f !== void 0 ? _f : []);
151
158
  selectUser(u, hist);
152
159
  }
153
160
  }
154
161
  }
155
162
  restoredRef.current = true;
156
- }, [data, selectUser, clearChat, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
163
+ }, [data, selectUser, clearChat, viewer === null || viewer === void 0 ? void 0 : viewer.projectId, viewer === null || viewer === void 0 ? void 0 : viewer.uid]);
157
164
  useEffect(() => {
158
- var _a;
159
- if (!((_a = data === null || data === void 0 ? void 0 : data.widget) === null || _a === void 0 ? void 0 : _a.viewerBlocked))
165
+ var _a, _b;
166
+ if (!(data === null || data === void 0 ? void 0 : data.widget))
167
+ return;
168
+ const w = data.widget;
169
+ const uid = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : w.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
170
+ let blocked = w.viewerBlocked === true;
171
+ if (!blocked && uid) {
172
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
173
+ blocked = (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
174
+ }
175
+ if (!blocked)
160
176
  return;
161
177
  clearChat();
162
178
  setScreen('home');
163
179
  setActiveTab('home');
164
180
  setViewingTicketId(null);
165
- }, [(_d = data === null || data === void 0 ? void 0 : data.widget) === null || _d === void 0 ? void 0 : _d.viewerBlocked, clearChat]);
181
+ }, [data === null || data === void 0 ? void 0 : data.widget, data === null || data === void 0 ? void 0 : data.developers, data === null || data === void 0 ? void 0 : data.users, viewer === null || viewer === void 0 ? void 0 : viewer.uid, clearChat]);
166
182
  useEffect(() => {
167
183
  if (!(data === null || data === void 0 ? void 0 : data.widget))
168
184
  return;
169
185
  persistWidgetState();
170
- }, [(_e = data === null || data === void 0 ? void 0 : data.widget) === null || _e === void 0 ? void 0 : _e.id, screen, activeTab, userListCtx, activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
186
+ }, [(_d = data === null || data === void 0 ? void 0 : data.widget) === null || _d === void 0 ? void 0 : _d.id, screen, activeTab, userListCtx, activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
171
187
  const incomingSoundRef = useRef(0);
172
188
  useEffect(() => {
173
189
  incomingSoundRef.current = messages.length;
@@ -318,16 +334,30 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
318
334
  return w;
319
335
  }, [data === null || data === void 0 ? void 0 : data.widget, viewer]);
320
336
  const primaryColor = theme.primaryColor;
337
+ /** All developers are listed; only end-`user` rows are filtered by `viewer.projectId`. */
321
338
  const allUsers = useMemo(() => {
322
- var _a;
339
+ var _a, _b;
323
340
  if (!data)
324
341
  return [];
325
342
  const pid = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim();
326
- const list = [...data.developers, ...data.users];
343
+ const devs = (_b = data.developers) !== null && _b !== void 0 ? _b : [];
327
344
  if (!pid)
328
- return list;
329
- return list.filter(u => u.project === pid);
345
+ return [...devs, ...data.users];
346
+ const usersInProject = data.users.filter(u => u.project === pid);
347
+ return [...devs, ...usersInProject];
330
348
  }, [data, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
349
+ const effectiveViewerBlocked = useMemo(() => {
350
+ var _a, _b;
351
+ if (!widgetConfig)
352
+ return false;
353
+ if (widgetConfig.viewerBlocked === true)
354
+ return true;
355
+ const uid = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : widgetConfig.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
356
+ if (!uid || !data)
357
+ return false;
358
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
359
+ return (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
360
+ }, [widgetConfig, viewer === null || viewer === void 0 ? void 0 : viewer.uid, data]);
331
361
  const viewerIsDev = (widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerType) === 'developer';
332
362
  const viewerUid = widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerUid;
333
363
  const filteredUsers = screen === 'user-list'
@@ -434,7 +464,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
434
464
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
435
465
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
436
466
  zIndex: 20, display: 'flex', gap: 6,
437
- }, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && widgetConfig.viewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey })), widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_f = widgetConfig.viewerType) !== null && _f !== void 0 ? _f : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
467
+ }, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
438
468
  setListEntranceAnimation(false);
439
469
  setViewingTicketId(id);
440
470
  setScreen('ticket-detail');
@@ -442,7 +472,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
442
472
  const t = tickets.find(x => x.id === viewingTicketId);
443
473
  return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
444
474
  })()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
445
- !widgetConfig.viewerBlocked &&
475
+ !effectiveViewerBlocked &&
446
476
  permissionsOk &&
447
477
  screen !== 'chat' &&
448
478
  screen !== 'call' &&
@@ -85,6 +85,11 @@ export interface ChatUser {
85
85
  avatar: string | null;
86
86
  status: OnlineStatus;
87
87
  designation: string;
88
+ /**
89
+ * When `true` for the row matching the current viewer (`viewerUid` / `viewer.uid`),
90
+ * the widget shows the spam/blocked screen (same as `widget.viewerBlocked`).
91
+ */
92
+ viewerBlocked?: boolean;
88
93
  }
89
94
  export interface ChatMessage {
90
95
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajaxter-chat",
3
- "version": "3.0.14",
3
+ "version": "3.0.15",
4
4
  "description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -62,7 +62,8 @@
62
62
  "type": "user",
63
63
  "avatar": null,
64
64
  "status": "online",
65
- "designation": "Product Manager"
65
+ "designation": "Product Manager",
66
+ "viewerBlocked": false
66
67
  },
67
68
  {
68
69
  "uid": "usr_002",
@@ -73,7 +74,8 @@
73
74
  "type": "user",
74
75
  "avatar": null,
75
76
  "status": "away",
76
- "designation": "Business Analyst"
77
+ "designation": "Business Analyst",
78
+ "viewerBlocked": true
77
79
  },
78
80
  {
79
81
  "uid": "usr_003",
@@ -69,8 +69,9 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
69
69
  setTickets(data.sampleTickets);
70
70
  setBlockedUids(data.blockedUsers);
71
71
  const pid = viewer?.projectId?.trim();
72
- const inProject = (u: ChatUser) => !pid || u.project === pid;
73
- const all = [...(data.developers ?? []), ...(data.users ?? [])].filter(inProject);
72
+ const devs = data.developers ?? [];
73
+ const usr = pid ? (data.users ?? []).filter(u => u.project === pid) : (data.users ?? []);
74
+ const all = [...devs, ...usr];
74
75
  const recents: RecentChat[] = Object.entries(data.sampleChats).map(([uid, msgs]) => {
75
76
  const user = all.find(u => u.uid === uid);
76
77
  if (!user || msgs.length === 0) return null;
@@ -137,7 +138,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
137
138
  if (!data?.widget || restoredRef.current) return;
138
139
  const w = data.widget;
139
140
  setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
140
- if (w.viewerBlocked) {
141
+ const uidForBlock = (viewer?.uid ?? w.viewerUid)?.trim();
142
+ let viewerIsBlocked = w.viewerBlocked === true;
143
+ if (!viewerIsBlocked && uidForBlock) {
144
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uidForBlock);
145
+ viewerIsBlocked = rec?.viewerBlocked === true;
146
+ }
147
+ if (viewerIsBlocked) {
141
148
  clearChat();
142
149
  setScreen('home');
143
150
  setActiveTab('home');
@@ -167,15 +174,23 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
167
174
  }
168
175
  }
169
176
  restoredRef.current = true;
170
- }, [data, selectUser, clearChat, viewer?.projectId]);
177
+ }, [data, selectUser, clearChat, viewer?.projectId, viewer?.uid]);
171
178
 
172
179
  useEffect(() => {
173
- if (!data?.widget?.viewerBlocked) return;
180
+ if (!data?.widget) return;
181
+ const w = data.widget;
182
+ const uid = (viewer?.uid ?? w.viewerUid)?.trim();
183
+ let blocked = w.viewerBlocked === true;
184
+ if (!blocked && uid) {
185
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
186
+ blocked = rec?.viewerBlocked === true;
187
+ }
188
+ if (!blocked) return;
174
189
  clearChat();
175
190
  setScreen('home');
176
191
  setActiveTab('home');
177
192
  setViewingTicketId(null);
178
- }, [data?.widget?.viewerBlocked, clearChat]);
193
+ }, [data?.widget, data?.developers, data?.users, viewer?.uid, clearChat]);
179
194
 
180
195
  useEffect(() => {
181
196
  if (!data?.widget) return;
@@ -338,14 +353,25 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
338
353
 
339
354
  const primaryColor = theme.primaryColor;
340
355
 
356
+ /** All developers are listed; only end-`user` rows are filtered by `viewer.projectId`. */
341
357
  const allUsers = useMemo(() => {
342
358
  if (!data) return [];
343
359
  const pid = viewer?.projectId?.trim();
344
- const list = [...data.developers, ...data.users];
345
- if (!pid) return list;
346
- return list.filter(u => u.project === pid);
360
+ const devs = data.developers ?? [];
361
+ if (!pid) return [...devs, ...data.users];
362
+ const usersInProject = data.users.filter(u => u.project === pid);
363
+ return [...devs, ...usersInProject];
347
364
  }, [data, viewer?.projectId]);
348
365
 
366
+ const effectiveViewerBlocked = useMemo(() => {
367
+ if (!widgetConfig) return false;
368
+ if (widgetConfig.viewerBlocked === true) return true;
369
+ const uid = (viewer?.uid ?? widgetConfig.viewerUid)?.trim();
370
+ if (!uid || !data) return false;
371
+ const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
372
+ return rec?.viewerBlocked === true;
373
+ }, [widgetConfig, viewer?.uid, data]);
374
+
349
375
  const viewerIsDev = widgetConfig?.viewerType === 'developer';
350
376
  const viewerUid = widgetConfig?.viewerUid;
351
377
 
@@ -567,12 +593,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
567
593
  )}
568
594
 
569
595
  {/* ── ACTIVE: viewer spam-blocked (no chat/tickets UI) ── */}
570
- {widgetConfig.status === 'ACTIVE' && widgetConfig.viewerBlocked && (
596
+ {widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (
571
597
  <ViewerBlockedScreen config={widgetConfig} apiKey={apiKey} />
572
598
  )}
573
599
 
574
600
  {/* ── ACTIVE: microphone, location, screen share required ── */}
575
- {widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && !permissionsOk && (
601
+ {widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (
576
602
  <PermissionsGateScreen
577
603
  primaryColor={primaryColor}
578
604
  widgetId={widgetConfig.id}
@@ -581,7 +607,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
581
607
  )}
582
608
 
583
609
  {/* ── ACTIVE ── */}
584
- {widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && permissionsOk && (
610
+ {widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (
585
611
  <div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
586
612
 
587
613
  {screen === 'home' && (
@@ -699,7 +725,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
699
725
 
700
726
  {/* ── Bottom Tabs (hidden during chat/call/user-list/block-list) ── */}
701
727
  {widgetConfig.status === 'ACTIVE' &&
702
- !widgetConfig.viewerBlocked &&
728
+ !effectiveViewerBlocked &&
703
729
  permissionsOk &&
704
730
  screen !== 'chat' &&
705
731
  screen !== 'call' &&
@@ -100,6 +100,11 @@ export interface ChatUser {
100
100
  avatar: string | null;
101
101
  status: OnlineStatus;
102
102
  designation: string;
103
+ /**
104
+ * When `true` for the row matching the current viewer (`viewerUid` / `viewer.uid`),
105
+ * the widget shows the spam/blocked screen (same as `widget.viewerBlocked`).
106
+ */
107
+ viewerBlocked?: boolean;
103
108
  }
104
109
 
105
110
  // ─── Message ────────────────────────────────────────────────────────────────