ajaxter-chat 3.0.9 → 3.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState, useMemo, useRef, useEffect } from 'react';
2
2
  import { ChatUser, UserListContext } from '../../types';
3
3
  import { avatarColor, initials } from '../../utils/chat';
4
4
 
@@ -12,11 +12,37 @@ interface UserListScreenProps {
12
12
  onSelectUser: (user: ChatUser) => void;
13
13
  /** Shown on “New Conversation” list — opens block list */
14
14
  onBlockList?: () => void;
15
+ /** “Need Support” (user → agents): show home icon instead of back arrow */
16
+ useHomeHeader?: boolean;
17
+ /** Stagger animation — only when opening from home burger menu */
18
+ animateEntrance?: boolean;
19
+ }
20
+
21
+ function matchesUser(u: ChatUser, q: string): boolean {
22
+ if (!q.trim()) return true;
23
+ const s = q.trim().toLowerCase();
24
+ return (
25
+ u.name.toLowerCase().includes(s) ||
26
+ u.email.toLowerCase().includes(s) ||
27
+ u.designation.toLowerCase().includes(s) ||
28
+ u.project.toLowerCase().includes(s)
29
+ );
15
30
  }
16
31
 
17
32
  export const UserListScreen: React.FC<UserListScreenProps> = ({
18
33
  context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList,
34
+ useHomeHeader = false,
35
+ animateEntrance = false,
19
36
  }) => {
37
+ const [query, setQuery] = useState('');
38
+ const searchRef = useRef<HTMLInputElement>(null);
39
+
40
+ useEffect(() => {
41
+ searchRef.current?.focus();
42
+ }, []);
43
+
44
+ const filtered = useMemo(() => users.filter(u => matchesUser(u, query)), [users, query]);
45
+
20
46
  const isStaff = viewerType === 'developer';
21
47
  const title = context === 'support'
22
48
  ? (isStaff ? 'Provide Support' : 'Need Support')
@@ -25,11 +51,13 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
25
51
  ? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
26
52
  : (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
27
53
 
54
+ const rootAnim = animateEntrance ? 'cw-slideIn 0.22s ease' : undefined;
55
+
28
56
  return (
29
- <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
57
+ <div style={{ display:'flex', flexDirection:'column', height:'100%', animation: rootAnim }}>
30
58
  {/* Header */}
31
59
  <div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0, position:'relative' }}>
32
- <BackBtn onClick={onBack} />
60
+ {useHomeHeader ? <HomeBtn onClick={onBack} /> : <BackBtn onClick={onBack} />}
33
61
  <div style={{ flex: 1, minWidth: 0 }}>
34
62
  <div style={{ fontWeight:700, fontSize:16, color:'#fff' }}>{title}</div>
35
63
  <div style={{ fontSize:12, color:'rgba(255,255,255,0.8)' }}>{subtitle}</div>
@@ -63,11 +91,42 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
63
91
  )}
64
92
  </div>
65
93
 
