ajaxter-chat 2.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +119 -128
  2. package/dist/components/BlockList/index.d.ts +10 -0
  3. package/dist/components/BlockList/index.js +33 -0
  4. package/dist/components/CallScreen/index.d.ts +13 -0
  5. package/dist/components/CallScreen/index.js +48 -0
  6. package/dist/components/ChatScreen/index.d.ts +10 -3
  7. package/dist/components/ChatScreen/index.js +142 -57
  8. package/dist/components/ChatWidget.js +192 -98
  9. package/dist/components/EmojiPicker/index.d.ts +8 -0
  10. package/dist/components/EmojiPicker/index.js +18 -0
  11. package/dist/components/HomeScreen/index.d.ts +2 -3
  12. package/dist/components/HomeScreen/index.js +25 -41
  13. package/dist/components/MaintenanceView/index.d.ts +0 -1
  14. package/dist/components/MaintenanceView/index.js +4 -6
  15. package/dist/components/RecentChatsScreen/index.d.ts +4 -3
  16. package/dist/components/RecentChatsScreen/index.js +7 -37
  17. package/dist/components/Tabs/BottomTabs.d.ts +1 -1
  18. package/dist/components/Tabs/BottomTabs.js +25 -20
  19. package/dist/components/TicketScreen/index.d.ts +3 -3
  20. package/dist/components/TicketScreen/index.js +39 -56
  21. package/dist/components/UserListScreen/index.d.ts +2 -4
  22. package/dist/components/UserListScreen/index.js +33 -62
  23. package/dist/config/index.d.ts +7 -3
  24. package/dist/config/index.js +28 -25
  25. package/dist/hooks/useChat.d.ts +8 -3
  26. package/dist/hooks/useChat.js +22 -18
  27. package/dist/hooks/useRemoteConfig.d.ts +6 -0
  28. package/dist/hooks/useRemoteConfig.js +26 -0
  29. package/dist/hooks/useWebRTC.d.ts +11 -0
  30. package/dist/hooks/useWebRTC.js +112 -0
  31. package/dist/index.d.ts +9 -5
  32. package/dist/index.js +8 -4
  33. package/dist/types/index.d.ts +62 -21
  34. package/dist/utils/chat.d.ts +13 -0
  35. package/dist/utils/chat.js +62 -0
  36. package/dist/utils/theme.d.ts +3 -1
  37. package/dist/utils/theme.js +14 -7
  38. package/package.json +4 -4
  39. package/public/chatData.json +162 -0
  40. package/src/components/BlockList/index.tsx +94 -0
  41. package/src/components/CallScreen/index.tsx +144 -0
  42. package/src/components/ChatScreen/index.tsx +403 -139
  43. package/src/components/ChatWidget.tsx +394 -250
  44. package/src/components/EmojiPicker/index.tsx +48 -0
  45. package/src/components/HomeScreen/index.tsx +58 -82
  46. package/src/components/MaintenanceView/index.tsx +6 -9
  47. package/src/components/RecentChatsScreen/index.tsx +51 -96
  48. package/src/components/Tabs/BottomTabs.tsx +45 -37
  49. package/src/components/TicketScreen/index.tsx +87 -133
  50. package/src/components/UserListScreen/index.tsx +75 -153
  51. package/src/config/index.ts +32 -26
  52. package/src/hooks/useChat.ts +31 -14
  53. package/src/hooks/useRemoteConfig.ts +26 -0
  54. package/src/hooks/useWebRTC.ts +130 -0
  55. package/src/index.ts +26 -15
  56. package/src/types/index.ts +85 -40
  57. package/src/utils/chat.ts +70 -0
  58. package/src/utils/theme.ts +18 -7
  59. package/dist/hooks/useUsers.d.ts +0 -7
  60. package/dist/hooks/useUsers.js +0 -26
  61. package/dist/services/userService.d.ts +0 -2
  62. package/dist/services/userService.js +0 -9
  63. package/dist/src/components/ChatScreen/index.d.ts +0 -12
  64. package/dist/src/components/ChatScreen/index.js +0 -83
  65. package/dist/src/components/ChatWidget.d.ts +0 -4
  66. package/dist/src/components/ChatWidget.js +0 -141
  67. package/dist/src/components/HomeScreen/index.d.ts +0 -9
  68. package/dist/src/components/HomeScreen/index.js +0 -71
  69. package/dist/src/components/MaintenanceView/index.d.ts +0 -7
  70. package/dist/src/components/MaintenanceView/index.js +0 -16
  71. package/dist/src/components/RecentChatsScreen/index.d.ts +0 -16
  72. package/dist/src/components/RecentChatsScreen/index.js +0 -38
  73. package/dist/src/components/Tabs/BottomTabs.d.ts +0 -10
  74. package/dist/src/components/Tabs/BottomTabs.js +0 -29
  75. package/dist/src/components/TicketScreen/index.d.ts +0 -9
  76. package/dist/src/components/TicketScreen/index.js +0 -71
  77. package/dist/src/components/UserListScreen/index.d.ts +0 -13
  78. package/dist/src/components/UserListScreen/index.js +0 -64
  79. package/dist/src/config/index.d.ts +0 -3
  80. package/dist/src/config/index.js +0 -38
  81. package/dist/src/hooks/useChat.d.ts +0 -8
  82. package/dist/src/hooks/useChat.js +0 -26
  83. package/dist/src/hooks/useUsers.d.ts +0 -7
  84. package/dist/src/hooks/useUsers.js +0 -26
  85. package/dist/src/index.d.ts +0 -14
  86. package/dist/src/index.js +0 -13
  87. package/dist/src/services/userService.d.ts +0 -2
  88. package/dist/src/services/userService.js +0 -9
  89. package/dist/src/types/index.d.ts +0 -59
  90. package/dist/src/types/index.js +0 -1
  91. package/dist/src/utils/theme.d.ts +0 -3
  92. package/dist/src/utils/theme.js +0 -13
  93. package/src/hooks/useUsers.ts +0 -27
  94. package/src/services/userService.ts +0 -9
