ajaxter-chat 3.0.9 → 3.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ChatScreen/index.js +256 -34
- package/dist/components/ChatWidget.js +1 -1
- package/dist/components/TicketFormScreen/index.js +19 -13
- package/dist/components/UserListScreen/index.d.ts +2 -0
- package/dist/components/UserListScreen/index.js +8 -3
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/ChatScreen/index.tsx +365 -62
- package/src/components/ChatWidget.tsx +1 -0
- package/src/components/TicketFormScreen/index.tsx +15 -4
- package/src/components/UserListScreen/index.tsx +19 -2
- package/src/types/index.ts +2 -0
|
@@ -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 (
|
|
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
|
-
(
|
|
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,
|
|
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(() =>
|
|
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
|
|
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
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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: [
|
|
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:
|
|
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: {
|
|
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: [
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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 => {
|
|
@@ -364,7 +364,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
364
364
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
365
365
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
366
366
|
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 && ((() => {
|
|
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, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer' })), 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 && ((() => {
|
|
368
368
|
const t = tickets.find(x => x.id === viewingTicketId);
|
|
369
369
|
return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
|
|
370
370
|
})()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
@@ -28,7 +28,7 @@ export const TicketFormScreen = ({ config, onSubmit, onCancel }) => {
|
|
|
28
28
|
return;
|
|
29
29
|
onSubmit(title.trim(), desc.trim(), priority);
|
|
30
30
|
};
|
|
31
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#fff' }, children: [_jsxs("div", { style: {
|
|
31
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#fff', minHeight: 0 }, children: [_jsxs("div", { style: {
|
|
32
32
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
33
33
|
padding: '14px 18px',
|
|
34
34
|
display: 'flex',
|
|
@@ -45,7 +45,7 @@ export const TicketFormScreen = ({ config, onSubmit, onCancel }) => {
|
|
|
45
45
|
alignItems: 'center',
|
|
46
46
|
justifyContent: 'center',
|
|
47
47
|
cursor: 'pointer',
|
|
48
|
-
}, children: _jsx("svg", { width: "18", height: "18", 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", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "New ticket" }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }, children: "Describe your issue below" })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px' }, children: [_jsx("input", { placeholder: "Title *", value: title, onChange: e => setTitle(e.target.value), style: inputStyle(config.primaryColor), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("textarea", { placeholder: "Describe the issue\u2026", value: desc, onChange: e => setDesc(e.target.value), rows: 5, style: Object.assign(Object.assign({}, inputStyle(config.primaryColor)), { resize: 'none', marginTop: 12 }), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("div", { style: { display: 'flex', gap: 8, marginTop: 12,
|
|
48
|
+
}, children: _jsx("svg", { width: "18", height: "18", 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", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "New ticket" }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }, children: "Describe your issue below" })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }, children: [_jsx("input", { placeholder: "Title *", value: title, onChange: e => setTitle(e.target.value), style: inputStyle(config.primaryColor), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("textarea", { placeholder: "Describe the issue\u2026", value: desc, onChange: e => setDesc(e.target.value), rows: 5, style: Object.assign(Object.assign({}, inputStyle(config.primaryColor)), { resize: 'none', marginTop: 12 }), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("div", { style: { display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }, children: ['low', 'medium', 'high'].map(p => (_jsx("button", { type: "button", onClick: () => setPriority(p), style: {
|
|
49
49
|
flex: 1,
|
|
50
50
|
padding: '8px',
|
|
51
51
|
border: `1.5px solid ${priority === p ? pm[p].color : '#e5e7eb'}`,
|
|
@@ -56,15 +56,21 @@ export const TicketFormScreen = ({ config, onSubmit, onCancel }) => {
|
|
|
56
56
|
fontSize: 12,
|
|
57
57
|
cursor: 'pointer',
|
|
58
58
|
textTransform: 'capitalize',
|
|
59
|
-
}, children: pm[p].label }, p))) }), _jsx("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
}, children: pm[p].label }, p))) })] }), _jsx("div", { style: {
|
|
60
|
+
flexShrink: 0,
|
|
61
|
+
padding: '12px 18px 18px',
|
|
62
|
+
borderTop: '1px solid #eef0f5',
|
|
63
|
+
background: '#fff',
|
|
64
|
+
boxShadow: '0 -4px 20px rgba(15,23,42,0.06)',
|
|
65
|
+
}, children: _jsx("button", { type: "button", onClick: handleSubmit, disabled: !title.trim(), style: {
|
|
66
|
+
width: '100%',
|
|
67
|
+
padding: '14px',
|
|
68
|
+
borderRadius: 10,
|
|
69
|
+
border: 'none',
|
|
70
|
+
background: title.trim() ? config.primaryColor : '#e5e7eb',
|
|
71
|
+
color: title.trim() ? '#fff' : '#9ca3af',
|
|
72
|
+
fontWeight: 700,
|
|
73
|
+
fontSize: 15,
|
|
74
|
+
cursor: title.trim() ? 'pointer' : 'not-allowed',
|
|
75
|
+
}, children: "Submit ticket" }) })] }));
|
|
70
76
|
};
|
|
@@ -10,6 +10,8 @@ interface UserListScreenProps {
|
|
|
10
10
|
onSelectUser: (user: ChatUser) => void;
|
|
11
11
|
/** Shown on “New Conversation” list — opens block list */
|
|
12
12
|
onBlockList?: () => void;
|
|
13
|
+
/** “Need Support” (user → agents): show home icon instead of back arrow */
|
|
14
|
+
useHomeHeader?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export declare const UserListScreen: React.FC<UserListScreenProps>;
|
|
15
17
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { avatarColor, initials } from '../../utils/chat';
|
|
3
|
-
export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList, }) => {
|
|
3
|
+
export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList, useHomeHeader = false, }) => {
|
|
4
4
|
const isStaff = viewerType === 'developer';
|
|
5
5
|
const title = context === 'support'
|
|
6
6
|
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
@@ -8,7 +8,7 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
|
|
|
8
8
|
const subtitle = context === 'support'
|
|
9
9
|
? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
|
|
10
10
|
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
11
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0, position: 'relative' }, children: [_jsx(BackBtn, { onClick: onBack }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] }), context === 'conversation' && onBlockList && (_jsxs("button", { type: "button", onClick: onBlockList, style: {
|
|
11
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0, position: 'relative' }, children: [useHomeHeader ? _jsx(HomeBtn, { onClick: onBack }) : _jsx(BackBtn, { onClick: onBack }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] }), context === 'conversation' && onBlockList && (_jsxs("button", { type: "button", onClick: onBlockList, style: {
|
|
12
12
|
flexShrink: 0,
|
|
13
13
|
background: 'rgba(255,255,255,0.2)',
|
|
14
14
|
border: 'none',
|
|
@@ -45,9 +45,14 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
|
|
|
45
45
|
border: `1px solid ${u.type === 'developer' ? primaryColor + '30' : '#16a34a30'}`,
|
|
46
46
|
}, children: u.type === 'developer' ? 'Dev' : 'User' })] }, u.uid))) })] }));
|
|
47
47
|
};
|
|
48
|
-
const BackBtn = ({ onClick }) => (_jsx("button", { onClick: onClick, style: {
|
|
48
|
+
const BackBtn = ({ onClick }) => (_jsx("button", { type: "button", onClick: onClick, style: {
|
|
49
49
|
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: '50%',
|
|
50
50
|
width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
51
51
|
cursor: 'pointer', flexShrink: 0,
|
|
52
52
|
}, 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" }) }) }));
|
|
53
|
+
const HomeBtn = ({ onClick }) => (_jsx("button", { type: "button", onClick: onClick, title: "Home", "aria-label": "Home", style: {
|
|
54
|
+
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: '50%',
|
|
55
|
+
width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
56
|
+
cursor: 'pointer', flexShrink: 0,
|
|
57
|
+
}, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z", stroke: "#fff", strokeWidth: "2.2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M9 21V12h6v9", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" })] }) }));
|
|
53
58
|
const Empty = () => (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: "\uD83D\uDC65" }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: "No users available" }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: "Check back later" })] }));
|