94
+ <div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
95
+ <label style={{ display: 'block', margin: 0 }}>
96
+ <span style={{ position: 'absolute', width: 1, height: 1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)' }}>Search</span>
97
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
98
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
99
+ <circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
100
+ <path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
101
+ </svg>
102
+ <input
103
+ ref={searchRef}
104
+ type="search"
105
+ value={query}
106
+ onChange={e => setQuery(e.target.value)}
107
+ placeholder="Search by name…"
108
+ autoComplete="off"
109
+ style={{
110
+ flex: 1,
111
+ minWidth: 0,
112
+ border: 'none',
113
+ outline: 'none',
114
+ background: 'transparent',
115
+ fontSize: 14,
116
+ padding: '10px 0',
117
+ fontFamily: 'inherit',
118
+ color: '#1a2332',
119
+ }}
120
+ />
121
+ </div>
122
+ </label>
123
+ </div>
124
+
66
125
  {/* User list */}
67
126
  <div style={{ flex:1, overflowY:'auto' }}>
68
- {users.length === 0 ? (
69
- <Empty />
70
- ) : users.map((u, i) => (
127
+ {filtered.length === 0 ? (
128
+ <Empty hasQuery={!!query.trim()} />
129
+ ) : filtered.map((u, i) => (
71
130
  <button
72
131
  key={u.uid}
73
132
  onClick={() => onSelectUser(u)}
@@ -76,13 +135,14 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
76
135
  alignItems:'center', gap:13, background:'transparent',
77
136
  border:'none', borderBottom:'1px solid #f0f2f5',
78
137
  cursor:'pointer', textAlign:'left',
79
- animation:`cw-fadeUp 0.28s ease both`, animationDelay:`${i*0.05}s`,
138
+ ...(animateEntrance
139
+ ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` }
140
+ : {}),
80
141
  transition:'background 0.14s',
81
142
  }}
82
143
  onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8faff'}
83
144
  onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
84
145
  >
85
- {/* Avatar with online dot */}
86
146
  <div style={{ position:'relative', flexShrink:0 }}>
87
147
  <div style={{
88
148
  width:44, height:44, borderRadius:'50%',
@@ -96,14 +156,12 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
96
156
  backgroundColor: u.status==='online' ? '#22c55e' : u.status==='away' ? '#f59e0b' : '#d1d5db',
97
157
  }} />
98
158
  </div>
99
- {/* Info */}
100
159
  <div style={{ flex:1, minWidth:0 }}>
101
160
  <div style={{ fontWeight:700, fontSize:14, color:'#1a2332', marginBottom:2 }}>{u.name}</div>
102
161
  <div style={{ fontSize:12, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
103
162
  {u.designation} · {u.project}
104
163
  </div>
105
164
  </div>
106
- {/* Type badge */}
107
165
  <span style={{
108
166
  fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20,
109
167
  textTransform:'uppercase', letterSpacing:'0.05em', flexShrink:0,
@@ -119,7 +177,7 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
119
177
  };
120
178
 
121
179
  const BackBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
122
- <button onClick={onClick} style={{
180
+ <button type="button" onClick={onClick} style={{
123
181
  background:'rgba(255,255,255,0.22)', border:'none', borderRadius:'50%',
124
182
  width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
125
183
  cursor:'pointer', flexShrink:0,
@@ -130,10 +188,24 @@ const BackBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
130
188
  </button>
131
189
  );
132
190
 
133
- const Empty: React.FC = () => (
191
+ const HomeBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
192
+ <button type="button" onClick={onClick} title="Home" aria-label="Home" style={{
193
+ background:'rgba(255,255,255,0.22)', border:'none', borderRadius:'50%',
194
+ width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
195
+ cursor:'pointer', flexShrink:0,
196
+ }}>
197
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
198
+ <path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"
199
+ stroke="#fff" strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
200
+ <path d="M9 21V12h6v9" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
201
+ </svg>
202
+ </button>
203
+ );
204
+
205
+ const Empty: React.FC<{ hasQuery: boolean }> = ({ hasQuery }) => (
134
206
  <div style={{ padding:'50px 24px', textAlign:'center' }}>
135
- <div style={{ fontSize:36, marginBottom:10 }}>👥</div>
136
- <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No users available</div>
137
- <div style={{ fontSize:13, color:'#7b8fa1' }}>Check back later</div>
207
+ <div style={{ fontSize:36, marginBottom:10 }}>{hasQuery ? '🔍' : '👥'}</div>
208
+ <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{hasQuery ? 'No matches' : 'No users available'}</div>
209
+ <div style={{ fontSize:13, color:'#7b8fa1' }}>{hasQuery ? 'Try a different search' : 'Check back later'}</div>
138
210
  </div>
139
211
  );
@@ -98,6 +98,8 @@ export interface ChatMessage {
98
98
  attachmentSize?: string;
99
99
  /** Blob URL for attachment download (local send) */
100
100
  attachmentUrl?: string;
101
+ /** e.g. image/png — used for inline image preview in bubbles */
102
+ attachmentMime?: string;
101
103
  voiceDuration?: number; // seconds
102
104
  /** Blob URL for in-bubble audio playback (local recording) */
103
105
  voiceUrl?: string;