@@ -1,159 +1,297 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { ChatMessage, ChatUser, ChatWidgetTheme } from '../../types';
3
- import { mergeTheme } from '../../utils/theme';
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { ChatMessage, ChatUser, WidgetConfig } from '../../types';
3
+ import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
4
+ import { EmojiPicker } from '../EmojiPicker';
4
5
 
5
6
  interface ChatScreenProps {
6
- activeUser: ChatUser;
7
- messages: ChatMessage[];
8
- onSend: (text: string) => void;
9
- onBack: () => void;
10
- onClose: () => void;
11
- theme?: ChatWidgetTheme;
7
+ activeUser: ChatUser;
8
+ messages: ChatMessage[];
9
+ config: WidgetConfig;
10
+ isPaused: boolean;
11
+ isReported: boolean;
12
+ isBlocked: boolean;
13
+ onSend: (text: string, type?: ChatMessage['type'], extra?: Partial<ChatMessage>) => void;
14
+ onBack: () => void;
15
+ onClose: () => void;
16
+ onTogglePause:() => void;
17
+ onReport: () => void;
18
+ onBlock: () => void;
19
+ onStartCall: (withVideo: boolean) => void;
12
20
  }
13
21
 
14
22
  export const ChatScreen: React.FC<ChatScreenProps> = ({
15
- activeUser, messages, onSend, onBack, onClose, theme,
23
+ activeUser, messages, config, isPaused, isReported, isBlocked,
24
+ onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall,
16
25
  }) => {
17
- const t = mergeTheme(theme);
18
- const [text, setText] = useState('');
19
- const endRef = useRef<HTMLDivElement>(null);
20
- const inputRef = useRef<HTMLTextAreaElement>(null);
26
+ const [text, setText] = useState('');
27
+ const [showEmoji, setShowEmoji] = useState(false);
28
+ const [showMenu, setShowMenu] = useState(false);
29
+ const [isRecording, setIsRecording] = useState(false);
30
+ const [recordSec, setRecordSec] = useState(0);
31
+ const [showConfirm, setShowConfirm] = useState<'report'|'block'|'pause'|null>(null);
21
32
 
22
- useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
33
+ const endRef = useRef<HTMLDivElement>(null);
34
+ const inputRef = useRef<HTMLTextAreaElement>(null);
35
+ const fileRef = useRef<HTMLInputElement>(null);
36
+ const recordTimer = useRef<ReturnType<typeof setInterval> | null>(null);
37
+ const mediaRecorder = useRef<MediaRecorder | null>(null);
23
38
 
24
- const handleSend = () => {
25
- if (!text.trim()) return;
26
- onSend(text);
39
+ useEffect(() => { endRef.current?.scrollIntoView({ behavior:'smooth' }); }, [messages]);
40
+
41
+ const handleSend = useCallback(() => {
42
+ if (!text.trim() || isPaused || isBlocked) return;
43
+ onSend(text.trim());
27
44
  setText('');
28
45
  inputRef.current?.focus();
29
- };
46
+ }, [text, isPaused, isBlocked, onSend]);
30
47
 
31
48
  const handleKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
32
49
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); }
33
50
  };
