ajaxter-chat 3.0.8 → 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.d.ts +2 -0
- package/dist/components/ChatScreen/index.js +283 -34
- package/dist/components/ChatWidget.js +111 -15
- package/dist/components/HomeScreen/index.d.ts +2 -0
- package/dist/components/HomeScreen/index.js +2 -2
- package/dist/components/Tabs/BottomTabs.d.ts +0 -1
- package/dist/components/Tabs/BottomTabs.js +13 -20
- package/dist/components/TicketDetailScreen/index.d.ts +9 -0
- package/dist/components/TicketDetailScreen/index.js +46 -0
- package/dist/components/TicketFormScreen/index.d.ts +9 -0
- package/dist/components/TicketFormScreen/index.js +76 -0
- package/dist/components/TicketScreen/index.d.ts +2 -1
- package/dist/components/TicketScreen/index.js +8 -35
- package/dist/components/UserListScreen/index.d.ts +4 -0
- package/dist/components/UserListScreen/index.js +21 -3
- package/dist/types/index.d.ts +3 -1
- package/dist/utils/fileName.d.ts +2 -0
- package/dist/utils/fileName.js +7 -0
- package/dist/utils/messageSound.d.ts +4 -0
- package/dist/utils/messageSound.js +51 -0
- package/dist/utils/widgetSession.d.ts +13 -0
- package/dist/utils/widgetSession.js +24 -0
- package/package.json +1 -1
- package/src/components/ChatScreen/index.tsx +415 -58
- package/src/components/ChatWidget.tsx +140 -17
- package/src/components/HomeScreen/index.tsx +4 -2
- package/src/components/Tabs/BottomTabs.tsx +2 -22
- package/src/components/TicketDetailScreen/index.tsx +111 -0
- package/src/components/TicketFormScreen/index.tsx +151 -0
- package/src/components/TicketScreen/index.tsx +18 -58
- package/src/components/UserListScreen/index.tsx +51 -5
- package/src/types/index.ts +4 -0
- package/src/utils/fileName.ts +6 -0
- package/src/utils/messageSound.ts +47 -0
- package/src/utils/widgetSession.ts +34 -0
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { Ticket, WidgetConfig } from '../../types';
|
|
3
3
|
|
|
4
4
|
interface TicketScreenProps {
|
|
5
5
|
tickets: Ticket[];
|
|
6
6
|
config: WidgetConfig;
|
|
7
|
-
|
|
7
|
+
onNewTicket: () => void;
|
|
8
|
+
onSelectTicket:(id: string) => void;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config,
|
|
11
|
-
const [showForm, setShowForm] = useState(false);
|
|
12
|
-
const [title, setTitle] = useState('');
|
|
13
|
-
const [desc, setDesc] = useState('');
|
|
14
|
-
const [priority, setPriority] = useState<Ticket['priority']>('medium');
|
|
15
|
-
|
|
16
|
-
const handleSubmit = () => {
|
|
17
|
-
if (!title.trim()) return;
|
|
18
|
-
onRaiseTicket(title.trim(), desc.trim(), priority);
|
|
19
|
-
setTitle(''); setDesc(''); setPriority('medium'); setShowForm(false);
|
|
20
|
-
};
|
|
21
|
-
|
|
11
|
+
export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onNewTicket, onSelectTicket }) => {
|
|
22
12
|
const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
|
|
23
13
|
open: { label:'Open', bg:`${config.primaryColor}14`, color: config.primaryColor },
|
|
24
14
|
'in-progress': { label:'In Progress', bg:'#fef3c7', color:'#d97706' },
|
|
@@ -34,7 +24,6 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onR
|
|
|
34
24
|
|
|
35
25
|
return (
|
|
36
26
|
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
|
|
37
|
-
{/* Header */}
|
|
38
27
|
<div style={{
|
|
39
28
|
background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
40
29
|
padding:'18px 18px 22px', flexShrink:0, position:'relative',
|
|
@@ -46,46 +35,16 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onR
|
|
|
46
35
|
{tickets.length} ticket{tickets.length!==1?'s':''} raised
|
|
47
36
|
</p>
|
|
48
37
|
</div>
|
|
49
|
-
<button onClick={
|
|
38
|
+
<button type="button" onClick={onNewTicket} style={{
|
|
50
39
|
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:20,
|
|
51
40
|
padding:'7px 14px', color:'#fff', fontWeight:700, fontSize:13,
|
|
52
41
|
cursor:'pointer', display:'flex', alignItems:'center', gap:5,
|
|
53
42
|
}}>
|
|
54
|
-
|
|
43
|
+
+ New
|
|
55
44
|
</button>
|
|
56
45
|
</div>
|
|
57
46
|
</div>
|
|
58
47
|
|
|
59
|
-
{/* New Ticket Form */}
|
|
60
|
-
{showForm && (
|
|
61
|
-
<div style={{ padding:'16px', borderBottom:'1px solid #eef0f5', background:'#fafcff', flexShrink:0, animation:'cw-fadeUp 0.2s ease' }}>
|
|
62
|
-
<input placeholder="Title *" value={title} onChange={e => setTitle(e.target.value)}
|
|
63
|
-
style={inputStyle(config.primaryColor)} onFocus={e=>(e.target.style.borderColor=config.primaryColor)} onBlur={e=>(e.target.style.borderColor='#e5e7eb')}
|
|
64
|
-
/>
|
|
65
|
-
<textarea placeholder="Describe the issue…" value={desc} onChange={e => setDesc(e.target.value)} rows={3}
|
|
66
|
-
style={{ ...inputStyle(config.primaryColor), resize:'none', marginTop:8 }} onFocus={e=>(e.target.style.borderColor=config.primaryColor)} onBlur={e=>(e.target.style.borderColor='#e5e7eb')}
|
|
67
|
-
/>
|
|
68
|
-
{/* Priority */}
|
|
69
|
-
<div style={{ display:'flex', gap:8, marginTop:8, marginBottom:12 }}>
|
|
70
|
-
{(['low','medium','high'] as Ticket['priority'][]).map(p => (
|
|
71
|
-
<button key={p} onClick={() => setPriority(p)} style={{
|
|
72
|
-
flex:1, padding:'6px', border:`1.5px solid ${priority===p ? pm[p].color : '#e5e7eb'}`,
|
|
73
|
-
borderRadius:8, background: priority===p ? pm[p].color+'15' : '#fff',
|
|
74
|
-
color: pm[p].color, fontWeight:700, fontSize:12, cursor:'pointer', textTransform:'capitalize',
|
|
75
|
-
transition:'all 0.15s',
|
|
76
|
-
}}>{pm[p].label}</button>
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
79
|
-
<button onClick={handleSubmit} disabled={!title.trim()} style={{
|
|
80
|
-
width:'100%', padding:'10px', borderRadius:10, border:'none',
|
|
81
|
-
background: title.trim() ? config.primaryColor : '#e5e7eb',
|
|
82
|
-
color: title.trim() ? '#fff' : '#9ca3af',
|
|
83
|
-
fontWeight:700, fontSize:14, cursor: title.trim() ? 'pointer' : 'not-allowed',
|
|
84
|
-
}}>Submit Ticket</button>
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
|
|
88
|
-
{/* List */}
|
|
89
48
|
<div style={{ flex:1, overflowY:'auto' }}>
|
|
90
49
|
{tickets.length === 0 ? (
|
|
91
50
|
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
@@ -94,7 +53,17 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onR
|
|
|
94
53
|
<div style={{ fontSize:13, color:'#7b8fa1' }}>Raise a ticket for major issues</div>
|
|
95
54
|
</div>
|
|
96
55
|
) : tickets.map((t, i) => (
|
|
97
|
-
<
|
|
56
|
+
<button
|
|
57
|
+
key={t.id}
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => onSelectTicket(t.id)}
|
|
60
|
+
style={{
|
|
61
|
+
width:'100%', padding:'14px 16px', borderBottom:'1px solid #f0f2f5',
|
|
62
|
+
animation:`cw-fadeUp 0.3s ease both`, animationDelay:`${i*0.05}s`,
|
|
63
|
+
background:'transparent', borderLeft:'none', borderRight:'none', borderTop:'none',
|
|
64
|
+
cursor:'pointer', textAlign:'left', fontFamily:'inherit',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
98
67
|
<div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', marginBottom:5 }}>
|
|
99
68
|
<span style={{ fontWeight:700, fontSize:14, color:'#1a2332', flex:1, paddingRight:10 }}>{t.title}</span>
|
|
100
69
|
<span style={{ fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20, backgroundColor:sm[t.status].bg, color:sm[t.status].color, whiteSpace:'nowrap', textTransform:'uppercase', letterSpacing:'0.04em', flexShrink:0 }}>
|
|
@@ -107,18 +76,9 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onR
|
|
|
107
76
|
<span>#{t.id}</span>
|
|
108
77
|
<span>{new Date(t.createdAt).toLocaleDateString([], { month:'short', day:'numeric' })}</span>
|
|
109
78
|
</div>
|
|
110
|
-
</
|
|
79
|
+
</button>
|
|
111
80
|
))}
|
|
112
81
|
</div>
|
|
113
82
|
</div>
|
|
114
83
|
);
|
|
115
84
|
};
|
|
116
|
-
|
|
117
|
-
function inputStyle(primaryColor: string): React.CSSProperties {
|
|
118
|
-
return {
|
|
119
|
-
width:'100%', padding:'9px 13px', borderRadius:10,
|
|
120
|
-
border:'1.5px solid #e5e7eb', outline:'none',
|
|
121
|
-
fontSize:14, color:'#1a2332', boxSizing:'border-box',
|
|
122
|
-
fontFamily:'inherit', transition:'border-color 0.2s',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
@@ -10,10 +10,15 @@ interface UserListScreenProps {
|
|
|
10
10
|
viewerType?: 'user' | 'developer';
|
|
11
11
|
onBack: () => void;
|
|
12
12
|
onSelectUser: (user: ChatUser) => void;
|
|
13
|
+
/** Shown on “New Conversation” list — opens block list */
|
|
14
|
+
onBlockList?: () => void;
|
|
15
|
+
/** “Need Support” (user → agents): show home icon instead of back arrow */
|
|
16
|
+
useHomeHeader?: boolean;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
16
|
-
context, users, primaryColor, viewerType = 'user', onBack, onSelectUser,
|
|
20
|
+
context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList,
|
|
21
|
+
useHomeHeader = false,
|
|
17
22
|
}) => {
|
|
18
23
|
const isStaff = viewerType === 'developer';
|
|
19
24
|
const title = context === 'support'
|
|
@@ -26,12 +31,39 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
26
31
|
return (
|
|
27
32
|
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
|
|
28
33
|
{/* Header */}
|
|
29
|
-
<div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0 }}>
|
|
30
|
-
<BackBtn onClick={onBack} />
|
|
31
|
-
<div>
|
|
34
|
+
<div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0, position:'relative' }}>
|
|
35
|
+
{useHomeHeader ? <HomeBtn onClick={onBack} /> : <BackBtn onClick={onBack} />}
|
|
36
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
32
37
|
<div style={{ fontWeight:700, fontSize:16, color:'#fff' }}>{title}</div>
|
|
33
38
|
<div style={{ fontSize:12, color:'rgba(255,255,255,0.8)' }}>{subtitle}</div>
|
|
34
39
|
</div>
|
|
40
|
+
{context === 'conversation' && onBlockList && (
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onBlockList}
|
|
44
|
+
style={{
|
|
45
|
+
flexShrink: 0,
|
|
46
|
+
background: 'rgba(255,255,255,0.2)',
|
|
47
|
+
border: 'none',
|
|
48
|
+
borderRadius: 10,
|
|
49
|
+
padding: '8px 12px',
|
|
50
|
+
color: '#fff',
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
fontWeight: 700,
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
display: 'flex',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: 6,
|
|
57
|
+
}}
|
|
58
|
+
title="Blocked users"
|
|
59
|
+
>
|
|
60
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
61
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.8" />
|
|
62
|
+
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
63
|
+
</svg>
|
|
64
|
+
Blocked
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
35
67
|
</div>
|
|
36
68
|
|
|
37
69
|
{/* User list */}
|
|
@@ -90,7 +122,7 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
|
90
122
|
};
|
|
91
123
|
|
|
92
124
|
const BackBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
93
|
-
<button onClick={onClick} style={{
|
|
125
|
+
<button type="button" onClick={onClick} style={{
|
|
94
126
|
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:'50%',
|
|
95
127
|
width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
|
|
96
128
|
cursor:'pointer', flexShrink:0,
|
|
@@ -101,6 +133,20 @@ const BackBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
|
101
133
|
</button>
|
|
102
134
|
);
|
|
103
135
|
|
|
136
|
+
const HomeBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
137
|
+
<button type="button" onClick={onClick} title="Home" aria-label="Home" style={{
|
|
138
|
+
background:'rgba(255,255,255,0.22)', border:'none', borderRadius:'50%',
|
|
139
|
+
width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center',
|
|
140
|
+
cursor:'pointer', flexShrink:0,
|
|
141
|
+
}}>
|
|
142
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
143
|
+
<path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"
|
|
144
|
+
stroke="#fff" strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
145
|
+
<path d="M9 21V12h6v9" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
149
|
+
|
|
104
150
|
const Empty: React.FC = () => (
|
|
105
151
|
<div style={{ padding:'50px 24px', textAlign:'center' }}>
|
|
106
152
|
<div style={{ fontSize:36, marginBottom:10 }}>👥</div>
|
package/src/types/index.ts
CHANGED
|
@@ -65,6 +65,8 @@ export type Screen =
|
|
|
65
65
|
| 'chat'
|
|
66
66
|
| 'recent-chats'
|
|
67
67
|
| 'tickets'
|
|
68
|
+
| 'ticket-new'
|
|
69
|
+
| 'ticket-detail'
|
|
68
70
|
| 'block-list'
|
|
69
71
|
| 'call';
|
|
70
72
|
export type UserListContext = 'support' | 'conversation';
|
|
@@ -96,6 +98,8 @@ export interface ChatMessage {
|
|
|
96
98
|
attachmentSize?: string;
|
|
97
99
|
/** Blob URL for attachment download (local send) */
|
|
98
100
|
attachmentUrl?: string;
|
|
101
|
+
/** e.g. image/png — used for inline image preview in bubbles */
|
|
102
|
+
attachmentMime?: string;
|
|
99
103
|
voiceDuration?: number; // seconds
|
|
100
104
|
/** Blob URL for in-bubble audio playback (local recording) */
|
|
101
105
|
voiceUrl?: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Display name: first 10 chars + "[...]" if longer; full name in tooltip via title */
|
|
2
|
+
export function shortAttachmentLabel(fileName: string, maxChars = 10): string {
|
|
3
|
+
const n = fileName.trim();
|
|
4
|
+
if (n.length <= maxChars) return n;
|
|
5
|
+
return `${n.slice(0, maxChars)}[...]`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Short notification tone for incoming chat messages */
|
|
2
|
+
export function playMessageSound(): void {
|
|
3
|
+
try {
|
|
4
|
+
const AC = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
|
5
|
+
if (!AC) return;
|
|
6
|
+
const ctx = new AC();
|
|
7
|
+
const o = ctx.createOscillator();
|
|
8
|
+
const g = ctx.createGain();
|
|
9
|
+
o.connect(g);
|
|
10
|
+
g.connect(ctx.destination);
|
|
11
|
+
o.type = 'sine';
|
|
12
|
+
o.frequency.value = 880;
|
|
13
|
+
g.gain.value = 0.07;
|
|
14
|
+
o.start();
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
try {
|
|
17
|
+
o.stop();
|
|
18
|
+
void ctx.close();
|
|
19
|
+
} catch {
|
|
20
|
+
/* ignore */
|
|
21
|
+
}
|
|
22
|
+
}, 100);
|
|
23
|
+
} catch {
|
|
24
|
+
/* ignore */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const soundPrefKey = (widgetId: string) => `ajaxter_sound_${widgetId}`;
|
|
29
|
+
|
|
30
|
+
export function getMessageSoundEnabled(widgetId: string): boolean {
|
|
31
|
+
if (typeof window === 'undefined') return true;
|
|
32
|
+
try {
|
|
33
|
+
const v = localStorage.getItem(soundPrefKey(widgetId));
|
|
34
|
+
if (v === null) return true;
|
|
35
|
+
return v === '1';
|
|
36
|
+
} catch {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setMessageSoundEnabled(widgetId: string, enabled: boolean): void {
|
|
42
|
+
try {
|
|
43
|
+
localStorage.setItem(soundPrefKey(widgetId), enabled ? '1' : '0');
|
|
44
|
+
} catch {
|
|
45
|
+
/* ignore */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Screen, BottomTab, UserListContext, ChatMessage } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface PersistedWidgetSession {
|
|
4
|
+
screen: Screen;
|
|
5
|
+
activeTab: BottomTab;
|
|
6
|
+
userListCtx: UserListContext;
|
|
7
|
+
activeUserUid: string | null;
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
viewingTicketId: string | null;
|
|
10
|
+
chatReturnCtx: UserListContext;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sessionKey(widgetId: string): string {
|
|
14
|
+
return `ajaxter_widget_session_${widgetId}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadSession(widgetId: string): PersistedWidgetSession | null {
|
|
18
|
+
if (typeof window === 'undefined') return null;
|
|
19
|
+
try {
|
|
20
|
+
const raw = sessionStorage.getItem(sessionKey(widgetId));
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
return JSON.parse(raw) as PersistedWidgetSession;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveSession(widgetId: string, state: PersistedWidgetSession): void {
|
|
29
|
+
try {
|
|
30
|
+
sessionStorage.setItem(sessionKey(widgetId), JSON.stringify(state));
|
|
31
|
+
} catch {
|
|
32
|
+
/* quota */
|
|
33
|
+
}
|
|
34
|
+
}
|