ajaxter-chat 1.0.3 → 3.0.1
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/README.md +124 -241
- package/dist/components/BlockList/index.d.ts +10 -0
- package/dist/components/BlockList/index.js +33 -0
- package/dist/components/CallScreen/index.d.ts +13 -0
- package/dist/components/CallScreen/index.js +48 -0
- package/dist/components/ChatScreen/index.d.ts +19 -0
- package/dist/components/ChatScreen/index.js +168 -0
- package/dist/components/ChatWidget.d.ts +0 -24
- package/dist/components/ChatWidget.js +228 -43
- package/dist/components/EmojiPicker/index.d.ts +8 -0
- package/dist/components/EmojiPicker/index.js +18 -0
- package/dist/components/HomeScreen/index.d.ts +8 -0
- package/dist/components/HomeScreen/index.js +55 -0
- package/dist/components/MaintenanceView/index.d.ts +0 -1
- package/dist/components/MaintenanceView/index.js +13 -52
- package/dist/components/RecentChatsScreen/index.d.ts +17 -0
- package/dist/components/RecentChatsScreen/index.js +8 -0
- package/dist/components/Tabs/BottomTabs.d.ts +10 -0
- package/dist/components/Tabs/BottomTabs.js +34 -0
- package/dist/components/TicketScreen/index.d.ts +9 -0
- package/dist/components/TicketScreen/index.js +54 -0
- package/dist/components/UserListScreen/index.d.ts +11 -0
- package/dist/components/UserListScreen/index.js +35 -0
- package/dist/config/index.d.ts +3 -16
- package/dist/config/index.js +20 -103
- package/dist/hooks/useChat.d.ts +10 -9
- package/dist/hooks/useChat.js +22 -40
- package/dist/hooks/useRemoteConfig.d.ts +6 -0
- package/dist/hooks/useRemoteConfig.js +22 -0
- package/dist/hooks/useWebRTC.d.ts +11 -0
- package/dist/hooks/useWebRTC.js +112 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +15 -16
- package/dist/types/index.d.ts +66 -38
- package/dist/utils/chat.d.ts +13 -0
- package/dist/utils/chat.js +62 -0
- package/dist/utils/theme.d.ts +3 -2
- package/dist/utils/theme.js +13 -21
- package/package.json +10 -20
- package/public/chatData.json +162 -0
- package/src/components/BlockList/index.tsx +94 -0
- package/src/components/CallScreen/index.tsx +144 -0
- package/src/components/ChatScreen/index.tsx +469 -0
- package/src/components/ChatWidget.tsx +471 -0
- package/src/components/EmojiPicker/index.tsx +48 -0
- package/src/components/HomeScreen/index.tsx +106 -0
- package/src/components/MaintenanceView/index.tsx +38 -0
- package/src/components/RecentChatsScreen/index.tsx +63 -0
- package/src/components/Tabs/BottomTabs.tsx +90 -0
- package/src/components/TicketScreen/index.tsx +124 -0
- package/src/components/UserListScreen/index.tsx +103 -0
- package/src/config/index.ts +40 -0
- package/src/hooks/useChat.ts +48 -0
- package/src/hooks/useRemoteConfig.ts +20 -0
- package/src/hooks/useWebRTC.ts +130 -0
- package/src/index.ts +29 -0
- package/src/types/index.ts +127 -0
- package/src/utils/chat.ts +70 -0
- package/src/utils/theme.ts +27 -0
- package/dist/components/BottomNav/index.d.ts +0 -10
- package/dist/components/BottomNav/index.js +0 -32
- package/dist/components/ChatBox/index.d.ts +0 -15
- package/dist/components/ChatBox/index.js +0 -228
- package/dist/components/ChatButton/index.d.ts +0 -9
- package/dist/components/ChatButton/index.js +0 -17
- package/dist/components/ChatWindow/index.d.ts +0 -10
- package/dist/components/ChatWindow/index.js +0 -286
- package/dist/components/HomeView/index.d.ts +0 -12
- package/dist/components/HomeView/index.js +0 -51
- package/dist/components/UserList/index.d.ts +0 -13
- package/dist/components/UserList/index.js +0 -136
- package/dist/hooks/useUsers.d.ts +0 -14
- package/dist/hooks/useUsers.js +0 -32
- package/dist/services/userService.d.ts +0 -7
- package/dist/services/userService.js +0 -18
package/README.md
CHANGED
|
@@ -1,303 +1,186 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-chat-widget-extension v3
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
- ✅ Environment-variable-driven behavior (status, type, API endpoint)
|
|
6
|
-
- ✅ Fully themeable via a `theme` prop (colors, fonts, button, position)
|
|
7
|
-
- ✅ SSR-safe — works with Next.js App Router and Pages Router
|
|
8
|
-
- ✅ TypeScript-first
|
|
9
|
-
- ✅ WebSocket-ready architecture
|
|
10
|
-
- ✅ Loading skeletons, empty states, error handling built in
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Folder Structure
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
my_first_project/
|
|
18
|
-
├── src/
|
|
19
|
-
│ ├── index.ts # Public API exports
|
|
20
|
-
│ ├── types/
|
|
21
|
-
│ │ └── index.ts # All TypeScript types & interfaces
|
|
22
|
-
│ ├── config/
|
|
23
|
-
│ │ └── index.ts # Env variable loader (Next.js + React safe)
|
|
24
|
-
│ ├── services/
|
|
25
|
-
│ │ └── userService.ts # Fetch users from API
|
|
26
|
-
│ ├── hooks/
|
|
27
|
-
│ │ ├── useUsers.ts # Fetch & filter users hook
|
|
28
|
-
│ │ └── useChat.ts # Chat state & message management hook
|
|
29
|
-
│ ├── utils/
|
|
30
|
-
│ │ └── theme.ts # Theme merging & CSS variable utilities
|
|
31
|
-
│ └── components/
|
|
32
|
-
│ ├── ChatWidget.tsx # 🏠 Root widget — mount this in your app
|
|
33
|
-
│ ├── ChatButton/
|
|
34
|
-
│ │ └── index.tsx # Floating action button
|
|
35
|
-
│ ├── ChatWindow/
|
|
36
|
-
│ │ └── index.tsx # Expandable chat panel
|
|
37
|
-
│ ├── UserList/
|
|
38
|
-
│ │ └── index.tsx # User list with loading/error/empty states
|
|
39
|
-
│ ├── ChatBox/
|
|
40
|
-
│ │ └── index.tsx # Conversation panel + message input
|
|
41
|
-
│ └── MaintenanceView/
|
|
42
|
-
│ └── index.tsx # Shown when CHAT_STATUS=MAINTENANCE
|
|
43
|
-
├── examples/
|
|
44
|
-
│ ├── react-app/
|
|
45
|
-
│ │ ├── .env.example # React env variables template
|
|
46
|
-
│ │ └── App.tsx # React usage example
|
|
47
|
-
│ └── nextjs-app/
|
|
48
|
-
│ ├── .env.local.example # Next.js env variables template
|
|
49
|
-
│ ├── app/
|
|
50
|
-
│ │ ├── layout.tsx # App Router: root layout
|
|
51
|
-
│ │ ├── ChatWidgetWrapper.tsx # App Router: 'use client' boundary
|
|
52
|
-
│ │ └── page.tsx # App Router: home page
|
|
53
|
-
│ └── pages/
|
|
54
|
-
│ ├── _app.tsx # Pages Router: global app
|
|
55
|
-
│ └── index.tsx # Pages Router: index page
|
|
56
|
-
├── package.json
|
|
57
|
-
├── tsconfig.json
|
|
58
|
-
└── README.md
|
|
59
|
-
```
|
|
3
|
+
A production-ready **drawer/slider-based** chat widget for React.js and Next.js.
|
|
4
|
+
All configuration is loaded remotely from your hosted `chatData.json`.
|
|
60
5
|
|
|
61
6
|
---
|
|
62
7
|
|
|
63
|
-
##
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npm install ajaxter-chat
|
|
67
|
-
# or
|
|
68
|
-
yarn add ajaxter-chat
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## Environment Variables
|
|
74
|
-
|
|
75
|
-
### React.js (.env)
|
|
8
|
+
## Setup (2 env vars only)
|
|
76
9
|
|
|
10
|
+
### React (.env)
|
|
77
11
|
```env
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
REACT_APP_CHAT_USER_LIST=api/v1/chat/users
|
|
81
|
-
REACT_APP_CHAT_STATUS=ACTIVE
|
|
82
|
-
REACT_APP_CHAT_TYPE=BOTH
|
|
12
|
+
REACT_APP_CHAT_API_KEY=demo1234
|
|
13
|
+
REACT_APP_CHAT_WIDGET_ID=demo
|
|
83
14
|
```
|
|
84
15
|
|
|
85
16
|
### Next.js (.env.local)
|
|
86
|
-
|
|
87
17
|
```env
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
NEXT_PUBLIC_CHAT_USER_LIST=api/v1/chat/users
|
|
91
|
-
NEXT_PUBLIC_CHAT_STATUS=ACTIVE
|
|
92
|
-
NEXT_PUBLIC_CHAT_TYPE=BOTH
|
|
18
|
+
NEXT_PUBLIC_CHAT_API_KEY=demo1234
|
|
19
|
+
NEXT_PUBLIC_CHAT_WIDGET_ID=demo
|
|
93
20
|
```
|
|
94
21
|
|
|
95
|
-
### Variable Reference
|
|
96
|
-
|
|
97
|
-
| Variable | Type | Description |
|
|
98
|
-
|--------------------|---------------------------------------|------------------------------------------|
|
|
99
|
-
| `CHAT_HOST_URL` | string | Base URL of your chat/user API |
|
|
100
|
-
| `CHAT_HOST_PORT` | number | Port for your API server |
|
|
101
|
-
| `CHAT_USER_LIST` | string | User list URL — see **User List API** below |
|
|
102
|
-
| `CHAT_STATUS` | `ACTIVE` \| `DISABLE` \| `MAINTENANCE` | Controls widget visibility & state |
|
|
103
|
-
| `CHAT_TYPE` | `SUPPORT` \| `CHAT` \| `BOTH` | Controls which users are shown |
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## CHAT_STATUS Behavior
|
|
108
|
-
|
|
109
|
-
| Value | Behavior |
|
|
110
|
-
|-----------------|---------------------------------------------------------------|
|
|
111
|
-
| `ACTIVE` | Widget is fully enabled |
|
|
112
|
-
| `DISABLE` | Widget is **not rendered at all** — zero DOM footprint |
|
|
113
|
-
| `MAINTENANCE` | Widget opens but shows a maintenance message (non-interactive)|
|
|
114
|
-
|
|
115
22
|
---
|
|
116
23
|
|
|
117
|
-
##
|
|
118
|
-
|
|
119
|
-
| Value | User List Shown | UI |
|
|
120
|
-
|-----------|------------------------------------------|-----------------------------|
|
|
121
|
-
| `SUPPORT` | Only `type: "developer"` users | Single panel |
|
|
122
|
-
| `CHAT` | Only `type: "user"` users | Single panel |
|
|
123
|
-
| `BOTH` | Both developers and users | Two tabs (Support / Users) |
|
|
124
|
-
|
|
125
|
-
---
|
|
24
|
+
## Remote Config URL
|
|
126
25
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
The widget calls `CHAT_USER_LIST` in three ways:
|
|
130
|
-
|
|
131
|
-
1. **Same-origin / BFF (recommended)** — value starts with `/`, e.g. `/api/v1/chat/users`. The browser only shows a request to **your** app; your route handler proxies to the real API server-side, so the upstream URL (e.g. `http://your-api.com:4000/...`) does not appear as the client request URL in DevTools.
|
|
132
|
-
2. **Full URL** — value starts with `http://` or `https://`; that exact URL is fetched (visible in Network).
|
|
133
|
-
3. **Legacy** — otherwise it is built as
|
|
134
|
-
`CHAT_HOST_URL` + optional `:CHAT_HOST_PORT` + path.
|
|
135
|
-
|
|
136
|
-
Example (Next.js route at `app/api/v1/chat/users/route.ts` that forwards to your backend):
|
|
137
|
-
|
|
138
|
-
```env
|
|
139
|
-
NEXT_PUBLIC_CHAT_USER_LIST=/api/v1/chat/users
|
|
26
|
+
The widget fetches all config from:
|
|
140
27
|
```
|
|
141
|
-
|
|
142
|
-
Legacy example:
|
|
143
|
-
|
|
28
|
+
GET https://window.mscorpres.com/TEST/chatData.json
|
|
144
29
|
```
|
|
145
|
-
|
|
30
|
+
With headers:
|
|
146
31
|
```
|
|
32
|
+
X-Chat-Api-Key: demo1234
|
|
33
|
+
X-Chat-Widget-Id: demo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## chatData.json Schema
|
|
147
39
|
|
|
148
|
-
|
|
40
|
+
Host this file at the URL above:
|
|
149
41
|
|
|
150
42
|
```json
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
43
|
+
{
|
|
44
|
+
"widget": {
|
|
45
|
+
"id": "demo",
|
|
46
|
+
"apiKey": "demo1234",
|
|
47
|
+
"status": "ACTIVE",
|
|
48
|
+
"chatType": "BOTH",
|
|
49
|
+
"primaryColor": "#2563EB",
|
|
50
|
+
"buttonLabel": "Support",
|
|
51
|
+
"buttonPosition": "bottom-right",
|
|
52
|
+
"welcomeTitle": "Hi there 👋",
|
|
53
|
+
"welcomeSubtitle": "Need help? Start a conversation:",
|
|
54
|
+
"allowVoiceMessage": true,
|
|
55
|
+
"allowAttachment": true,
|
|
56
|
+
"allowEmoji": true,
|
|
57
|
+
"allowWebCall": true,
|
|
58
|
+
"maxEmojiCount": 20,
|
|
59
|
+
"allowTranscriptDownload": true,
|
|
60
|
+
"allowReport": true,
|
|
61
|
+
"allowBlock": true
|
|
159
62
|
},
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"type": "user"
|
|
167
|
-
}
|
|
168
|
-
]
|
|
63
|
+
"developers": [ { "uid":"dev_001","name":"...","type":"developer","status":"online", ... } ],
|
|
64
|
+
"users": [ { "uid":"usr_001","name":"...","type":"user","status":"online", ... } ],
|
|
65
|
+
"sampleChats": { "dev_001": [ { "id":"msg_001","senderId":"me","text":"Hi!","type":"text", ... } ] },
|
|
66
|
+
"sampleTickets": [ { "id":"TKT-0001","title":"...","status":"open","priority":"high", ... } ],
|
|
67
|
+
"blockedUsers": []
|
|
68
|
+
}
|
|
169
69
|
```
|
|
170
70
|
|
|
171
71
|
---
|
|
172
72
|
|
|
173
73
|
## Usage
|
|
174
74
|
|
|
175
|
-
### React
|
|
176
|
-
|
|
75
|
+
### React
|
|
177
76
|
```tsx
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
function App() {
|
|
182
|
-
return (
|
|
183
|
-
<div>
|
|
184
|
-
<main>Your app content</main>
|
|
185
|
-
|
|
186
|
-
<ChatWidget
|
|
187
|
-
theme={{
|
|
188
|
-
primaryColor: '#6C63FF',
|
|
189
|
-
buttonColor: '#6C63FF',
|
|
190
|
-
buttonTextColor: '#ffffff',
|
|
191
|
-
buttonLabel: 'Chat with us',
|
|
192
|
-
buttonPosition: 'bottom-right',
|
|
193
|
-
fontFamily: "'DM Sans', sans-serif",
|
|
194
|
-
borderRadius: '16px',
|
|
195
|
-
}}
|
|
196
|
-
/>
|
|
197
|
-
</div>
|
|
198
|
-
);
|
|
77
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
78
|
+
export default function App() {
|
|
79
|
+
return <><main>App</main><ChatWidget /></>;
|
|
199
80
|
}
|
|
200
81
|
```
|
|
201
82
|
|
|
202
|
-
### Next.js
|
|
203
|
-
|
|
83
|
+
### Next.js App Router
|
|
204
84
|
```tsx
|
|
205
85
|
// app/ChatWidgetWrapper.tsx
|
|
206
86
|
'use client';
|
|
207
|
-
import { ChatWidget } from '
|
|
208
|
-
|
|
209
|
-
export function ChatWidgetWrapper() {
|
|
210
|
-
return <ChatWidget theme={{ primaryColor: '#0ea5e9' }} />;
|
|
211
|
-
}
|
|
212
|
-
```
|
|
87
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
88
|
+
export function ChatWidgetWrapper() { return <ChatWidget />; }
|
|
213
89
|
|
|
214
|
-
```tsx
|
|
215
90
|
// app/layout.tsx
|
|
216
91
|
import { ChatWidgetWrapper } from './ChatWidgetWrapper';
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
<html lang="en">
|
|
221
|
-
<body>
|
|
222
|
-
{children}
|
|
223
|
-
<ChatWidgetWrapper />
|
|
224
|
-
</body>
|
|
225
|
-
</html>
|
|
226
|
-
);
|
|
92
|
+
export default function Layout({ children }) {
|
|
93
|
+
return <html><body>{children}<ChatWidgetWrapper /></body></html>;
|
|
227
94
|
}
|
|
228
95
|
```
|
|
229
96
|
|
|
230
|
-
### Next.js
|
|
231
|
-
|
|
97
|
+
### Next.js Pages Router
|
|
232
98
|
```tsx
|
|
233
99
|
// pages/_app.tsx
|
|
234
|
-
import { ChatWidget } from '
|
|
235
|
-
|
|
100
|
+
import { ChatWidget } from 'react-chat-widget-extension';
|
|
236
101
|
export default function MyApp({ Component, pageProps }) {
|
|
237
|
-
return
|
|
238
|
-
<>
|
|
239
|
-
<Component {...pageProps} />
|
|
240
|
-
<ChatWidget theme={{ primaryColor: '#10b981' }} />
|
|
241
|
-
</>
|
|
242
|
-
);
|
|
102
|
+
return <><Component {...pageProps}/><ChatWidget /></>;
|
|
243
103
|
}
|
|
244
104
|
```
|
|
245
105
|
|
|
246
106
|
---
|
|
247
107
|
|
|
248
|
-
##
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
| Feature | Details |
|
|
111
|
+
|---|---|
|
|
112
|
+
| **Drawer/Slider UI** | Slides in from right (or left) with smooth animation. Backdrop closes it. |
|
|
113
|
+
| **Home Screen** | "Hi there 👋" hero, cards for Support / New Conversation / Raise Ticket |
|
|
114
|
+
| **User List** | Slide-in panel with online status dot, designation, project |
|
|
115
|
+
| **Chat Screen** | Matches the UI image — hamburger back, title "Support", phone + fullscreen icons |
|
|
116
|
+
| **Enter Your Details** | Blue card at top of chat (ticket chat body placeholder) |
|
|
117
|
+
| **Voice Messages** | Record via MediaRecorder API, shows duration |
|
|
118
|
+
| **Attachments** | File picker, shows filename + size as message |
|
|
119
|
+
| **Emoji Picker** | 20 curated emojis, limited count via `maxEmojiCount` |
|
|
120
|
+
| **Pause Chat** | Developer can pause — user cannot send messages |
|
|
121
|
+
| **Resume Chat** | One-click resume from banner or menu |
|
|
122
|
+
| **Report Chat** | Flags chat, shows warning banner |
|
|
123
|
+
| **Block User** | Blocks users only (not developers), with confirm dialog |
|
|
124
|
+
| **Block List Tab** | View all blocked users, one-click unblock |
|
|
125
|
+
| **Download Transcript** | Saves plain-text .txt file of full conversation |
|
|
126
|
+
| **WebRTC Calls** | Secure P2P voice/video call with mute + camera toggle |
|
|
127
|
+
| **Ticket Screen** | Raise tickets with title, description, priority selector |
|
|
128
|
+
| **Ticket History** | Shows all tickets with status badge and priority color |
|
|
129
|
+
| **Recent Chats Tab** | Shows past conversations with unread badges |
|
|
130
|
+
| **Maximize/Minimize** | Toggle drawer width 380px ↔ 480px |
|
|
131
|
+
| **Slide Close** | Smooth slide-out animation on close |
|
|
132
|
+
| **SSR Safe** | isMounted guard, works in Next.js App Router and Pages Router |
|
|
133
|
+
| **CHAT_STATUS** | ACTIVE / DISABLE / MAINTENANCE handled |
|
|
264
134
|
|
|
265
135
|
---
|
|
266
136
|
|
|
267
|
-
## WebSocket Integration
|
|
268
|
-
|
|
269
|
-
The `useChat` hook is ready for WebSocket integration. Look for the `TODO` comments in:
|
|
270
|
-
|
|
271
|
-
- `src/hooks/useChat.ts` → Add `socket.emit('message', newMsg)` in `sendMessage`
|
|
272
|
-
- `src/hooks/useChat.ts` → Add `socket.on('message', ...)` listener in `selectUser`
|
|
273
|
-
- `src/components/ChatWindow/index.tsx` → Initialize socket connection on mount
|
|
274
|
-
|
|
275
|
-
Example with socket.io:
|
|
137
|
+
## WebSocket Integration Points
|
|
276
138
|
|
|
139
|
+
In `src/hooks/useChat.ts` — find the `// TODO:` comments:
|
|
277
140
|
```ts
|
|
278
|
-
// In
|
|
279
|
-
socket.emit('
|
|
141
|
+
// In selectUser:
|
|
142
|
+
// socket.emit('join', { roomId: user.uid });
|
|
143
|
+
// socket.on('message', msg => setMessages(prev => [...prev, msg]));
|
|
280
144
|
|
|
281
|
-
// In
|
|
282
|
-
socket.
|
|
283
|
-
setMessages((prev) => [...prev, msg]);
|
|
284
|
-
});
|
|
145
|
+
// In sendMessage:
|
|
146
|
+
// socket.emit('message', msg);
|
|
285
147
|
```
|
|
286
148
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
cd my_first_project
|
|
293
|
-
npm install
|
|
294
|
-
npm run build # Compile TypeScript → dist/
|
|
295
|
-
npm run type-check # Verify types without emitting
|
|
296
|
-
npm run dev # Watch mode
|
|
149
|
+
In `src/hooks/useWebRTC.ts` — find the `// TODO:` comments:
|
|
150
|
+
```ts
|
|
151
|
+
// ICE candidate: socket.emit('ice-candidate', { candidate, to: peer.uid });
|
|
152
|
+
// Offer: socket.emit('call-offer', { offer, to: peer.uid });
|
|
153
|
+
// Answer: socket.emit('call-answer', { answer, to: peer.uid });
|
|
297
154
|
```
|
|
298
155
|
|
|
299
156
|
---
|
|
300
157
|
|
|
301
|
-
##
|
|
158
|
+
## Folder Structure
|
|
302
159
|
|
|
303
|
-
|
|
160
|
+
```
|
|
161
|
+
src/
|
|
162
|
+
├── index.ts
|
|
163
|
+
├── types/index.ts
|
|
164
|
+
├── config/index.ts ← reads CHAT_API_KEY + CHAT_WIDGET_ID, fetches chatData.json
|
|
165
|
+
├── utils/theme.ts
|
|
166
|
+
├── utils/chat.ts ← transcript, avatarColor, formatTime, downloadText
|
|
167
|
+
├── hooks/
|
|
168
|
+
│ ├── useRemoteConfig.ts ← fetches chatData.json
|
|
169
|
+
│ ├── useChat.ts ← messages, pause, report
|
|
170
|
+
│ └── useWebRTC.ts ← P2P WebRTC calling
|
|
171
|
+
└── components/
|
|
172
|
+
├── ChatWidget.tsx ← 🏠 Root drawer orchestrator
|
|
173
|
+
├── HomeScreen/
|
|
174
|
+
├── UserListScreen/
|
|
175
|
+
├── ChatScreen/ ← voice, attach, emoji, pause, report, block, transcript, call
|
|
176
|
+
├── CallScreen/ ← WebRTC video/audio UI
|
|
177
|
+
├── EmojiPicker/
|
|
178
|
+
├── TicketScreen/
|
|
179
|
+
├── RecentChatsScreen/
|
|
180
|
+
├── BlockList/
|
|
181
|
+
├── MaintenanceView/
|
|
182
|
+
└── Tabs/BottomTabs.tsx ← Home / Chats / Tickets / Blocked
|
|
183
|
+
|
|
184
|
+
public/
|
|
185
|
+
└── chatData.json ← Sample JSON — host at window.mscorpres.com/TEST/chatData.json
|
|
186
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatUser, WidgetConfig } from '../../types';
|
|
3
|
+
interface BlockListScreenProps {
|
|
4
|
+
blockedUsers: ChatUser[];
|
|
5
|
+
config: WidgetConfig;
|
|
6
|
+
onUnblock: (uid: string) => void;
|
|
7
|
+
onBack: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const BlockListScreen: React.FC<BlockListScreenProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { avatarColor, initials } from '../../utils/chat';
|
|
3
|
+
export const BlockListScreen = ({ blockedUsers, config, onUnblock, onBack, }) => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }, children: [_jsxs("div", { style: {
|
|
4
|
+
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
5
|
+
padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0,
|
|
6
|
+
}, children: [_jsx("button", { onClick: onBack, style: backBtnStyle, 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", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: "Block List" }), _jsxs("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: [blockedUsers.length, " blocked user", blockedUsers.length !== 1 ? 's' : ''] })] })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: blockedUsers.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: "\u2705" }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: "No blocked users" }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: "Users you block will appear here" })] })) : (blockedUsers.map((user, i) => (_jsxs("div", { style: {
|
|
7
|
+
padding: '13px 16px', display: 'flex', alignItems: 'center', gap: 13,
|
|
8
|
+
borderBottom: '1px solid #f0f2f5',
|
|
9
|
+
animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s`,
|
|
10
|
+
}, children: [_jsx("div", { style: {
|
|
11
|
+
width: 44, height: 44, borderRadius: '50%',
|
|
12
|
+
backgroundColor: avatarColor(user.name),
|
|
13
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
14
|
+
color: '#fff', fontWeight: 700, fontSize: 14, flexShrink: 0,
|
|
15
|
+
filter: 'grayscale(0.6)', opacity: 0.7,
|
|
16
|
+
}, children: initials(user.name) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#6b7280' }, children: user.name }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: user.email })] }), _jsx("button", { onClick: () => onUnblock(user.uid), style: {
|
|
17
|
+
padding: '6px 14px', borderRadius: 20,
|
|
18
|
+
border: `1.5px solid ${config.primaryColor}`,
|
|
19
|
+
background: 'transparent', color: config.primaryColor,
|
|
20
|
+
fontSize: 12, fontWeight: 700, cursor: 'pointer',
|
|
21
|
+
transition: 'all 0.15s', flexShrink: 0,
|
|
22
|
+
}, onMouseEnter: e => {
|
|
23
|
+
e.currentTarget.style.background = config.primaryColor;
|
|
24
|
+
e.currentTarget.style.color = '#fff';
|
|
25
|
+
}, onMouseLeave: e => {
|
|
26
|
+
e.currentTarget.style.background = 'transparent';
|
|
27
|
+
e.currentTarget.style.color = config.primaryColor;
|
|
28
|
+
}, children: "Unblock" })] }, user.uid)))) })] }));
|
|
29
|
+
const backBtnStyle = {
|
|
30
|
+
background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: '50%',
|
|
31
|
+
width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
32
|
+
cursor: 'pointer', flexShrink: 0,
|
|
33
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CallSession } from '../../types';
|
|
3
|
+
interface CallScreenProps {
|
|
4
|
+
session: CallSession;
|
|
5
|
+
localVideoRef: React.RefObject<HTMLVideoElement | null>;
|
|
6
|
+
remoteVideoRef: React.RefObject<HTMLVideoElement | null>;
|
|
7
|
+
onEnd: () => void;
|
|
8
|
+
onToggleMute: () => void;
|
|
9
|
+
onToggleCamera: () => void;
|
|
10
|
+
primaryColor: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const CallScreen: React.FC<CallScreenProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { avatarColor, initials } from '../../utils/chat';
|
|
4
|
+
export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onToggleMute, onToggleCamera, primaryColor, }) => {
|
|
5
|
+
const [duration, setDuration] = useState(0);
|
|
6
|
+
const peer = session.peer;
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (session.state !== 'connected' || !session.startedAt)
|
|
9
|
+
return;
|
|
10
|
+
const t = setInterval(() => {
|
|
11
|
+
setDuration(Math.floor((Date.now() - session.startedAt.getTime()) / 1000));
|
|
12
|
+
}, 1000);
|
|
13
|
+
return () => clearInterval(t);
|
|
14
|
+
}, [session.state, session.startedAt]);
|
|
15
|
+
const mins = String(Math.floor(duration / 60)).padStart(2, '0');
|
|
16
|
+
const secs = String(duration % 60).padStart(2, '0');
|
|
17
|
+
return (_jsxs("div", { style: {
|
|
18
|
+
display: 'flex', flexDirection: 'column', height: '100%',
|
|
19
|
+
background: session.isCameraOn ? '#000' : `linear-gradient(145deg,${primaryColor}dd,#0f172a)`,
|
|
20
|
+
color: '#fff', animation: 'cw-slideIn 0.22s ease',
|
|
21
|
+
position: 'relative', overflow: 'hidden',
|
|
22
|
+
}, children: [_jsx("video", { ref: remoteVideoRef, autoPlay: true, playsInline: true, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: session.state === 'connected' ? 1 : 0 } }), _jsx("video", { ref: localVideoRef, autoPlay: true, playsInline: true, muted: true, style: {
|
|
23
|
+
position: 'absolute', bottom: 120, right: 14,
|
|
24
|
+
width: 90, height: 120, borderRadius: 10,
|
|
25
|
+
objectFit: 'cover', border: '2px solid rgba(255,255,255,0.3)',
|
|
26
|
+
display: session.isCameraOn ? 'block' : 'none',
|
|
27
|
+
zIndex: 10,
|
|
28
|
+
} }), _jsxs("div", { style: { position: 'relative', zIndex: 5, display: 'flex', flexDirection: 'column', height: '100%', background: 'rgba(0,0,0,0.35)' }, children: [_jsx("div", { style: { padding: '16px 18px', display: 'flex', alignItems: 'center', gap: 10 }, children: _jsx("div", { style: { flex: 1 }, children: _jsxs("div", { style: { fontWeight: 700, fontSize: 15, color: '#fff' }, children: [session.state === 'calling' && 'Calling...', session.state === 'connected' && 'Connected', session.state === 'ended' && 'Call Ended'] }) }) }), _jsx("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }, children: peer && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
29
|
+
width: 90, height: 90, borderRadius: '50%',
|
|
30
|
+
backgroundColor: avatarColor(peer.name),
|
|
31
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
32
|
+
fontSize: 28, fontWeight: 700, color: '#fff',
|
|
33
|
+
boxShadow: '0 0 0 4px rgba(255,255,255,0.2)',
|
|
34
|
+
animation: session.state === 'calling' ? 'cw-pulse 1.5s ease infinite' : 'none',
|
|
35
|
+
}, children: initials(peer.name) }), _jsxs("div", { style: { textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 20, fontWeight: 800 }, children: peer.name }), _jsxs("div", { style: { fontSize: 13, opacity: 0.8, marginTop: 4 }, children: [session.state === 'calling' && 'Ringing...', session.state === 'connected' && `${mins}:${secs}`, session.state === 'ended' && 'Call ended'] })] })] })) }), _jsxs("div", { style: { padding: '24px', display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 20 }, children: [_jsx(CallBtn, { active: session.isMuted, activeColor: "#374151", onClick: onToggleMute, title: session.isMuted ? 'Unmute' : 'Mute', children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: session.isMuted
|
|
36
|
+
? _jsxs(_Fragment, { children: [_jsx("path", { d: "M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z", stroke: "#fff", strokeWidth: "2" }), _jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23", stroke: "#ef4444", strokeWidth: "2" })] })
|
|
37
|
+
: _jsxs(_Fragment, { children: [_jsx("path", { d: "M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round" }), _jsx("path", { d: "M19 10v2a7 7 0 01-14 0v-2M12 19v4", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round" })] }) }) }), _jsx("button", { onClick: onEnd, style: {
|
|
38
|
+
width: 60, height: 60, borderRadius: '50%', backgroundColor: '#ef4444',
|
|
39
|
+
border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
40
|
+
boxShadow: '0 4px 16px rgba(239,68,68,0.5)', flexShrink: 0,
|
|
41
|
+
}, children: _jsx("svg", { width: "24", height: "24", 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", transform: "rotate(135 12 12)" }) }) }), _jsx(CallBtn, { active: session.isCameraOn, activeColor: primaryColor, onClick: onToggleCamera, title: "Camera", children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M23 7l-7 5 7 5V7zM1 5h13a2 2 0 012 2v10a2 2 0 01-2 2H1a2 2 0 01-2-2V7a2 2 0 012-2z", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
|
|
42
|
+
};
|
|
43
|
+
const CallBtn = ({ active, activeColor, onClick, title, children, }) => (_jsx("button", { onClick: onClick, title: title, style: {
|
|
44
|
+
width: 50, height: 50, borderRadius: '50%',
|
|
45
|
+
backgroundColor: active ? activeColor : 'rgba(255,255,255,0.2)',
|
|
46
|
+
border: 'none', cursor: 'pointer',
|
|
47
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
48
|
+
}, children: children }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatMessage, ChatUser, WidgetConfig } from '../../types';
|
|
3
|
+
interface ChatScreenProps {
|
|
4
|
+
activeUser: ChatUser;
|
|
5
|
+
messages: ChatMessage[];
|
|
6
|
+
config: WidgetConfig;
|
|
7
|
+
isPaused: boolean;
|
|
8
|
+
isReported: boolean;
|
|
9
|
+
isBlocked: boolean;
|
|
10
|
+
onSend: (text: string, type?: ChatMessage['type'], extra?: Partial<ChatMessage>) => void;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
onTogglePause: () => void;
|
|
14
|
+
onReport: () => void;
|
|
15
|
+
onBlock: () => void;
|
|
16
|
+
onStartCall: (withVideo: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const ChatScreen: React.FC<ChatScreenProps>;
|
|
19
|
+
export {};
|