34
51
 
35
- const initials = activeUser.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
52
+ // Voice recording
53
+ const startRecording = async () => {
54
+ if (isPaused || isBlocked) return;
55
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
56
+ const mr = new MediaRecorder(stream);
57
+ mediaRecorder.current = mr;
58
+ const chunks: BlobPart[] = [];
59
+ mr.ondataavailable = e => chunks.push(e.data);
60
+ mr.onstop = () => {
61
+ stream.getTracks().forEach(t => t.stop());
62
+ // In production: upload blob, get URL, then send as voice message
63
+ onSend('[Voice Message]', 'voice', { voiceDuration: recordSec });
64
+ setRecordSec(0);
65
+ };
66
+ mr.start();
67
+ setIsRecording(true);
68
+ recordTimer.current = setInterval(() => setRecordSec(s => s + 1), 1000);
69
+ };
70
+
71
+ const stopRecording = () => {
72
+ mediaRecorder.current?.stop();
73
+ if (recordTimer.current) clearInterval(recordTimer.current);
74
+ setIsRecording(false);
75
+ };
76
+
77
+ // Attachment
78
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79
+ const file = e.target.files?.[0];
80
+ if (!file || isPaused || isBlocked) return;
81
+ onSend(`[Attachment: ${file.name}]`, 'attachment', {
82
+ attachmentName: file.name,
83
+ attachmentSize: `${(file.size / 1024).toFixed(1)} KB`,
84
+ });
85
+ e.target.value = '';
86
+ };
87
+
88
+ // Download transcript
89
+ const handleTranscript = () => {
90
+ const content = generateTranscript(messages, activeUser);
91
+ downloadText(content, `chat-${activeUser.name.replace(/\s+/g,'_')}-${Date.now()}.txt`);
92
+ setShowMenu(false);
93
+ };
94
+
95
+ // Confirm actions
96
+ const handleConfirm = (action: 'report'|'block'|'pause') => {
97
+ setShowConfirm(null); setShowMenu(false);
98
+ if (action === 'report') onReport();
99
+ if (action === 'block') onBlock();
100
+ if (action === 'pause') onTogglePause();
101
+ };
102
+
103
+ const peerAvatar = avatarColor(activeUser.name);
104
+ const peerInit = initials(activeUser.name);
105
+
106
+ // Group messages by date
107
+ const grouped = groupByDate(messages);
36
108
 
