ajaxter-chat 3.0.16 → 3.0.17
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/CallScreen/index.d.ts +2 -0
- package/dist/components/CallScreen/index.js +12 -2
- package/dist/components/ChatScreen/index.js +20 -20
- package/dist/components/ChatWidget.js +16 -2
- package/dist/components/HomeScreen/index.js +25 -25
- package/dist/components/MiniCallBar/index.d.ts +14 -0
- package/dist/components/MiniCallBar/index.js +68 -0
- package/package.json +1 -1
- package/src/components/CallScreen/index.tsx +23 -1
- package/src/components/ChatScreen/index.tsx +2 -1
- package/src/components/ChatWidget.tsx +29 -0
- package/src/components/HomeScreen/index.tsx +0 -3
- package/src/components/MiniCallBar/index.tsx +150 -0
|
@@ -8,6 +8,8 @@ interface CallScreenProps {
|
|
|
8
8
|
onToggleMute: () => void;
|
|
9
9
|
onToggleCamera: () => void;
|
|
10
10
|
primaryColor: string;
|
|
11
|
+
/** Collapse the drawer while keeping the call active (mic/cam stay on). */
|
|
12
|
+
onMinimize?: () => void;
|
|
11
13
|
}
|
|
12
14
|
export declare const CallScreen: React.FC<CallScreenProps>;
|
|
13
15
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { avatarColor, initials } from '../../utils/chat';
|
|
4
|
-
export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onToggleMute, onToggleCamera, primaryColor, }) => {
|
|
4
|
+
export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onToggleMute, onToggleCamera, primaryColor, onMinimize, }) => {
|
|
5
5
|
const [duration, setDuration] = useState(0);
|
|
6
6
|
const peer = session.peer;
|
|
7
7
|
useEffect(() => {
|
|
@@ -25,7 +25,17 @@ export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onTo
|
|
|
25
25
|
objectFit: 'cover', border: '2px solid rgba(255,255,255,0.3)',
|
|
26
26
|
display: session.isCameraOn ? 'block' : 'none',
|
|
27
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: [
|
|
28
|
+
} }), _jsxs("div", { style: { position: 'relative', zIndex: 5, display: 'flex', flexDirection: 'column', height: '100%', background: 'rgba(0,0,0,0.35)' }, children: [_jsxs("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'] }) }), (session.state === 'calling' || session.state === 'connected') && onMinimize && (_jsx("button", { type: "button", onClick: onMinimize, title: "Minimize \u2014 keep call while you use the page", style: {
|
|
29
|
+
padding: '8px 12px',
|
|
30
|
+
borderRadius: 10,
|
|
31
|
+
border: '1px solid rgba(255,255,255,0.35)',
|
|
32
|
+
background: 'rgba(0,0,0,0.25)',
|
|
33
|
+
color: '#fff',
|
|
34
|
+
fontSize: 13,
|
|
35
|
+
fontWeight: 600,
|
|
36
|
+
cursor: 'pointer',
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
}, children: "Minimize" }))] }), _jsx("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }, children: peer && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
29
39
|
width: 90, height: 90, borderRadius: '50%',
|
|
30
40
|
backgroundColor: avatarColor(peer.name),
|
|
31
41
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
@@ -256,32 +256,32 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
256
256
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsxs("div", { style: {
|
|
257
257
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
258
258
|
padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
|
|
259
|
-
}, children: [_jsx("button", { type: "button", onClick: onBack, style: hdrBtn, "aria-label": "Back", 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", { 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' }, children: [peerInit, _jsx("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' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (
|
|
259
|
+
}, children: [_jsx("button", { type: "button", onClick: onBack, style: hdrBtn, "aria-label": "Back", 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", { 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' }, children: [peerInit, _jsx("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' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (_jsx("label", { style: {
|
|
260
260
|
display: 'flex',
|
|
261
261
|
alignItems: 'center',
|
|
262
262
|
gap: 6,
|
|
263
263
|
cursor: 'pointer',
|
|
264
264
|
flexShrink: 0,
|
|
265
265
|
marginLeft: 4,
|
|
266
|
-
}, children:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
266
|
+
}, children: _jsx("button", { type: "button", role: "switch", "aria-checked": messageSoundEnabled, onClick: () => onToggleMessageSound(!messageSoundEnabled), "aria-label": "Toggle message sound", title: "Toggle message sound", style: {
|
|
267
|
+
width: 36,
|
|
268
|
+
height: 20,
|
|
269
|
+
borderRadius: 10,
|
|
270
|
+
border: 'none',
|
|
271
|
+
background: messageSoundEnabled ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.2)',
|
|
272
|
+
position: 'relative',
|
|
273
|
+
cursor: 'pointer',
|
|
274
|
+
padding: 0,
|
|
275
|
+
}, children: _jsx("span", { style: {
|
|
276
|
+
position: 'absolute',
|
|
277
|
+
top: 2,
|
|
278
|
+
left: messageSoundEnabled ? 18 : 2,
|
|
279
|
+
width: 16,
|
|
280
|
+
height: 16,
|
|
281
|
+
borderRadius: '50%',
|
|
282
|
+
background: '#fff',
|
|
283
|
+
transition: 'left 0.15s ease',
|
|
284
|
+
} }) }) })), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", 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" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [navEntriesForChat(config.chatType, viewerIsDev).map(item => (_jsx(MenuItem, { icon: item.icon, label: item.label, onClick: () => { setShowMenu(false); onNavAction(item.key); } }, item.key))), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("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 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
|
|
285
285
|
position: 'relative',
|
|
286
286
|
marginBottom: 10,
|
|
287
287
|
padding: '12px 36px 12px 12px',
|
|
@@ -17,6 +17,7 @@ import { TicketDetailScreen } from './TicketDetailScreen';
|
|
|
17
17
|
import { TicketFormScreen } from './TicketFormScreen';
|
|
18
18
|
import { BlockListScreen } from './BlockList';
|
|
19
19
|
import { CallScreen } from './CallScreen';
|
|
20
|
+
import { MiniCallBar } from './MiniCallBar';
|
|
20
21
|
import { MaintenanceView } from './MaintenanceView';
|
|
21
22
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
22
23
|
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
@@ -36,6 +37,8 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
|
36
37
|
/* Drawer open state */
|
|
37
38
|
const [isOpen, setIsOpen] = useState(false);
|
|
38
39
|
const [closing, setClosing] = useState(false); // for slide-out animation
|
|
40
|
+
/** True when user hid the drawer during ringing/connected call; WebRTC session stays active. */
|
|
41
|
+
const [callMinimized, setCallMinimized] = useState(false);
|
|
39
42
|
/* Navigation */
|
|
40
43
|
const [activeTab, setActiveTab] = useState('home');
|
|
41
44
|
const [screen, setScreen] = useState('home');
|
|
@@ -82,10 +85,16 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
|
82
85
|
const { messages, activeUser, isPaused, isReported, selectUser, sendMessage, togglePause, reportChat, clearChat, setMessages, } = useChat();
|
|
83
86
|
/* WebRTC hook */
|
|
84
87
|
const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
|
|
88
|
+
const callInProgress = callSession.state === 'calling' || callSession.state === 'connected';
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!callInProgress)
|
|
91
|
+
setCallMinimized(false);
|
|
92
|
+
}, [callInProgress]);
|
|
85
93
|
/* ── Drawer open/close with slide animation ───────────────────────────── */
|
|
86
94
|
const openDrawer = () => {
|
|
87
95
|
setClosing(false);
|
|
88
96
|
setIsOpen(true);
|
|
97
|
+
setCallMinimized(false);
|
|
89
98
|
};
|
|
90
99
|
const persistWidgetState = useCallback(() => {
|
|
91
100
|
var _a;
|
|
@@ -315,8 +324,13 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
|
315
324
|
}, [activeUser, startCall]);
|
|
316
325
|
const handleEndCall = useCallback(() => {
|
|
317
326
|
endCall();
|
|
327
|
+
setCallMinimized(false);
|
|
318
328
|
setScreen('chat');
|
|
319
329
|
}, [endCall]);
|
|
330
|
+
const minimizeCall = useCallback(() => {
|
|
331
|
+
setCallMinimized(true);
|
|
332
|
+
closeDrawer();
|
|
333
|
+
}, [closeDrawer]);
|
|
320
334
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
321
335
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
322
336
|
const widgetConfig = useMemo(() => {
|
|
@@ -442,7 +456,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
|
442
456
|
@media (max-width: 1024px) {
|
|
443
457
|
.cw-drawer-panel { width: 100%; }
|
|
444
458
|
}
|
|
445
|
-
` }), !isOpen && (_jsxs("button", { className: "cw-root", type: "button", onClick: openDrawer, "aria-label": totalUnread > 0 ? `${theme.buttonLabel}, ${totalUnread} unread` : theme.buttonLabel, title: totalUnread > 0 ? `${totalUnread} unread message${totalUnread === 1 ? '' : 's'}` : theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
|
|
459
|
+
` }), !isOpen && callMinimized && callInProgress && callSession.peer && (_jsx(MiniCallBar, { session: callSession, primaryColor: primaryColor, buttonPosition: theme.buttonPosition, onExpand: openDrawer, onEnd: handleEndCall })), !isOpen && (_jsxs("button", { className: "cw-root", type: "button", onClick: openDrawer, "aria-label": totalUnread > 0 ? `${theme.buttonLabel}, ${totalUnread} unread` : theme.buttonLabel, title: totalUnread > 0 ? `${totalUnread} unread message${totalUnread === 1 ? '' : 's'}` : theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
|
|
446
460
|
e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
|
|
447
461
|
e.currentTarget.style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
|
|
448
462
|
}, onMouseLeave: e => {
|
|
@@ -481,7 +495,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
|
481
495
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
482
496
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
483
497
|
zIndex: 20, display: 'flex', gap: 6,
|
|
484
|
-
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey, onClose: closeDrawer })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, apiKey: apiKey, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
|
|
498
|
+
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey, onClose: closeDrawer })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, apiKey: apiKey, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor, onMinimize: minimizeCall })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
|
|
485
499
|
setListEntranceAnimation(false);
|
|
486
500
|
setViewingTicketId(id);
|
|
487
501
|
setScreen('ticket-detail');
|
|
@@ -72,37 +72,37 @@ export const HomeScreen = ({ config, apiKey, onNavigate, onOpenTicket, tickets }
|
|
|
72
72
|
justifyContent: 'center',
|
|
73
73
|
gap: 5,
|
|
74
74
|
flexShrink: 0,
|
|
75
|
-
}, children: [_jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } })] }), _jsx("div", { style: { flex: 1, minWidth: 0 } }),
|
|
75
|
+
}, children: [_jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } })] }), _jsx("div", { style: { flex: 1, minWidth: 0 } }), _jsx("div", { style: {
|
|
76
76
|
display: 'flex',
|
|
77
77
|
alignItems: 'center',
|
|
78
78
|
gap: 6,
|
|
79
79
|
flexShrink: 0,
|
|
80
80
|
flexWrap: 'wrap',
|
|
81
81
|
justifyContent: 'flex-end',
|
|
82
|
-
}, children:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
}, children: _jsx("div", { role: "group", "aria-label": "Your status", style: {
|
|
83
|
+
display: 'flex',
|
|
84
|
+
borderRadius: 10,
|
|
85
|
+
padding: 3,
|
|
86
|
+
background: '#f1f5f9',
|
|
87
|
+
gap: 2,
|
|
88
|
+
}, children: STATUS_OPTIONS.map(({ value, label }) => {
|
|
89
|
+
const isOn = presence === value;
|
|
90
|
+
return (_jsx("button", { type: "button", onClick: () => setPresenceAndSave(value), style: {
|
|
91
|
+
border: 'none',
|
|
92
|
+
borderRadius: 8,
|
|
93
|
+
padding: '7px 10px',
|
|
94
|
+
fontSize: 11,
|
|
95
|
+
fontWeight: 700,
|
|
96
|
+
letterSpacing: '0.04em',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
fontFamily: 'inherit',
|
|
99
|
+
textTransform: 'uppercase',
|
|
100
|
+
background: isOn ? config.primaryColor : 'transparent',
|
|
101
|
+
color: isOn ? '#fff' : '#64748b',
|
|
102
|
+
boxShadow: isOn ? `0 2px 8px ${config.primaryColor}55` : 'none',
|
|
103
|
+
transition: 'background 0.15s, color 0.15s',
|
|
104
|
+
}, children: label }, value));
|
|
105
|
+
}) }) })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }, children: [_jsx("h1", { style: {
|
|
106
106
|
margin: '0 0 8px',
|
|
107
107
|
fontSize: 24,
|
|
108
108
|
fontWeight: 800,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CallSession } from '../../types';
|
|
3
|
+
export interface MiniCallBarProps {
|
|
4
|
+
session: CallSession;
|
|
5
|
+
primaryColor: string;
|
|
6
|
+
buttonPosition: 'bottom-left' | 'bottom-right';
|
|
7
|
+
onExpand: () => void;
|
|
8
|
+
onEnd: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Shown when the user minimizes the widget during an active call (ringing or connected).
|
|
12
|
+
* Sits above the main launcher button so the user can work on the page and return to the call.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MiniCallBar: React.FC<MiniCallBarProps>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { avatarColor, initials } from '../../utils/chat';
|
|
5
|
+
/**
|
|
6
|
+
* Shown when the user minimizes the widget during an active call (ringing or connected).
|
|
7
|
+
* Sits above the main launcher button so the user can work on the page and return to the call.
|
|
8
|
+
*/
|
|
9
|
+
export const MiniCallBar = ({ session, primaryColor, buttonPosition, onExpand, onEnd, }) => {
|
|
10
|
+
var _a;
|
|
11
|
+
const peer = session.peer;
|
|
12
|
+
const [duration, setDuration] = useState(0);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (session.state !== 'connected' || !session.startedAt)
|
|
15
|
+
return;
|
|
16
|
+
const t = setInterval(() => {
|
|
17
|
+
setDuration(Math.floor((Date.now() - session.startedAt.getTime()) / 1000));
|
|
18
|
+
}, 1000);
|
|
19
|
+
return () => clearInterval(t);
|
|
20
|
+
}, [session.state, session.startedAt]);
|
|
21
|
+
const mins = String(Math.floor(duration / 60)).padStart(2, '0');
|
|
22
|
+
const secs = String(duration % 60).padStart(2, '0');
|
|
23
|
+
const pos = buttonPosition === 'bottom-left'
|
|
24
|
+
? { left: 24, right: 'auto' }
|
|
25
|
+
: { right: 24, left: 'auto' };
|
|
26
|
+
return (_jsxs("div", { role: "toolbar", "aria-label": "Call in progress", style: Object.assign(Object.assign({ position: 'fixed', bottom: 88, zIndex: 10000 }, pos), { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', maxWidth: 'min(360px, calc(100vw - 48px))', borderRadius: 14, background: `linear-gradient(135deg, ${primaryColor}ee, #0f172a)`, color: '#fff', boxShadow: '0 10px 32px rgba(0,0,0,0.28)', animation: 'cw-miniBarIn 0.28s cubic-bezier(0.22,1,0.36,1)', cursor: 'default' }), children: [_jsx("style", { children: `
|
|
27
|
+
@keyframes cw-miniBarIn {
|
|
28
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
29
|
+
to { opacity: 1; transform: translateY(0); }
|
|
30
|
+
}
|
|
31
|
+
` }), _jsxs("button", { type: "button", onClick: onExpand, title: "Open call", style: {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
gap: 10,
|
|
35
|
+
flex: 1,
|
|
36
|
+
minWidth: 0,
|
|
37
|
+
padding: 0,
|
|
38
|
+
border: 'none',
|
|
39
|
+
background: 'transparent',
|
|
40
|
+
color: 'inherit',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
textAlign: 'left',
|
|
43
|
+
}, children: [peer && (_jsx("div", { style: {
|
|
44
|
+
width: 40,
|
|
45
|
+
height: 40,
|
|
46
|
+
borderRadius: '50%',
|
|
47
|
+
backgroundColor: avatarColor(peer.name),
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
fontSize: 14,
|
|
52
|
+
fontWeight: 700,
|
|
53
|
+
flexShrink: 0,
|
|
54
|
+
animation: session.state === 'calling' ? 'cw-pulse 1.5s ease infinite' : 'none',
|
|
55
|
+
}, children: initials(peer.name) })), _jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = peer === null || peer === void 0 ? void 0 : peer.name) !== null && _a !== void 0 ? _a : 'Call' }), _jsxs("div", { style: { fontSize: 12, opacity: 0.9, marginTop: 2 }, children: [session.state === 'calling' && 'Calling…', session.state === 'connected' && `${mins}:${secs}`] })] })] }), _jsx("button", { type: "button", onClick: onEnd, title: "End call", style: {
|
|
56
|
+
width: 40,
|
|
57
|
+
height: 40,
|
|
58
|
+
borderRadius: '50%',
|
|
59
|
+
border: 'none',
|
|
60
|
+
background: '#ef4444',
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
display: 'flex',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
flexShrink: 0,
|
|
66
|
+
boxShadow: '0 2px 10px rgba(239,68,68,0.45)',
|
|
67
|
+
}, children: _jsx("svg", { width: "18", height: "18", 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)" }) }) })] }));
|
|
68
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.17",
|
|
4
4
|
"description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,11 +10,13 @@ interface CallScreenProps {
|
|
|
10
10
|
onToggleMute: () => void;
|
|
11
11
|
onToggleCamera: () => void;
|
|
12
12
|
primaryColor: string;
|
|
13
|
+
/** Collapse the drawer while keeping the call active (mic/cam stay on). */
|
|
14
|
+
onMinimize?: () => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const CallScreen: React.FC<CallScreenProps> = ({
|
|
16
18
|
session, localVideoRef, remoteVideoRef,
|
|
17
|
-
onEnd, onToggleMute, onToggleCamera, primaryColor,
|
|
19
|
+
onEnd, onToggleMute, onToggleCamera, primaryColor, onMinimize,
|
|
18
20
|
}) => {
|
|
19
21
|
const [duration, setDuration] = useState(0);
|
|
20
22
|
const peer = session.peer as ChatUser;
|
|
@@ -64,6 +66,26 @@ export const CallScreen: React.FC<CallScreenProps> = ({
|
|
|
64
66
|
{session.state === 'ended' && 'Call Ended'}
|
|
65
67
|
</div>
|
|
66
68
|
</div>
|
|
69
|
+
{(session.state === 'calling' || session.state === 'connected') && onMinimize && (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onMinimize}
|
|
73
|
+
title="Minimize — keep call while you use the page"
|
|
74
|
+
style={{
|
|
75
|
+
padding: '8px 12px',
|
|
76
|
+
borderRadius: 10,
|
|
77
|
+
border: '1px solid rgba(255,255,255,0.35)',
|
|
78
|
+
background: 'rgba(0,0,0,0.25)',
|
|
79
|
+
color: '#fff',
|
|
80
|
+
fontSize: 13,
|
|
81
|
+
fontWeight: 600,
|
|
82
|
+
cursor: 'pointer',
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
Minimize
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
67
89
|
</div>
|
|
68
90
|
|
|
69
91
|
{/* Center: avatar + name */}
|
|
@@ -314,12 +314,13 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
314
314
|
marginLeft: 4,
|
|
315
315
|
}}
|
|
316
316
|
>
|
|
317
|
-
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.85)', fontWeight: 600 }}>Sound</span>
|
|
318
317
|
<button
|
|
319
318
|
type="button"
|
|
320
319
|
role="switch"
|
|
321
320
|
aria-checked={messageSoundEnabled}
|
|
322
321
|
onClick={() => onToggleMessageSound(!messageSoundEnabled)}
|
|
322
|
+
aria-label="Toggle message sound"
|
|
323
|
+
title="Toggle message sound"
|
|
323
324
|
style={{
|
|
324
325
|
width: 36,
|
|
325
326
|
height: 20,
|
|
@@ -19,6 +19,7 @@ import { TicketDetailScreen } from './TicketDetailScreen';
|
|
|
19
19
|
import { TicketFormScreen } from './TicketFormScreen';
|
|
20
20
|
import { BlockListScreen } from './BlockList';
|
|
21
21
|
import { CallScreen } from './CallScreen';
|
|
22
|
+
import { MiniCallBar } from './MiniCallBar';
|
|
22
23
|
import { MaintenanceView } from './MaintenanceView';
|
|
23
24
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
24
25
|
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
@@ -45,6 +46,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
45
46
|
/* Drawer open state */
|
|
46
47
|
const [isOpen, setIsOpen] = useState(false);
|
|
47
48
|
const [closing, setClosing] = useState(false); // for slide-out animation
|
|
49
|
+
/** True when user hid the drawer during ringing/connected call; WebRTC session stays active. */
|
|
50
|
+
const [callMinimized, setCallMinimized] = useState(false);
|
|
48
51
|
|
|
49
52
|
/* Navigation */
|
|
50
53
|
const [activeTab, setActiveTab] = useState<BottomTab>('home');
|
|
@@ -98,10 +101,18 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
98
101
|
/* WebRTC hook */
|
|
99
102
|
const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
|
|
100
103
|
|
|
104
|
+
const callInProgress =
|
|
105
|
+
callSession.state === 'calling' || callSession.state === 'connected';
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!callInProgress) setCallMinimized(false);
|
|
109
|
+
}, [callInProgress]);
|
|
110
|
+
|
|
101
111
|
/* ── Drawer open/close with slide animation ───────────────────────────── */
|
|
102
112
|
const openDrawer = () => {
|
|
103
113
|
setClosing(false);
|
|
104
114
|
setIsOpen(true);
|
|
115
|
+
setCallMinimized(false);
|
|
105
116
|
};
|
|
106
117
|
|
|
107
118
|
const persistWidgetState = useCallback(() => {
|
|
@@ -333,9 +344,15 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
333
344
|
|
|
334
345
|
const handleEndCall = useCallback(() => {
|
|
335
346
|
endCall();
|
|
347
|
+
setCallMinimized(false);
|
|
336
348
|
setScreen('chat');
|
|
337
349
|
}, [endCall]);
|
|
338
350
|
|
|
351
|
+
const minimizeCall = useCallback(() => {
|
|
352
|
+
setCallMinimized(true);
|
|
353
|
+
closeDrawer();
|
|
354
|
+
}, [closeDrawer]);
|
|
355
|
+
|
|
339
356
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
340
357
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
341
358
|
|
|
@@ -472,6 +489,17 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
472
489
|
}
|
|
473
490
|
`}</style>
|
|
474
491
|
|
|
492
|
+
{/* ── Minimized call bar (drawer closed, call still active) ── */}
|
|
493
|
+
{!isOpen && callMinimized && callInProgress && callSession.peer && (
|
|
494
|
+
<MiniCallBar
|
|
495
|
+
session={callSession}
|
|
496
|
+
primaryColor={primaryColor}
|
|
497
|
+
buttonPosition={theme.buttonPosition}
|
|
498
|
+
onExpand={openDrawer}
|
|
499
|
+
onEnd={handleEndCall}
|
|
500
|
+
/>
|
|
501
|
+
)}
|
|
502
|
+
|
|
475
503
|
{/* ── Floating Button (unread badge + tooltip when closed) ── */}
|
|
476
504
|
{!isOpen && (
|
|
477
505
|
<button
|
|
@@ -698,6 +726,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
|
|
|
698
726
|
onToggleMute={toggleMute}
|
|
699
727
|
onToggleCamera={toggleCamera}
|
|
700
728
|
primaryColor={primaryColor}
|
|
729
|
+
onMinimize={minimizeCall}
|
|
701
730
|
/>
|
|
702
731
|
)}
|
|
703
732
|
|
|
@@ -140,9 +140,6 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, apiKey, onNaviga
|
|
|
140
140
|
justifyContent: 'flex-end',
|
|
141
141
|
}}
|
|
142
142
|
>
|
|
143
|
-
<span style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
|
144
|
-
Status
|
|
145
|
-
</span>
|
|
146
143
|
<div
|
|
147
144
|
role="group"
|
|
148
145
|
aria-label="Your status"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { CallSession, ChatUser } from '../../types';
|
|
5
|
+
import { avatarColor, initials } from '../../utils/chat';
|
|
6
|
+
|
|
7
|
+
export interface MiniCallBarProps {
|
|
8
|
+
session: CallSession;
|
|
9
|
+
primaryColor: string;
|
|
10
|
+
buttonPosition: 'bottom-left' | 'bottom-right';
|
|
11
|
+
onExpand: () => void;
|
|
12
|
+
onEnd: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shown when the user minimizes the widget during an active call (ringing or connected).
|
|
17
|
+
* Sits above the main launcher button so the user can work on the page and return to the call.
|
|
18
|
+
*/
|
|
19
|
+
export const MiniCallBar: React.FC<MiniCallBarProps> = ({
|
|
20
|
+
session,
|
|
21
|
+
primaryColor,
|
|
22
|
+
buttonPosition,
|
|
23
|
+
onExpand,
|
|
24
|
+
onEnd,
|
|
25
|
+
}) => {
|
|
26
|
+
const peer = session.peer as ChatUser | null;
|
|
27
|
+
const [duration, setDuration] = useState(0);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (session.state !== 'connected' || !session.startedAt) return;
|
|
31
|
+
const t = setInterval(() => {
|
|
32
|
+
setDuration(Math.floor((Date.now() - session.startedAt!.getTime()) / 1000));
|
|
33
|
+
}, 1000);
|
|
34
|
+
return () => clearInterval(t);
|
|
35
|
+
}, [session.state, session.startedAt]);
|
|
36
|
+
|
|
37
|
+
const mins = String(Math.floor(duration / 60)).padStart(2, '0');
|
|
38
|
+
const secs = String(duration % 60).padStart(2, '0');
|
|
39
|
+
|
|
40
|
+
const pos: React.CSSProperties =
|
|
41
|
+
buttonPosition === 'bottom-left'
|
|
42
|
+
? { left: 24, right: 'auto' }
|
|
43
|
+
: { right: 24, left: 'auto' };
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
role="toolbar"
|
|
48
|
+
aria-label="Call in progress"
|
|
49
|
+
style={{
|
|
50
|
+
position: 'fixed',
|
|
51
|
+
bottom: 88,
|
|
52
|
+
zIndex: 10000,
|
|
53
|
+
...pos,
|
|
54
|
+
display: 'flex',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: 10,
|
|
57
|
+
padding: '10px 14px',
|
|
58
|
+
maxWidth: 'min(360px, calc(100vw - 48px))',
|
|
59
|
+
borderRadius: 14,
|
|
60
|
+
background: `linear-gradient(135deg, ${primaryColor}ee, #0f172a)`,
|
|
61
|
+
color: '#fff',
|
|
62
|
+
boxShadow: '0 10px 32px rgba(0,0,0,0.28)',
|
|
63
|
+
animation: 'cw-miniBarIn 0.28s cubic-bezier(0.22,1,0.36,1)',
|
|
64
|
+
cursor: 'default',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<style>{`
|
|
68
|
+
@keyframes cw-miniBarIn {
|
|
69
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
70
|
+
to { opacity: 1; transform: translateY(0); }
|
|
71
|
+
}
|
|
72
|
+
`}</style>
|
|
73
|
+
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={onExpand}
|
|
77
|
+
title="Open call"
|
|
78
|
+
style={{
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
gap: 10,
|
|
82
|
+
flex: 1,
|
|
83
|
+
minWidth: 0,
|
|
84
|
+
padding: 0,
|
|
85
|
+
border: 'none',
|
|
86
|
+
background: 'transparent',
|
|
87
|
+
color: 'inherit',
|
|
88
|
+
cursor: 'pointer',
|
|
89
|
+
textAlign: 'left',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{peer && (
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
width: 40,
|
|
96
|
+
height: 40,
|
|
97
|
+
borderRadius: '50%',
|
|
98
|
+
backgroundColor: avatarColor(peer.name),
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
fontWeight: 700,
|
|
104
|
+
flexShrink: 0,
|
|
105
|
+
animation: session.state === 'calling' ? 'cw-pulse 1.5s ease infinite' : 'none',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{initials(peer.name)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
<div style={{ minWidth: 0, flex: 1 }}>
|
|
112
|
+
<div style={{ fontWeight: 700, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
113
|
+
{peer?.name ?? 'Call'}
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{ fontSize: 12, opacity: 0.9, marginTop: 2 }}>
|
|
116
|
+
{session.state === 'calling' && 'Calling…'}
|
|
117
|
+
{session.state === 'connected' && `${mins}:${secs}`}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</button>
|
|
121
|
+
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={onEnd}
|
|
125
|
+
title="End call"
|
|
126
|
+
style={{
|
|
127
|
+
width: 40,
|
|
128
|
+
height: 40,
|
|
129
|
+
borderRadius: '50%',
|
|
130
|
+
border: 'none',
|
|
131
|
+
background: '#ef4444',
|
|
132
|
+
cursor: 'pointer',
|
|
133
|
+
display: 'flex',
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
justifyContent: 'center',
|
|
136
|
+
flexShrink: 0,
|
|
137
|
+
boxShadow: '0 2px 10px rgba(239,68,68,0.45)',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
141
|
+
<path
|
|
142
|
+
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"
|
|
143
|
+
fill="#fff"
|
|
144
|
+
transform="rotate(135 12 12)"
|
|
145
|
+
/>
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|