ajaxter-chat 3.0.9 → 3.0.11

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.
@@ -4,24 +4,28 @@ import { avatarColor, initials, formatTime, formatDate, generateTranscript, down
4
4
  import { shortAttachmentLabel } from '../../utils/fileName';
5
5
  import { shouldShowPrivacyNotice, dismissPrivacyNotice } from '../../utils/privacyConsent';
6
6
  import { EmojiPicker } from '../EmojiPicker';
7
- import { SlideNavMenu } from '../SlideNavMenu';
8
7
  export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported, isBlocked, onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction, otherDevelopers = [], onTransferToDeveloper, messageSoundEnabled = true, onToggleMessageSound, }) => {
9
- var _a;
10
8
  const [text, setText] = useState('');
11
9
  const [showEmoji, setShowEmoji] = useState(false);
12
10
  const [showMenu, setShowMenu] = useState(false);
13
- const [slideMenuOpen, setSlideMenuOpen] = useState(false);
14
11
  const [transferOpen, setTransferOpen] = useState(false);
15
12
  const [isRecording, setIsRecording] = useState(false);
16
13
  const [recordSec, setRecordSec] = useState(0);
17
14
  const [showConfirm, setShowConfirm] = useState(null);
18
15
  const [showPrivacy, setShowPrivacy] = useState(false);
16
+ const [pendingAttach, setPendingAttach] = useState(null);
17
+ const [waveBars, setWaveBars] = useState(() => Array(24).fill(0.08));
19
18
  const endRef = useRef(null);
20
19
  const inputRef = useRef(null);
21
20
  const fileRef = useRef(null);
22
21
  const recordTimer = useRef(null);
23
22
  const mediaRecorder = useRef(null);
24
23
  const recordChunks = useRef([]);
24
+ const discardRecordingRef = useRef(false);
25
+ const waveStreamRef = useRef(null);
26
+ const audioCtxRef = useRef(null);
27
+ const analyserRef = useRef(null);
28
+ const waveRafRef = useRef(0);
25
29
  useEffect(() => { var _a; (_a = endRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
26
30
  const privacyEnabled = config.showPrivacyNotice !== false;
27
31
  useEffect(() => {
@@ -41,63 +45,189 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
41
45
  dismissPrivacyNotice(config.id);
42
46
  setShowPrivacy(false);
43
47
  }, [config.id]);
48
+ const clearPendingAttach = useCallback((revoke) => {
49
+ setPendingAttach(prev => {
50
+ if (prev && revoke)
51
+ URL.revokeObjectURL(prev.url);
52
+ return null;
53
+ });
54
+ }, []);
44
55
  const handleSend = useCallback(() => {
45
- var _a;
46
- if (!text.trim() || isPaused || isBlocked)
56
+ var _a, _b;
57
+ if (isPaused || isBlocked)
58
+ return;
59
+ if (pendingAttach) {
60
+ const { file, url } = pendingAttach;
61
+ const body = text.trim();
62
+ onSend(body || ' ', 'attachment', {
63
+ attachmentName: file.name,
64
+ attachmentSize: `${(file.size / 1024).toFixed(1)} KB`,
65
+ attachmentUrl: url,
66
+ attachmentMime: file.type,
67
+ });
68
+ setPendingAttach(null);
69
+ setText('');
70
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
71
+ return;
72
+ }
73
+ if (!text.trim())
47
74
  return;
48
75
  onSend(text.trim());
49
76
  setText('');
50
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
51
- }, [text, isPaused, isBlocked, onSend]);
77
+ (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus();
78
+ }, [text, isPaused, isBlocked, onSend, pendingAttach]);
52
79
  const handleKey = (e) => {
53
80
  if (e.key === 'Enter' && !e.shiftKey) {
54
81
  e.preventDefault();
55
82
  handleSend();
56
83
  }
57
84
  };
85
+ const recordSecRef = useRef(0);
86
+ const stopWaveLoop = useCallback(() => {
87
+ var _a;
88
+ if (waveRafRef.current) {
89
+ cancelAnimationFrame(waveRafRef.current);
90
+ waveRafRef.current = 0;
91
+ }
92
+ analyserRef.current = null;
93
+ void ((_a = audioCtxRef.current) === null || _a === void 0 ? void 0 : _a.close());
94
+ audioCtxRef.current = null;
95
+ waveStreamRef.current = null;
96
+ setWaveBars(Array(24).fill(0.08));
97
+ }, []);
58
98
  const startRecording = async () => {
59
99
  if (isPaused || isBlocked)
60
100
  return;
61
101
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
102
+ waveStreamRef.current = stream;
103
+ discardRecordingRef.current = false;
104
+ setRecordSec(0);
105
+ recordSecRef.current = 0;
106
+ try {
107
+ const audioCtx = new AudioContext();
108
+ await audioCtx.resume();
109
+ audioCtxRef.current = audioCtx;
110
+ const source = audioCtx.createMediaStreamSource(stream);
111
+ const analyser = audioCtx.createAnalyser();
112
+ analyser.fftSize = 128;
113
+ analyser.smoothingTimeConstant = 0.65;
114
+ source.connect(analyser);
115
+ analyserRef.current = analyser;
116
+ const data = new Uint8Array(analyser.frequencyBinCount);
117
+ const tick = () => {
118
+ const a = analyserRef.current;
119
+ if (!a)
120
+ return;
121
+ a.getByteFrequencyData(data);
122
+ const bars = [];
123
+ const step = Math.max(1, Math.floor(data.length / 24));
124
+ for (let i = 0; i < 24; i++) {
125
+ const v = data[Math.min(i * step, data.length - 1)] / 255;
126
+ bars.push(Math.max(0.08, v));
127
+ }
128
+ setWaveBars(bars);
129
+ waveRafRef.current = requestAnimationFrame(tick);
130
+ };
131
+ waveRafRef.current = requestAnimationFrame(tick);
132
+ }
133
+ catch (_a) {
134
+ /* optional waveform */
135
+ }
62
136
  recordChunks.current = [];
63
137
  const mr = new MediaRecorder(stream);
64
138
  mediaRecorder.current = mr;
65
139
  mr.ondataavailable = e => { if (e.data.size)
66
140
  recordChunks.current.push(e.data); };
67
141
  mr.onstop = () => {
142
+ stopWaveLoop();
68
143
  stream.getTracks().forEach(t => t.stop());
69
144
  const chunks = recordChunks.current;
145
+ if (discardRecordingRef.current) {
146
+ discardRecordingRef.current = false;
147
+ setRecordSec(0);
148
+ recordSecRef.current = 0;
149
+ return;
150
+ }
70
151
  if (!chunks.length) {
71
152
  setRecordSec(0);
153
+ recordSecRef.current = 0;
72
154
  return;
73
155
  }
74
156
  const blob = new Blob(chunks, { type: chunks[0] instanceof Blob ? chunks[0].type : 'audio/webm' });
75
157
  const voiceUrl = URL.createObjectURL(blob);
76
- const dur = Math.max(1, recordSec);
158
+ const dur = Math.max(1, recordSecRef.current);
77
159
  onSend('Voice message', 'voice', { voiceDuration: dur, voiceUrl });
78
160
  setRecordSec(0);
161
+ recordSecRef.current = 0;
79
162
  };
80
163
  mr.start(200);
81
164
  setIsRecording(true);
82
- recordTimer.current = setInterval(() => setRecordSec(s => s + 1), 1000);
165
+ recordTimer.current = setInterval(() => {
166
+ setRecordSec(s => {
167
+ const n = s + 1;
168
+ recordSecRef.current = n;
169
+ return n;
170
+ });
171
+ }, 1000);
83
172
  };
84
- const stopRecording = () => {
173
+ const cancelRecording = () => {
85
174
  var _a;
175
+ if (!isRecording)
176
+ return;
177
+ discardRecordingRef.current = true;
178
+ if (recordTimer.current) {
179
+ clearInterval(recordTimer.current);
180
+ recordTimer.current = null;
181
+ }
86
182
  (_a = mediaRecorder.current) === null || _a === void 0 ? void 0 : _a.stop();
87
- if (recordTimer.current)
183
+ setIsRecording(false);
184
+ };
185
+ const stopRecordingSend = () => {
186
+ var _a;
187
+ if (!isRecording)
188
+ return;
189
+ discardRecordingRef.current = false;
190
+ if (recordTimer.current) {
88
191
  clearInterval(recordTimer.current);
192
+ recordTimer.current = null;
193
+ }
194
+ (_a = mediaRecorder.current) === null || _a === void 0 ? void 0 : _a.stop();
89
195
  setIsRecording(false);
90
196
  };
197
+ const handlePaste = (e) => {
198
+ var _a;
199
+ if (isPaused || isBlocked || !config.allowAttachment)
200
+ return;
201
+ const items = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.items;
202
+ if (!(items === null || items === void 0 ? void 0 : items.length))
203
+ return;
204
+ for (let i = 0; i < items.length; i++) {
205
+ const item = items[i];
206
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
207
+ const f = item.getAsFile();
208
+ if (f) {
209
+ e.preventDefault();
210
+ const url = URL.createObjectURL(f);
211
+ setPendingAttach(prev => {
212
+ if (prev)
213
+ URL.revokeObjectURL(prev.url);
214
+ return { file: f, url };
215
+ });
216
+ return;
217
+ }
218
+ }
219
+ }
220
+ };
91
221
  const handleFileChange = (e) => {
92
222
  var _a;
93
223
  const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
94
224
  if (!file || isPaused || isBlocked)
95
225
  return;
96
- const attachmentUrl = URL.createObjectURL(file);
97
- onSend(file.name, 'attachment', {
98
- attachmentName: file.name,
99
- attachmentSize: `${(file.size / 1024).toFixed(1)} KB`,
100
- attachmentUrl,
226
+ const url = URL.createObjectURL(file);
227
+ setPendingAttach(prev => {
228
+ if (prev)
229
+ URL.revokeObjectURL(prev.url);
230
+ return { file, url };
101
231
  });
102
232
  e.target.value = '';
103
233
  };
@@ -123,10 +253,10 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
123
253
  const headerRole = viewerIsDev
124
254
  ? (activeUser.type === 'user' ? 'Customer' : 'Developer')
125
255
  : 'Support';
126
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsx(SlideNavMenu, { open: slideMenuOpen, onClose: () => setSlideMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_a = config.viewerType) !== null && _a !== void 0 ? _a : 'user', onSelect: onNavAction, onBackHome: onBack }), _jsxs("div", { style: {
256
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsxs("div", { style: {
127
257
  background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
128
258
  padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
129
- }, children: [_jsx("button", { type: "button", onClick: () => setSlideMenuOpen(true), style: hdrBtn, "aria-label": "Open menu", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" })] }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (_jsxs("label", { style: {
259
+ }, children: [_jsx("button", { type: "button", onClick: onBack, style: hdrBtn, "aria-label": "Back", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (_jsxs("label", { style: {
130
260
  display: 'flex',
131
261
  alignItems: 'center',
132
262
  gap: 6,
@@ -151,7 +281,7 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
151
281
  borderRadius: '50%',
152
282
  background: '#fff',
153
283
  transition: 'left 0.15s ease',
154
- } }) })] })), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
284
+ } }) })] })), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [navEntriesForChat(config.chatType, viewerIsDev).map(item => (_jsx(MenuItem, { icon: item.icon, label: item.label, onClick: () => { setShowMenu(false); onNavAction(item.key); } }, item.key))), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
155
285
  position: 'relative',
156
286
  marginBottom: 10,
157
287
  padding: '12px 36px 12px 12px',
@@ -174,12 +304,90 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
174
304
  justifyContent: 'center',
175
305
  padding: 0,
176
306
  lineHeight: 1,
177
- }, children: _jsx("span", { style: { fontSize: 14, color: '#475569', fontWeight: 700 }, children: "\u00D7" }) }), _jsxs("p", { style: { margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }, children: ["By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with", ' ', config.privacyPolicyUrl ? (_jsx("a", { href: config.privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", style: { color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })) : (_jsx("span", { style: { textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })), "."] })] })), showEmoji && config.allowEmoji && (_jsx(EmojiPicker, { primaryColor: config.primaryColor, onSelect: e => setText(t => t + e), onClose: () => setShowEmoji(false) })), isRecording && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, padding: '8px 12px', background: '#fee2e2', borderRadius: 10 }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: '50%', background: '#ef4444', display: 'inline-block', animation: 'cw-pulse 1s infinite' } }), _jsxs("span", { style: { fontSize: 13, color: '#991b1b', fontWeight: 600 }, children: ["Recording ", recordSec, "s"] }), _jsx("button", { type: "button", onClick: stopRecording, style: { marginLeft: 'auto', background: '#ef4444', color: '#fff', border: 'none', borderRadius: 6, padding: '4px 12px', fontSize: 12, cursor: 'pointer', fontWeight: 600 }, children: "Stop & send" })] })), _jsxs("div", { style: {
307
+ }, children: _jsx("span", { style: { fontSize: 14, color: '#475569', fontWeight: 700 }, children: "\u00D7" }) }), _jsxs("p", { style: { margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }, children: ["By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with", ' ', config.privacyPolicyUrl ? (_jsx("a", { href: config.privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", style: { color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })) : (_jsx("span", { style: { textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })), "."] })] })), showEmoji && config.allowEmoji && (_jsx(EmojiPicker, { primaryColor: config.primaryColor, onSelect: e => setText(t => t + e), onClose: () => setShowEmoji(false) })), isRecording && (_jsxs("div", { style: {
308
+ marginBottom: 10,
309
+ padding: '12px 12px 14px',
310
+ background: '#fff',
311
+ borderRadius: 14,
312
+ border: '1px solid #e8ecf1',
313
+ boxShadow: '0 1px 4px rgba(15,23,42,0.06)',
314
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 10 }, children: [_jsx("button", { type: "button", onClick: cancelRecording, title: "Discard recording", "aria-label": "Discard recording", style: {
315
+ background: 'none',
316
+ border: 'none',
317
+ cursor: 'pointer',
318
+ padding: 6,
319
+ lineHeight: 0,
320
+ flexShrink: 0,
321
+ }, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6", stroke: "#ef4444", strokeWidth: "2", strokeLinecap: "round" }), _jsx("path", { d: "M10 11v6M14 11v6", stroke: "#ef4444", strokeWidth: "2", strokeLinecap: "round" })] }) }), _jsx("div", { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 44, flex: 1, justifyContent: 'flex-end', minWidth: 0 }, children: waveBars.map((h, i) => (_jsx("span", { style: {
322
+ width: 3,
323
+ borderRadius: 2,
324
+ background: '#cbd5e1',
325
+ height: `${8 + h * 36}px`,
326
+ transition: 'height 0.05s ease-out',
327
+ } }, i))) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }, children: [_jsx("div", { style: { flex: 1 } }), _jsx("div", { style: {
328
+ background: '#ef4444',
329
+ color: '#fff',
330
+ fontWeight: 700,
331
+ fontSize: 13,
332
+ padding: '6px 14px',
333
+ borderRadius: 999,
334
+ minWidth: 52,
335
+ textAlign: 'center',
336
+ }, children: fmtTime(recordSec) }), _jsx("button", { type: "button", onClick: stopRecordingSend, title: "Send voice message", "aria-label": "Send voice message", style: {
337
+ width: 44,
338
+ height: 44,
339
+ borderRadius: '50%',
340
+ border: 'none',
341
+ background: config.primaryColor,
342
+ cursor: 'pointer',
343
+ display: 'flex',
344
+ alignItems: 'center',
345
+ justifyContent: 'center',
346
+ flexShrink: 0,
347
+ boxShadow: `0 4px 14px ${config.primaryColor}55`,
348
+ }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })), _jsxs("div", { style: {
178
349
  border: `1.5px solid ${isPaused || isBlocked ? '#e5e7eb' : '#bfdbfe'}`,
179
350
  borderRadius: 16,
180
351
  padding: '10px 12px 8px',
181
352
  background: isPaused || isBlocked ? '#f9fafb' : '#fff',
182
- }, children: [_jsx("textarea", { ref: inputRef, value: text, onChange: e => setText(e.target.value), onKeyDown: handleKey, placeholder: isPaused || isBlocked ? 'Chat is unavailable' : 'Compose your message…', disabled: isPaused || isBlocked || isRecording, rows: 2, style: {
353
+ }, children: [pendingAttach && (_jsxs("div", { style: {
354
+ display: 'flex',
355
+ alignItems: 'center',
356
+ gap: 10,
357
+ marginBottom: 10,
358
+ padding: '8px 10px',
359
+ borderRadius: 10,
360
+ background: '#f8fafc',
361
+ border: '1px solid #fecaca',
362
+ position: 'relative',
363
+ }, children: [_jsx("div", { style: {
364
+ width: 40,
365
+ height: 40,
366
+ borderRadius: 8,
367
+ background: config.primaryColor,
368
+ display: 'flex',
369
+ alignItems: 'center',
370
+ justifyContent: 'center',
371
+ flexShrink: 0,
372
+ }, children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 13, color: '#1a2332', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: pendingAttach.file.name, children: pendingAttach.file.name }), _jsx("div", { style: { fontSize: 11, color: '#94a3b8', fontWeight: 600, textTransform: 'uppercase' }, children: (pendingAttach.file.type.split('/')[1] || 'file').slice(0, 8) })] }), _jsx("button", { type: "button", onClick: () => clearPendingAttach(true), title: "Remove attachment", "aria-label": "Remove attachment", style: {
373
+ position: 'absolute',
374
+ top: 6,
375
+ right: 6,
376
+ width: 22,
377
+ height: 22,
378
+ borderRadius: '50%',
379
+ border: 'none',
380
+ background: '#ef4444',
381
+ color: '#fff',
382
+ cursor: 'pointer',
383
+ fontSize: 15,
384
+ fontWeight: 700,
385
+ lineHeight: 1,
386
+ display: 'flex',
387
+ alignItems: 'center',
388
+ justifyContent: 'center',
389
+ padding: 0,
390
+ }, children: "\u00D7" })] })), _jsx("textarea", { ref: inputRef, value: text, onChange: e => setText(e.target.value), onKeyDown: handleKey, onPaste: handlePaste, placeholder: isPaused || isBlocked ? 'Chat is unavailable' : 'Compose your message…', disabled: isPaused || isBlocked || isRecording, rows: 2, style: {
183
391
  width: '100%',
184
392
  resize: 'none',
185
393
  border: 'none',
@@ -192,19 +400,19 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
192
400
  overflowY: 'auto',
193
401
  fontFamily: 'inherit',
194
402
  marginBottom: 8,
195
- } }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [config.allowEmoji && (_jsx(ActionBtn, { onClick: () => setShowEmoji(v => !v), title: "Emoji", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "#94a3b8", strokeWidth: "1.8" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("circle", { cx: "9", cy: "9", r: "1", fill: "#94a3b8" }), _jsx("circle", { cx: "15", cy: "9", r: "1", fill: "#94a3b8" })] }) })), config.allowAttachment && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileRef, type: "file", style: { display: 'none' }, onChange: handleFileChange }), _jsx(ActionBtn, { onClick: () => { var _a; return (_a = fileRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: "Attach file", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })), config.allowVoiceMessage && !isRecording && (_jsx(ActionBtn, { onClick: startRecording, title: "Voice message", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("path", { d: "M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" })] }) }))] }), _jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isPaused || isBlocked || isRecording, style: {
403
+ } }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [config.allowEmoji && (_jsx(ActionBtn, { onClick: () => setShowEmoji(v => !v), title: "Emoji", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "#94a3b8", strokeWidth: "1.8" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("circle", { cx: "9", cy: "9", r: "1", fill: "#94a3b8" }), _jsx("circle", { cx: "15", cy: "9", r: "1", fill: "#94a3b8" })] }) })), config.allowAttachment && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileRef, type: "file", style: { display: 'none' }, onChange: handleFileChange }), _jsx(ActionBtn, { onClick: () => { var _a; return (_a = fileRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: "Attach file", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })), config.allowVoiceMessage && !isRecording && (_jsx(ActionBtn, { onClick: startRecording, title: "Voice message", children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" }), _jsx("path", { d: "M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8", stroke: "#94a3b8", strokeWidth: "1.8", strokeLinecap: "round" })] }) }))] }), _jsx("button", { type: "button", onClick: handleSend, disabled: (!text.trim() && !pendingAttach) || isPaused || isBlocked || isRecording, style: {
196
404
  width: 36,
197
405
  height: 36,
198
406
  borderRadius: '50%',
199
- backgroundColor: text.trim() && !isPaused && !isBlocked ? config.primaryColor : '#e2e8f0',
407
+ backgroundColor: (text.trim() || pendingAttach) && !isPaused && !isBlocked ? config.primaryColor : '#e2e8f0',
200
408
  border: 'none',
201
- cursor: text.trim() && !isPaused && !isBlocked ? 'pointer' : 'default',
409
+ cursor: (text.trim() || pendingAttach) && !isPaused && !isBlocked ? 'pointer' : 'default',
202
410
  display: 'flex',
203
411
  alignItems: 'center',
204
412
  justifyContent: 'center',
205
413
  flexShrink: 0,
206
414
  transition: 'background 0.15s',
207
- }, title: "Send", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: text.trim() && !isPaused && !isBlocked ? '#fff' : '#94a3b8', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }), (config.footerPoweredBy || config.branch) && (_jsxs("p", { style: { margin: '10px 0 0', textAlign: 'center', fontSize: 12, color: '#94a3b8' }, children: [config.footerPoweredBy, config.footerPoweredBy && config.branch ? ' · ' : '', config.branch && _jsx("span", { style: { fontWeight: 600, color: '#64748b' }, children: config.branch })] }))] }), transferOpen && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx("div", { style: {
415
+ }, title: "Send", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: (text.trim() || pendingAttach) && !isPaused && !isBlocked ? '#fff' : '#94a3b8', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }), (config.footerPoweredBy || config.branch) && (_jsxs("p", { style: { margin: '10px 0 0', textAlign: 'center', fontSize: 12, color: '#94a3b8' }, children: [config.footerPoweredBy, config.footerPoweredBy && config.branch ? ' · ' : '', config.branch && _jsx("span", { style: { fontWeight: 600, color: '#64748b' }, children: config.branch })] }))] }), transferOpen && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx("div", { style: {
208
416
  position: 'absolute',
209
417
  inset: 0,
210
418
  background: 'rgba(0,0,0,0.45)',
@@ -312,21 +520,24 @@ const VoiceRow = ({ msg, isMe, primaryColor }) => {
312
520
  }, "aria-label": playing ? 'Pause' : 'Play', children: playing ? (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("rect", { x: "6", y: "4", width: "4", height: "16" }), _jsx("rect", { x: "14", y: "4", width: "4", height: "16" })] })) : (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M8 5v14l11-7z" }) })) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { height: 4, borderRadius: 2, background: isMe ? 'rgba(255,255,255,0.35)' : '#e2e8f0', overflow: 'hidden' }, children: _jsx("div", { style: { width: `${pct}%`, height: '100%', background: isMe ? '#fff' : primaryColor, borderRadius: 2, transition: 'width 0.1s linear' } }) }), _jsx("div", { style: { fontSize: 11, marginTop: 4, opacity: 0.9 }, children: timeLabel })] })] }));
313
521
  };