37
109
  return (
38
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideInRight 0.25s ease' }}>
39
- {/* Teal header */}
110
+ <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease', position:'relative' }}>
111
+
112
+ {/* ── Header ── */}
40
113
  <div style={{
41
- backgroundColor: t.primaryColor,
42
- padding: '14px 18px',
43
- display: 'flex',
44
- alignItems: 'center',
45
- gap: '10px',
46
- flexShrink: 0,
114
+ background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
115
+ padding:'12px 14px', display:'flex', alignItems:'center', gap:10, flexShrink:0,
47
116
  }}>
48
- <button onClick={onBack} style={iconBtnStyle}>
49
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
50
- <path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
117
+ {/* hamburger/back */}
118
+ <button onClick={onBack} style={hdrBtn}>
119
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
120
+ <line x1="3" y1="6" x2="21" y2="6" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/>
121
+ <line x1="3" y1="12" x2="21" y2="12" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/>
122
+ <line x1="3" y1="18" x2="21" y2="18" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/>
51
123
  </svg>
52
124
  </button>
53
125
 
54
- <div style={{
55
- width: 36, height: 36, borderRadius: '50%',
56
- backgroundColor: 'rgba(255,255,255,0.25)',
57
- display: 'flex', alignItems: 'center', justifyContent: 'center',
58
- fontWeight: 700, fontSize: '13px', color: '#fff', flexShrink: 0,
59
- }}>
60
- {initials}
126
+ <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' }}>
127
+ {peerInit}
128
+ <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' }} />
61
129
  </div>
62
130
 
63
- <div style={{ flex: 1, minWidth: 0 }}>
64
- <div style={{ fontWeight: 700, fontSize: '14px', color: '#fff', fontFamily: t.fontFamily }}>
65
- {activeUser.name}
66
- </div>
67
- <div style={{ fontSize: '11px', color: 'rgba(255,255,255,0.8)', display: 'flex', alignItems: 'center', gap: 4 }}>
68
- <span style={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: '#a8f0c6', display: 'inline-block' }} />
69
- Online
70
- </div>
131
+ <div style={{ flex:1, minWidth:0 }}>
132
+ <div style={{ fontWeight:700, fontSize:14, color:'#fff', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{activeUser.name}</div>
133
+ <div style={{ fontSize:11, color:'rgba(255,255,255,0.8)' }}>{activeUser.designation}</div>
71
134
  </div>
72
135
 
73
- <button onClick={onClose} style={iconBtnStyle}>
136
+ {/* Title "Support" */}
137
+ <span style={{ fontSize:14, fontWeight:700, color:'#fff', opacity:0.9 }}>Support</span>
138
+
139
+ {/* Call button */}
140
+ {config.allowWebCall && (
141
+ <button onClick={() => onStartCall(false)} style={hdrBtn} title="Voice Call">
142
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none">
143
+ <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"/>
144
+ </svg>
145
+ </button>
146
+ )}
147
+
148
+ {/* Maximize/expand icon */}
149
+ <button style={hdrBtn} title="Fullscreen">
74
150
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
75
- <path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" />
151
+ <path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
76
152
  </svg>
77
153
  </button>
78
154
  </div>
79
155
 
80
- {/* Messages area */}
81
- <div style={{
82
- flex: 1, overflowY: 'auto', padding: '18px 16px',
83
- display: 'flex', flexDirection: 'column', gap: '10px',
84
- backgroundColor: '#f7f8fc',
85
- }}>
156
+ {/* ── Paused / Blocked / Reported banners ── */}
157
+ {isPaused && (
158
+ <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 }}>
159
+ Chat is paused users cannot send messages
160
+ <button onClick={onTogglePause} style={{ background:'#92400e', color:'#fff', border:'none', borderRadius:6, padding:'2px 8px', fontSize:11, cursor:'pointer', marginLeft:4 }}>Resume</button>
161
+ </div>
162
+ )}
163
+ {isBlocked && (
164
+ <div style={{ background:'#fee2e2', padding:'8px 16px', fontSize:12, fontWeight:600, color:'#991b1b', textAlign:'center', flexShrink:0 }}>
165
+ 🚫 This user is blocked
166
+ </div>
167
+ )}
168
+ {isReported && (
169
+ <div style={{ background:'#fef3c7', padding:'6px 16px', fontSize:11, color:'#92400e', textAlign:'center', flexShrink:0 }}>
170
+ ⚠️ This chat has been reported
171
+ </div>
172
+ )}
173
+
174
+ {/* ── "Enter your details" card (ticket chat body placeholder) ── */}
175
+ <div style={{ padding:'14px 14px 0', flexShrink:0 }}>
176
+ <div style={{
177
+ background:`linear-gradient(135deg, ${config.primaryColor}, ${config.primaryColor}cc)`,
178
+ borderRadius:12, padding:'14px 16px',
179
+ display:'flex', alignItems:'center', gap:12, cursor:'pointer',
180
+ }}>
181
+ <div style={{ width:32, height:32, borderRadius:'50%', background:'rgba(255,255,255,0.25)', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>
182
+ <span style={{ fontSize:16 }}>❓</span>
183
+ </div>
184
+ <div>
185
+ <div style={{ fontWeight:700, fontSize:14, color:'#fff' }}>Enter your details</div>
186
+ <div style={{ fontSize:12, color:'rgba(255,255,255,0.85)' }}>Click here to provide your information</div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ {/* ── Messages ── */}
192
+ <div style={{ flex:1, overflowY:'auto', padding:'14px', display:'flex', flexDirection:'column', gap:10, background:'#f8f9fc' }}
193
+ className="cw-scroll"
194
+ >
195
+ {grouped.map(({ date, msgs }) => (
196
+ <React.Fragment key={date}>
197
+ <DateDivider label={date} />
198
+ {msgs.map(msg => (
199
+ <Bubble key={msg.id} msg={msg} peer={activeUser} primaryColor={config.primaryColor} />
200
+ ))}
201
+ </React.Fragment>
202
+ ))}
86
203
  {messages.length === 0 && (
87
- <div style={{
88
- margin: 'auto', textAlign: 'center',
89
- fontSize: '13px', color: '#b0bec5',
90
- fontFamily: t.fontFamily,
91
- }}>
92
- <div style={{ fontSize: '28px', marginBottom: 8 }}>💬</div>
93
- Say hi to {activeUser.name}!
204
+ <div style={{ margin:'auto', textAlign:'center', color:'#c4cad4', fontSize:13 }}>
205
+ <div style={{ fontSize:28, marginBottom:8 }}>💬</div>
206
+ Say hello to {activeUser.name}!
94
207
  </div>
95
208
  )}
96
- {messages.map(msg => (
97
- <Bubble key={msg.id} msg={msg} primaryColor={t.primaryColor} font={t.fontFamily} />
98
- ))}
99
209
  <div ref={endRef} />
100
210
  </div>
101
211
 
102
- {/* Input bar matching image 2 exactly */}
103
- <div style={{
104
- borderTop: '1px solid #eef0f5',
105
- padding: '10px 14px',
106
- backgroundColor: '#fff',
107
- display: 'flex',
108
- alignItems: 'flex-end',
109
- gap: '10px',
110
- flexShrink: 0,
111
- }}>
112
- <textarea
113
- ref={inputRef}
114
- value={text}
115
- onChange={e => setText(e.target.value)}
116
- onKeyDown={handleKey}
117
- placeholder="Type and press [enter].."
118
- rows={1}
119
- style={{
120
- flex: 1, resize: 'none', border: 'none', outline: 'none',
121
- fontFamily: t.fontFamily, fontSize: '14px', lineHeight: '1.5',
122
- maxHeight: '80px', overflowY: 'auto', color: '#1a2332',
123
- padding: '6px 0', backgroundColor: 'transparent',
124
- }}
125
- />
126
- {/* Action icons: like, attachment, emoji — matching image 2 */}
127
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
128
- <IconBtn onClick={() => {}} title="Reaction">
129
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
130
- <path d="M14 9h.01M10 9h.01M12 2a10 10 0 100 20A10 10 0 0012 2zm0 14s-4-1.5-4-4" stroke="#9aa3af" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
131
- <path d="M8 15s1.5 2 4 2 4-2 4-2" stroke="#9aa3af" strokeWidth="1.8" strokeLinecap="round"/>
132
- </svg>
133
- </IconBtn>
134
- <IconBtn onClick={() => {}} title="Attach">
135
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
136
- <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="#9aa3af" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
137
- </svg>
138
- </IconBtn>
139
- <IconBtn onClick={() => {}} title="Emoji">
140
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
141
- <circle cx="12" cy="12" r="10" stroke="#9aa3af" strokeWidth="1.8"/>
142
- <path d="M8 14s1.5 2 4 2 4-2 4-2" stroke="#9aa3af" strokeWidth="1.8" strokeLinecap="round"/>
143
- <line x1="9" y1="9" x2="9.01" y2="9" stroke="#9aa3af" strokeWidth="2.5" strokeLinecap="round"/>
144
- <line x1="15" y1="9" x2="15.01" y2="9" stroke="#9aa3af" strokeWidth="2.5" strokeLinecap="round"/>
145
- </svg>
146
- </IconBtn>
212
+ {/* ── Input bar ── */}
213
+ <div style={{ borderTop:'1px solid #eef0f5', padding:'10px 14px', background:'#fff', flexShrink:0, position:'relative' }}>
214
+
215
+ {/* Emoji picker */}
216
+ {showEmoji && config.allowEmoji && (
217
+ <EmojiPicker
218
+ primaryColor={config.primaryColor}
219
+ onSelect={e => setText(t => t + e)}
220
+ onClose={() => setShowEmoji(false)}
221
+ />
222
+ )}
223
+
224
+ {/* Recording indicator */}
225
+ {isRecording && (
226
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8, padding:'6px 12px', background:'#fee2e2', borderRadius:8 }}>
227
+ <span style={{ width:8, height:8, borderRadius:'50%', background:'#ef4444', display:'inline-block', animation:'cw-pulse 1s infinite' }} />
228
+ <span style={{ fontSize:13, color:'#991b1b', fontWeight:600 }}>Recording {recordSec}s</span>
229
+ <button onClick={stopRecording} style={{ marginLeft:'auto', background:'#ef4444', color:'#fff', border:'none', borderRadius:6, padding:'3px 10px', fontSize:12, cursor:'pointer' }}>Stop</button>
230
+ </div>
231
+ )}
232
+
233
+ <div style={{ display:'flex', alignItems:'flex-end', gap:8 }}>
234
+ <textarea
235
+ ref={inputRef}
236
+ value={text}
237
+ onChange={e => setText(e.target.value)}
238
+ onKeyDown={handleKey}
239
+ placeholder={isPaused || isBlocked ? 'Chat is unavailable' : 'Message...'}
240
+ disabled={isPaused || isBlocked || isRecording}
241
+ rows={1}
242
+ style={{
243
+ flex:1, resize:'none', border:'1.5px solid #e5e7eb',
244
+ borderRadius:22, padding:'9px 14px',
245
+ fontSize:14, outline:'none', lineHeight:1.5,
246
+ maxHeight:80, overflowY:'auto', color:'#1a2332',
247
+ background: isPaused || isBlocked ? '#f9fafb' : '#fff',
248
+ transition:'border-color 0.2s',
249
+ fontFamily:'inherit',
250
+ }}
251
+ onFocus={e => (e.target.style.borderColor = config.primaryColor)}
252
+ onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
253
+ />
254
+
255
+ {/* Emoji */}
256
+ {config.allowEmoji && (
257
+ <ActionBtn onClick={() => setShowEmoji(v => !v)} title="Emoji">
258
+ <svg width="19" height="19" viewBox="0 0 24 24" fill="none">
259
+ <circle cx="12" cy="12" r="10" stroke="#9ca3af" strokeWidth="1.8"/>
260
+ <path d="M8 14s1.5 2 4 2 4-2 4-2" stroke="#9ca3af" strokeWidth="1.8" strokeLinecap="round"/>
261
+ <circle cx="9" cy="9" r="1" fill="#9ca3af"/><circle cx="15" cy="9" r="1" fill="#9ca3af"/>
262
+ </svg>
263
+ </ActionBtn>
264
+ )}
265
+
266
+ {/* Attachment */}
267
+ {config.allowAttachment && (
268
+ <>
269
+ <input ref={fileRef} type="file" style={{ display:'none' }} onChange={handleFileChange} />
270
+ <ActionBtn onClick={() => fileRef.current?.click()} title="Attach file">
271
+ <svg width="19" height="19" viewBox="0 0 24 24" fill="none">
272
+ <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="#9ca3af" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
273
+ </svg>
274
+ </ActionBtn>
275
+ </>
276
+ )}
277
+
278
+ {/* Voice */}
279
+ {config.allowVoiceMessage && !isRecording && (
280
+ <ActionBtn onClick={startRecording} title="Voice message">
281
+ <svg width="19" height="19" viewBox="0 0 24 24" fill="none">
282
+ <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke="#9ca3af" strokeWidth="1.8" strokeLinecap="round"/>
283
+ <path d="M19 10v2a7 7 0 01-14 0v-2M12 19v4M8 23h8" stroke="#9ca3af" strokeWidth="1.8" strokeLinecap="round"/>
284
+ </svg>
285
+ </ActionBtn>
286
+ )}
287
+
288
+ {/* Send */}
147
289
  {text.trim() && (
148
- <button
149
- onClick={handleSend}
150
- style={{
151
- width: 36, height: 36, borderRadius: '50%',
152
- backgroundColor: t.primaryColor, border: 'none',
153
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
154
- transition: 'transform 0.15s',
155
- }}
156
- >
290
+ <button onClick={handleSend} style={{
291
+ width:36, height:36, borderRadius:'50%', backgroundColor:config.primaryColor,
292
+ border:'none', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0,
293
+ transition:'transform 0.15s',
294
+ }}>
157
295
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
158
296
  <path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
159
297
  </svg>
@@ -161,45 +299,171 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
161
299
  )}
