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.
Files changed (35) hide show
  1. package/dist/components/ChatScreen/index.d.ts +2 -0
  2. package/dist/components/ChatScreen/index.js +283 -34
  3. package/dist/components/ChatWidget.js +111 -15
  4. package/dist/components/HomeScreen/index.d.ts +2 -0
  5. package/dist/components/HomeScreen/index.js +2 -2
  6. package/dist/components/Tabs/BottomTabs.d.ts +0 -1
  7. package/dist/components/Tabs/BottomTabs.js +13 -20
  8. package/dist/components/TicketDetailScreen/index.d.ts +9 -0
  9. package/dist/components/TicketDetailScreen/index.js +46 -0
  10. package/dist/components/TicketFormScreen/index.d.ts +9 -0
  11. package/dist/components/TicketFormScreen/index.js +76 -0
  12. package/dist/components/TicketScreen/index.d.ts +2 -1
  13. package/dist/components/TicketScreen/index.js +8 -35
  14. package/dist/components/UserListScreen/index.d.ts +4 -0
  15. package/dist/components/UserListScreen/index.js +21 -3
  16. package/dist/types/index.d.ts +3 -1
  17. package/dist/utils/fileName.d.ts +2 -0
  18. package/dist/utils/fileName.js +7 -0
  19. package/dist/utils/messageSound.d.ts +4 -0
  20. package/dist/utils/messageSound.js +51 -0
  21. package/dist/utils/widgetSession.d.ts +13 -0
  22. package/dist/utils/widgetSession.js +24 -0
  23. package/package.json +1 -1
  24. package/src/components/ChatScreen/index.tsx +415 -58
  25. package/src/components/ChatWidget.tsx +140 -17
  26. package/src/components/HomeScreen/index.tsx +4 -2
  27. package/src/components/Tabs/BottomTabs.tsx +2 -22
  28. package/src/components/TicketDetailScreen/index.tsx +111 -0
  29. package/src/components/TicketFormScreen/index.tsx +151 -0
  30. package/src/components/TicketScreen/index.tsx +18 -58
  31. package/src/components/UserListScreen/index.tsx +51 -5
  32. package/src/types/index.ts +4 -0
  33. package/src/utils/fileName.ts +6 -0
  34. package/src/utils/messageSound.ts +47 -0
  35. package/src/utils/widgetSession.ts +34 -0
@@ -1,24 +1,14 @@
1
- import React, { useState } from '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
- onRaiseTicket: (title: string, desc: string, priority: Ticket['priority']) => void;
7
+ onNewTicket: () => void;
8
+ onSelectTicket:(id: string) => void;
8
9
  }
9
10
 
10
- export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onRaiseTicket }) => {
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={() => setShowForm(v => !v)} style={{
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
- {showForm ? '✕ Cancel' : '+ New'}
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
- <div key={t.id} style={{ padding:'14px 16px', borderBottom:'1px solid #f0f2f5', animation:`cw-fadeUp 0.3s ease both`, animationDelay:`${i*0.05}s` }}>
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
- </div>
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>
@@ -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
+ }