314
522
  const AttachmentRow = ({ msg, isMe, primaryColor }) => {
315
- var _a;
523
+ var _a, _b;
316
524
  const name = (_a = msg.attachmentName) !== null && _a !== void 0 ? _a : 'File';
317
525
  const href = msg.attachmentUrl;
318
526
  const label = shortAttachmentLabel(name, 10);
319
- return (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }, children: [_jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: isMe ? '#fff' : '#334155', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [href ? (_jsxs("a", { href: href, download: name, title: name, style: {
320
- fontWeight: 700,
321
- fontSize: 14,
322
- wordBreak: 'break-word',
323
- color: isMe ? '#fff' : primaryColor,
324
- textDecoration: 'underline',
325
- }, children: ["[", label, "]"] })) : (_jsxs("div", { style: { fontWeight: 700, fontSize: 14, wordBreak: 'break-word' }, title: name, children: ["[", label, "]"] })), msg.attachmentSize && _jsx("div", { style: { fontSize: 11, opacity: 0.8 }, children: msg.attachmentSize })] })] }));
527
+ const mime = (_b = msg.attachmentMime) !== null && _b !== void 0 ? _b : '';
528
+ const isImage = mime.startsWith('image/') || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(name);
529
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: 8, flexWrap: 'wrap' }, children: [isImage && href && (_jsx("a", { href: href, download: name, title: name, style: { alignSelf: 'flex-start', lineHeight: 0 }, children: _jsx("img", { src: href, alt: "", style: { maxWidth: 220, maxHeight: 200, borderRadius: 10, objectFit: 'cover', display: 'block' } }) })), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 10 }, children: [!isImage && (_jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66L9.41 17.41a2 2 0 01-2.83-2.83l8.49-8.48", stroke: isMe ? '#fff' : '#334155', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [href ? (_jsxs("a", { href: href, download: name, title: name, style: {
530
+ fontWeight: 700,
531
+ fontSize: 14,
532
+ wordBreak: 'break-word',
533
+ color: isMe ? '#fff' : primaryColor,
534
+ textDecoration: 'underline',
535
+ }, children: ["[", label, "]"] })) : (_jsxs("div", { style: { fontWeight: 700, fontSize: 14, wordBreak: 'break-word' }, title: name, children: ["[", label, "]"] })), msg.attachmentSize && _jsx("div", { style: { fontSize: 11, opacity: 0.8 }, children: msg.attachmentSize })] })] })] }));
326
536
  };