162
300
  </div>
163
301
  </div>
302
+
303
+ {/* ── 3-dot overflow menu ── */}
304
+ <div style={{ position:'absolute', top:12, right:52, zIndex:50 }}>
305
+ <button
306
+ onClick={() => setShowMenu(v => !v)}
307
+ style={{ ...hdrBtn, background:'rgba(255,255,255,0.18)' }}
308
+ title="More options"
309
+ >
310
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
311
+ <circle cx="12" cy="5" r="1.5" fill="#fff"/>
312
+ <circle cx="12" cy="12" r="1.5" fill="#fff"/>
313
+ <circle cx="12" cy="19" r="1.5" fill="#fff"/>
314
+ </svg>
315
+ </button>
316
+
317
+ {showMenu && (
318
+ <div style={{
319
+ position:'absolute', top:'100%', right:0, marginTop:4,
320
+ background:'#fff', borderRadius:12,
321
+ boxShadow:'0 8px 30px rgba(0,0,0,0.16)',
322
+ padding:'6px', minWidth:200, zIndex:200,
323
+ animation:'cw-fadeUp 0.18s ease',
324
+ }}>
325
+ {config.allowTranscriptDownload && (
326
+ <MenuItem icon="📥" label="Download Transcript" onClick={handleTranscript} />
327
+ )}
328
+ <MenuItem icon={isPaused ? '▶️' : '⏸'} label={isPaused ? 'Resume Chat' : 'Pause Chat'} onClick={() => { setShowMenu(false); setShowConfirm('pause'); }} />
329
+ {config.allowReport && !isReported && (
330
+ <MenuItem icon="⚠️" label="Report Chat" onClick={() => { setShowMenu(false); setShowConfirm('report'); }} />
331
+ )}
332
+ {config.allowBlock && activeUser.type === 'user' && !isBlocked && (
333
+ <MenuItem icon="🚫" label="Block User" onClick={() => { setShowMenu(false); setShowConfirm('block'); }} danger />
334
+ )}
335
+ <div style={{ borderTop:'1px solid #f0f2f5', margin:'4px 0' }} />
336
+ <MenuItem icon="✕" label="Close Chat" onClick={onClose} />
337
+ </div>
338
+ )}
339
+ </div>
340
+
341
+ {/* ── Confirm dialog ── */}
342
+ {showConfirm && (
343
+ <div style={{
344
+ position:'absolute', inset:0, background:'rgba(0,0,0,0.45)',
345
+ display:'flex', alignItems:'center', justifyContent:'center', zIndex:300,
346
+ borderRadius:'inherit',
347
+ }}>
348
+ <div style={{ background:'#fff', borderRadius:16, padding:'24px 20px', width:280, boxShadow:'0 16px 48px rgba(0,0,0,0.22)', animation:'cw-fadeUp 0.2s ease' }}>
349
+ <div style={{ fontWeight:800, fontSize:16, color:'#1a2332', marginBottom:8 }}>
350
+ {showConfirm === 'pause' && (isPaused ? 'Resume Chat?' : 'Pause Chat?')}
351
+ {showConfirm === 'report' && 'Report this chat?'}
352
+ {showConfirm === 'block' && 'Block this user?'}
353
+ </div>
354
+ <p style={{ fontSize:13, color:'#7b8fa1', lineHeight:1.6, margin:'0 0 18px' }}>
355
+ {showConfirm === 'pause' && (isPaused ? 'The user will be able to send messages again.' : 'The user will not be able to send new messages.')}
356
+ {showConfirm === 'report' && 'This chat will be flagged for review by the admin team.'}
357
+ {showConfirm === 'block' && 'This user will be blocked and added to your block list. You can unblock them later.'}
358
+ </p>
359
+ <div style={{ display:'flex', gap:10 }}>
360
+ <button onClick={() => setShowConfirm(null)} style={{ flex:1, padding:'9px', borderRadius:10, border:'1.5px solid #e5e7eb', background:'#fff', cursor:'pointer', fontSize:13, fontWeight:600, color:'#374151' }}>
361
+ Cancel
362
+ </button>
363
+ <button onClick={() => handleConfirm(showConfirm)} style={{
364
+ flex:1, padding:'9px', borderRadius:10, border:'none',
365
+ background: showConfirm==='block' ? '#ef4444' : config.primaryColor,
366
+ color:'#fff', cursor:'pointer', fontSize:13, fontWeight:700,
367
+ }}>
368
+ Confirm
369
+ </button>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ )}
164
374
  </div>
165
375
  );
166
376
  };
167
377
 
168
- const Bubble: React.FC<{ msg: ChatMessage; primaryColor: string; font: string }> = ({ msg, primaryColor, font }) => {
378
+ // ── Sub-components ─────────────────────────────────────────────────────────────
379
+
380
+ const Bubble: React.FC<{ msg: ChatMessage; peer: ChatUser; primaryColor: string }> = ({ msg, peer, primaryColor }) => {
169
381
  const isMe = msg.senderId === 'me';
170
- const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
382
+
383
+ const content = msg.type === 'voice' ? (
384
+ <div style={{ display:'flex', alignItems:'center', gap:8 }}>
385
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
386
+ <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z" stroke={isMe?'#fff':'#374151'} strokeWidth="2" strokeLinecap="round"/>
387
+ <path d="M19 10v2a7 7 0 01-14 0v-2M12 19v4" stroke={isMe?'#fff':'#374151'} strokeWidth="2" strokeLinecap="round"/>
388
+ </svg>
389
+ <span>Voice Message {msg.voiceDuration ? `· ${msg.voiceDuration}s` : ''}</span>
390
+ </div>
391
+ ) : msg.type === 'attachment' ? (
392
+ <div style={{ display:'flex', alignItems:'center', gap:8 }}>
393
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
394
+ <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':'#374151'} strokeWidth="2" strokeLinecap="round"/>
395
+ </svg>
396
+ <div>
397
+ <div style={{ fontWeight:600 }}>{msg.attachmentName ?? 'File'}</div>
398
+ {msg.attachmentSize && <div style={{ fontSize:11, opacity:0.75 }}>{msg.attachmentSize}</div>}
399
+ </div>
400
+ </div>
401
+ ) : <span>{msg.text}</span>;
402
+
171
403
  return (
172
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: isMe ? 'flex-end' : 'flex-start', gap: 3 }}>
404
+ <div style={{ display:'flex', flexDirection:'column', alignItems: isMe?'flex-end':'flex-start', gap:3 }}>
173
405
  <div style={{
174
- maxWidth: '75%', padding: '10px 14px',
406
+ maxWidth:'76%', padding:'10px 13px',
175
407
  borderRadius: isMe ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
176
408
  backgroundColor: isMe ? primaryColor : '#fff',
177
409
  color: isMe ? '#fff' : '#1a2332',
178
- fontSize: '14px', lineHeight: '1.5',
179
- boxShadow: '0 1px 4px rgba(0,0,0,0.07)',
180
- fontFamily: font, wordBreak: 'break-word',
410
+ fontSize:14, lineHeight:1.5,
411
+ boxShadow:'0 1px 4px rgba(0,0,0,0.07)',
412
+ wordBreak:'break-word',
181
413
  }}>
182
- {msg.text}
414
+ {content}
183
415
  </div>
184
- <span style={{ fontSize: '11px', color: '#b0bec5', padding: '0 4px' }}>{time}</span>
416
+ <span style={{ fontSize:11, color:'#b0bec5', padding:'0 4px' }}>{formatTime(msg.timestamp)}</span>
185
417
  </div>
186
418
  );