327
537
  const Bubble = ({ msg, primaryColor }) => {
328
538
  const isMe = msg.senderId === 'me';
329
- const content = msg.type === 'voice' ? (_jsx(VoiceRow, { msg: msg, isMe: isMe, primaryColor: primaryColor })) : msg.type === 'attachment' ? (_jsx(AttachmentRow, { msg: msg, isMe: isMe, primaryColor: primaryColor })) : (_jsx("span", { children: msg.text }));
539
+ const caption = msg.text.trim();
540
+ const content = msg.type === 'voice' ? (_jsx(VoiceRow, { msg: msg, isMe: isMe, primaryColor: primaryColor })) : msg.type === 'attachment' ? (_jsxs(_Fragment, { children: [_jsx(AttachmentRow, { msg: msg, isMe: isMe, primaryColor: primaryColor }), caption && caption !== ' ' && (_jsx("div", { style: { marginTop: 6, fontSize: 14, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }, children: msg.text }))] })) : (_jsx("span", { children: msg.text }));
330
541
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: isMe ? 'flex-end' : 'flex-start', gap: 3 }, children: [_jsx("div", { style: {
331
542
  maxWidth: '85%', padding: '10px 13px',
332
543
  borderRadius: isMe ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
@@ -354,6 +565,17 @@ const hdrBtn = {
354
565
  width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
355
566
  cursor: 'pointer', flexShrink: 0,
356
567
  };
568
+ function navEntriesForChat(chatType, isStaff) {
569
+ const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
570
+ const showChat = chatType === 'CHAT' || chatType === 'BOTH';
571
+ const items = [];
572
+ if (showSupport)
573
+ items.push({ key: 'support', icon: '🛠', label: isStaff ? 'Provide Support' : 'Need Support' });
574
+ if (showChat)
575
+ items.push({ key: 'conversation', icon: '💬', label: isStaff ? 'Chat with developer' : 'New Conversation' });
576
+ items.push({ key: 'ticket', icon: '🎫', label: 'Raise ticket' });
577
+ return items;
578
+ }
357
579
  function groupByDate(messages) {
358
580
  const map = new Map();
359
581
  messages.forEach(m => {
@@ -40,6 +40,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
40
40
  const [chatReturnCtx, setChatReturnCtx] = useState('conversation');
41
41
  const [viewingTicketId, setViewingTicketId] = useState(null);
42
42
  const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
43
+ /** Stagger list animation only when opening from home burger menu */
44
+ const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
43
45
  /* App state */
44
46
  const [tickets, setTickets] = useState((_a = data === null || data === void 0 ? void 0 : data.sampleTickets) !== null && _a !== void 0 ? _a : []);
45
47
  const [recentChats, setRecentChats] = useState([]);
@@ -156,7 +158,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
156
158
  setMessageSoundEnabledState(enabled);
157
159
  }, [data === null || data === void 0 ? void 0 : data.widget]);
158
160
  /* ── Navigation ──────────────────────────────────────────────────────── */
159
- const handleCardClick = useCallback((ctx) => {
161
+ const handleCardClick = useCallback((ctx, options) => {
162
+ setListEntranceAnimation(!!(options === null || options === void 0 ? void 0 : options.fromMenu));
160
163
  if (ctx === 'ticket') {
161
164
  setActiveTab('tickets');
162
165
  setScreen('tickets');
@@ -167,6 +170,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
167
170
  }
168
171
  }, []);
169
172
  const handleNavFromMenu = useCallback((ctx) => {
173
+ setListEntranceAnimation(false);
170
174
  clearChat();
171
175
  if (ctx === 'ticket') {
172
176
  setActiveTab('tickets');
@@ -184,6 +188,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
184
188
  }, []);
185
189
  const handleSelectUser = useCallback((user, returnCtxOverride) => {
186
190
  var _a;
191
+ setListEntranceAnimation(false);
187
192
  setChatReturnCtx(returnCtxOverride !== null && returnCtxOverride !== void 0 ? returnCtxOverride : userListCtx);
188
193
  const history = (_a = data === null || data === void 0 ? void 0 : data.sampleChats[user.uid]) !== null && _a !== void 0 ? _a : [];
189
194
  selectUser(user, history);
@@ -196,19 +201,28 @@ export const ChatWidget = ({ theme: localTheme }) => {
196
201
  });
197
202
  }, [data, selectUser, userListCtx]);
198
203
  const handleBackFromChat = useCallback(() => {
204
+ setListEntranceAnimation(false);
199
205
  clearChat();
200
206
  setUserListCtx(chatReturnCtx);
201
207
  setScreen('user-list');
202
208
  }, [clearChat, chatReturnCtx]);
203
209
  const handleOpenTicket = useCallback((id) => {
210
+ setListEntranceAnimation(false);
204
211
  setViewingTicketId(id);
205
212
  setScreen('ticket-detail');
206
213
  setActiveTab('tickets');
207
214
  }, []);
208
215
  const handleTabChange = useCallback((tab) => {
216
+ setListEntranceAnimation(false);
209
217
  setActiveTab(tab);
210
218
  setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
211
219
  }, []);
220
+ useEffect(() => {
221
+ if (!listEntranceAnimation)
222
+ return;
223
+ const t = window.setTimeout(() => setListEntranceAnimation(false), 520);
224
+ return () => window.clearTimeout(t);
225
+ }, [listEntranceAnimation]);
212
226
  /* ── Block/Unblock ───────────────────────────────────────────────────── */
213
227
  const handleBlock = useCallback(() => {
214
228
  if (!activeUser)
@@ -347,7 +361,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
347
361
  }, onMouseLeave: e => {
348
362
  e.currentTarget.style.transform = 'scale(1)';
349
363
  e.currentTarget.style.boxShadow = `0 8px 28px ${theme.buttonColor}55`;
350
- }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { onClick: closeDrawer, style: {
364
+ }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { "aria-hidden": true, style: {
351
365
  position: 'fixed', inset: 0, zIndex: 9997,
352
366
  backgroundColor: 'rgba(0,0,0,0.35)',
353
367
  opacity: closing ? 0 : 1,
@@ -364,7 +378,11 @@ export const ChatWidget = ({ theme: localTheme }) => {
364
378
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
365
379
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
366
380
  zIndex: 20, display: 'flex', gap: 6,
367
- }, 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' && (_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: () => setScreen('home'), onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined })), 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)) })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => setScreen('ticket-new'), onSelectTicket: id => { setViewingTicketId(id); setScreen('ticket-detail'); } })), screen === 'ticket-new' && (_jsx(TicketFormScreen, { config: widgetConfig, onSubmit: handleRaiseTicket, onCancel: () => setScreen('tickets') })), screen === 'ticket-detail' && viewingTicketId && ((() => {
381
+ }, 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' && (_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 => {
382
+ setListEntranceAnimation(false);
383
+ setViewingTicketId(id);
384
+ setScreen('ticket-detail');
385
+ }, animateEntrance: listEntranceAnimation })), screen === 'ticket-new' && (_jsx(TicketFormScreen, { config: widgetConfig, onSubmit: handleRaiseTicket, onCancel: () => setScreen('tickets') })), screen === 'ticket-detail' && viewingTicketId && ((() => {
368
386
  const t = tickets.find(x => x.id === viewingTicketId);
369
387
  return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
370
388
  })()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
@@ -1,8 +1,12 @@
1
1
  import React from 'react';
2
2
  import { WidgetConfig, UserListContext, Ticket } from '../../types';
3
+ export interface HomeNavigateOptions {
4
+ /** When true, list screens play stagger animation (home burger menu only) */
5
+ fromMenu?: boolean;
6
+ }
3
7
  interface HomeScreenProps {
4
8
  config: WidgetConfig;
5
- onNavigate: (ctx: UserListContext | 'ticket') => void;
9
+ onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
6
10
  /** Open a specific pending ticket (full detail) */
7
11
  onOpenTicket: (ticketId: string) => void;
8
12
  tickets: Ticket[];