187
419
  };
188
420
 
189
- const IconBtn: React.FC<{ onClick: () => void; title: string; children: React.ReactNode }> = ({ onClick, title, children }) => (
190
- <button
191
- onClick={onClick}
192
- title={title}
193
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '4px', display: 'flex', alignItems: 'center', borderRadius: '6px' }}
194
- onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f3f4f6'}
195
- onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'none'}
421
+ const DateDivider: React.FC<{ label: string }> = ({ label }) => (
422
+ <div style={{ display:'flex', alignItems:'center', gap:10, margin:'4px 0' }}>
423
+ <div style={{ flex:1, height:1, background:'#e5e7eb' }} />
424
+ <span style={{ fontSize:11, fontWeight:600, color:'#9ca3af', whiteSpace:'nowrap' }}>{label}</span>
425
+ <div style={{ flex:1, height:1, background:'#e5e7eb' }} />
426
+ </div>
427
+ );
428
+
429
+ const MenuItem: React.FC<{ icon: string; label: string; onClick: () => void; danger?: boolean }> = ({ icon, label, onClick, danger }) => (
430
+ <button onClick={onClick} style={{
431
+ display:'flex', alignItems:'center', gap:10, width:'100%', padding:'9px 12px',
432
+ background:'none', border:'none', borderRadius:8, cursor:'pointer', textAlign:'left',
433
+ fontSize:13, fontWeight:600, color: danger ? '#ef4444' : '#374151',
434
+ transition:'background 0.12s',
435
+ }}
436
+ onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = danger ? '#fee2e2' : '#f3f4f6'}
437
+ onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'none'}
196
438
  >
197
- {children}
439
+ <span>{icon}</span> {label}
198
440
  </button>
199
441
  );
200
442
 
201
- const iconBtnStyle: React.CSSProperties = {
202
- background: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: '50%',
203
- width: 34, height: 34, display: 'flex', alignItems: 'center', justifyContent: 'center',
204
- cursor: 'pointer', flexShrink: 0,
443
+ const ActionBtn: React.FC<{ onClick: () => void; title: string; children: React.ReactNode }> = ({ onClick, title, children }) => (
444
+ <button onClick={onClick} title={title} style={{
445
+ background:'none', border:'none', cursor:'pointer', padding:'7px',
446
+ borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0,
447
+ transition:'background 0.13s',
448
+ }}
449
+ onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f3f4f6'}
450
+ onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'none'}
451
+ >{children}</button>
452
+ );
453
+
454
+ const hdrBtn: React.CSSProperties = {
455
+ background:'rgba(255,255,255,0.2)', border:'none', borderRadius:'50%',
456
+ width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
457
+ cursor:'pointer', flexShrink:0,
205
458
  };
459
+
460
+ // Group messages by date
461
+ function groupByDate(messages: ChatMessage[]): { date: string; msgs: ChatMessage[] }[] {
462
+ const map = new Map<string, ChatMessage[]>();
463
+ messages.forEach(m => {
464
+ const d = formatDate(m.timestamp);
465
+ if (!map.has(d)) map.set(d, []);
466
+ map.get(d)!.push(m);
467
+ });
468
+ return Array.from(map.entries()).map(([date, msgs]) => ({ date, msgs }));
469
+ }