ajaxter-chat 3.0.15 → 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/README.md CHANGED
Binary file
@@ -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: [_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: {
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 && (_jsxs("label", { style: {
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: [_jsx("span", { style: { fontSize: 10, color: 'rgba(255,255,255,0.85)', fontWeight: 600 }, children: "Sound" }), _jsx("button", { type: "button", role: "switch", "aria-checked": messageSoundEnabled, onClick: () => onToggleMessageSound(!messageSoundEnabled), 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: {
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(() => {
@@ -375,6 +389,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
375
389
  : [];
376
390
  const otherDevelopers = useMemo(() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid), [allUsers, viewerUid]);
377
391
  const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
392
+ const totalUnread = useMemo(() => recentChats.reduce((sum, c) => { var _a; return sum + Math.max(0, (_a = c.unread) !== null && _a !== void 0 ? _a : 0); }, 0), [recentChats]);
378
393
  const handleTransferToDeveloper = useCallback((dev) => {
379
394
  var _a;
380
395
  if (!activeUser || !widgetConfig)
@@ -441,13 +456,29 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
441
456
  @media (max-width: 1024px) {
442
457
  .cw-drawer-panel { width: 100%; }
443
458
  }
444
- ` }), !isOpen && (_jsxs("button", { className: "cw-root", onClick: openDrawer, "aria-label": 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 => {
445
460
  e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
446
461
  e.currentTarget.style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
447
462
  }, onMouseLeave: e => {
448
463
  e.currentTarget.style.transform = 'scale(1)';
449
464
  e.currentTarget.style.boxShadow = `0 8px 28px ${theme.buttonColor}55`;
450
- }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { "aria-hidden": true, style: {
465
+ }, children: [_jsxs("span", { style: { position: 'relative', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), totalUnread > 0 && (_jsx("span", { style: {
466
+ position: 'absolute',
467
+ top: -8,
468
+ right: -10,
469
+ minWidth: 20,
470
+ height: 20,
471
+ padding: '0 5px',
472
+ borderRadius: 999,
473
+ background: '#ef4444',
474
+ color: '#fff',
475
+ fontSize: 11,
476
+ fontWeight: 800,
477
+ lineHeight: '20px',
478
+ textAlign: 'center',
479
+ border: '2px solid #fff',
480
+ boxSizing: 'border-box',
481
+ }, children: totalUnread > 99 ? '99+' : totalUnread }))] }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { "aria-hidden": true, style: {
451
482
  position: 'fixed', inset: 0, zIndex: 9997,
452
483
  backgroundColor: 'rgba(0,0,0,0.35)',
453
484
  opacity: closing ? 0 : 1,
@@ -459,12 +490,12 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
459
490
  border: `3px solid ${primaryColor}30`,
460
491
  borderTopColor: primaryColor,
461
492
  animation: 'spin 0.8s linear infinite',
462
- } }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && (_jsx("div", { style: {
493
+ } }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && !effectiveViewerBlocked && (_jsx("div", { style: {
463
494
  position: 'absolute', top: 12,
464
495
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
465
496
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
466
497
  zIndex: 20, display: 'flex', gap: 6,
467
- }, 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 })), 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, 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 => {
468
499
  setListEntranceAnimation(false);
469
500
  setViewingTicketId(id);
470
501
  setScreen('ticket-detail');
@@ -6,6 +6,8 @@ export interface HomeNavigateOptions {
6
6
  }
7
7
  interface HomeScreenProps {
8
8
  config: WidgetConfig;
9
+ /** Same as env / chatData — required to POST presence in production */
10
+ apiKey: string;
9
11
  onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
10
12
  /** Open a specific pending ticket (full detail) */
11
13
  onOpenTicket: (ticketId: string) => void;
@@ -1,10 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useMemo } from 'react';
2
+ import { useState, useMemo, useEffect } from 'react';
3
3
  import { SlideNavMenu } from '../SlideNavMenu';
4
4
  import { truncateWords } from '../../utils/chat';
5
- export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
5
+ import { resolveInitialPresence, savePresenceStatus, syncPresenceToServer, } from '../../utils/presenceStatus';
6
+ const STATUS_OPTIONS = [
7
+ { value: 'ACTIVE', label: 'Active' },
8
+ { value: 'AWAY', label: 'Away' },
9
+ { value: 'DND', label: 'DND' },
10
+ ];
11
+ export const HomeScreen = ({ config, apiKey, onNavigate, onOpenTicket, tickets }) => {
6
12
  var _a, _b, _c, _d;
7
13
  const [menuOpen, setMenuOpen] = useState(false);
14
+ const [presence, setPresence] = useState(() => resolveInitialPresence(config.id, config.presenceStatus));
15
+ useEffect(() => {
16
+ setPresence(resolveInitialPresence(config.id, config.presenceStatus));
17
+ }, [config.id, config.presenceStatus]);
18
+ const setPresenceAndSave = (s) => {
19
+ var _a, _b;
20
+ setPresence(s);
21
+ savePresenceStatus(config.id, s);
22
+ const url = (_a = config.presenceUpdateUrl) === null || _a === void 0 ? void 0 : _a.trim();
23
+ if (!url)
24
+ return;
25
+ void syncPresenceToServer(url, {
26
+ widgetId: config.id,
27
+ apiKey,
28
+ viewerUid: ((_b = config.viewerUid) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
29
+ status: s,
30
+ }).catch(err => {
31
+ console.error('[ajaxter-chat] presence sync failed', err);
32
+ });
33
+ };
8
34
  const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
9
35
  const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
10
36
  const viewerIsDev = config.viewerType === 'developer';
@@ -25,28 +51,58 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
25
51
  };
26
52
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: ctx => {
27
53
  onNavigate(ctx, { fromMenu: true });
28
- } }), _jsx("div", { style: {
54
+ } }), _jsxs("div", { style: {
29
55
  flexShrink: 0,
30
- padding: '14px 16px 10px',
56
+ padding: '12px 14px 12px',
31
57
  display: 'flex',
32
58
  alignItems: 'center',
33
- gap: 12,
59
+ gap: 10,
34
60
  background: '#fff',
35
61
  borderBottom: '1px solid #eef0f5',
36
- }, children: _jsxs("button", { type: "button", "aria-label": "Open menu", onClick: () => setMenuOpen(true), style: {
37
- width: 40,
38
- height: 40,
39
- borderRadius: 10,
40
- border: 'none',
41
- background: '#f1f5f9',
42
- cursor: 'pointer',
43
- display: 'flex',
44
- flexDirection: 'column',
45
- alignItems: 'center',
46
- justifyContent: 'center',
47
- gap: 5,
48
- flexShrink: 0,
49
- }, 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 } })] }) }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }, children: [_jsx("h1", { style: {
62
+ }, children: [_jsxs("button", { type: "button", "aria-label": "Open menu", onClick: () => setMenuOpen(true), style: {
63
+ width: 40,
64
+ height: 40,
65
+ borderRadius: 10,
66
+ border: 'none',
67
+ background: '#f1f5f9',
68
+ cursor: 'pointer',
69
+ display: 'flex',
70
+ flexDirection: 'column',
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ gap: 5,
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 } }), _jsx("div", { style: {
76
+ display: 'flex',
77
+ alignItems: 'center',
78
+ gap: 6,
79
+ flexShrink: 0,
80
+ flexWrap: 'wrap',
81
+ justifyContent: 'flex-end',
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: {
50
106
  margin: '0 0 8px',
51
107
  fontSize: 24,
52
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
+ };
@@ -3,6 +3,7 @@ import { WidgetConfig } from '../../types';
3
3
  interface ViewerBlockedScreenProps {
4
4
  config: WidgetConfig;
5
5
  apiKey: string;
6
+ onClose: () => void;
6
7
  }
7
8
  export declare const ViewerBlockedScreen: React.FC<ViewerBlockedScreenProps>;
8
9
  export {};
@@ -1,9 +1,9 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { submitReenableRequest } from '../../utils/reenableRequest';
5
5
  const DEFAULT_MESSAGE = 'You have been marked as Blocked user due to spam';
6
- export const ViewerBlockedScreen = ({ config, apiKey }) => {
6
+ export const ViewerBlockedScreen = ({ config, apiKey, onClose }) => {
7
7
  var _a, _b;
8
8
  const [text, setText] = useState('');
9
9
  const [status, setStatus] = useState('idle');
@@ -56,7 +56,18 @@ export const ViewerBlockedScreen = ({ config, apiKey }) => {
56
56
  fontWeight: 600,
57
57
  color: '#1e293b',
58
58
  lineHeight: 1.55,
59
- }, children: body }), status === 'sent' ? (_jsx("p", { style: { margin: 0, fontSize: 14, color: '#16a34a', fontWeight: 600 }, children: "Your request was sent. We will review it shortly." })) : (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "cw-reenable-msg", style: { display: 'block', textAlign: 'left', fontSize: 13, fontWeight: 600, color: '#475569', marginBottom: 8 }, children: "Request access restoration" }), _jsx("textarea", { id: "cw-reenable-msg", value: text, onChange: e => { setText(e.target.value); setError(null); setStatus('idle'); }, placeholder: "Explain briefly why your access should be restored\u2026", rows: 4, disabled: status === 'sending', style: {
59
+ }, children: body }), status === 'sent' ? (_jsxs(_Fragment, { children: [_jsx("p", { style: { margin: '0 0 16px', fontSize: 14, color: '#16a34a', fontWeight: 600 }, children: "Your request was sent. We will review it shortly." }), _jsx("button", { type: "button", onClick: onClose, style: {
60
+ width: '100%',
61
+ padding: '12px 16px',
62
+ borderRadius: 12,
63
+ border: '2px solid #ef4444',
64
+ background: '#fff',
65
+ color: '#ef4444',
66
+ fontWeight: 700,
67
+ fontSize: 15,
68
+ cursor: 'pointer',
69
+ fontFamily: 'inherit',
70
+ }, children: "Close" })] })) : (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "cw-reenable-msg", style: { display: 'block', textAlign: 'left', fontSize: 13, fontWeight: 600, color: '#475569', marginBottom: 8 }, children: "Request access restoration" }), _jsx("textarea", { id: "cw-reenable-msg", value: text, onChange: e => { setText(e.target.value); setError(null); setStatus('idle'); }, placeholder: "Explain briefly why your access should be restored\u2026", rows: 4, maxLength: 500, minLength: 50, disabled: status === 'sending', style: {
60
71
  width: '100%',
61
72
  boxSizing: 'border-box',
62
73
  padding: '12px 14px',
@@ -65,8 +76,9 @@ export const ViewerBlockedScreen = ({ config, apiKey }) => {
65
76
  fontSize: 14,
66
77
  fontFamily: 'inherit',
67
78
  color: '#1e293b',
68
- resize: 'vertical',
79
+ resize: 'none',
69
80
  minHeight: 100,
81
+ maxHeight: 250,
70
82
  marginBottom: 14,
71
83
  outline: 'none',
72
84
  } }), _jsx("button", { type: "button", onClick: handleSubmit, disabled: status === 'sending' || !text.trim(), style: {
@@ -79,5 +91,17 @@ export const ViewerBlockedScreen = ({ config, apiKey }) => {
79
91
  fontWeight: 700,
80
92
  fontSize: 15,
81
93
  cursor: text.trim() && status !== 'sending' ? 'pointer' : 'default',
82
- }, children: status === 'sending' ? 'Sending…' : 'Submit request' }), error && (_jsx("p", { style: { margin: '12px 0 0', fontSize: 13, color: '#dc2626', lineHeight: 1.45 }, children: error })), !url && (_jsxs("p", { style: { margin: '14px 0 0', fontSize: 12, color: '#94a3b8', lineHeight: 1.5 }, children: ["Your administrator must set ", _jsx("code", { style: { fontSize: 11 }, children: "reenableRequestUrl" }), " in widget config for online requests."] }))] }))] }) }));
94
+ }, children: status === 'sending' ? 'Sending…' : 'Submit request' }), _jsx("button", { type: "button", onClick: onClose, style: {
95
+ width: '100%',
96
+ marginTop: 12,
97
+ padding: '12px 16px',
98
+ borderRadius: 12,
99
+ border: '2px solid #ef4444',
100
+ background: '#fff',
101
+ color: '#ef4444',
102
+ fontWeight: 700,
103
+ fontSize: 15,
104
+ cursor: 'pointer',
105
+ fontFamily: 'inherit',
106
+ }, children: "Close" }), error && (_jsx("p", { style: { margin: '12px 0 0', fontSize: 13, color: '#dc2626', lineHeight: 1.45 }, children: error })), !url && (_jsxs("p", { style: { margin: '14px 0 0', fontSize: 12, color: '#94a3b8', lineHeight: 1.5 }, children: ["Your administrator must set ", _jsx("code", { style: { fontSize: 11 }, children: "reenableRequestUrl" }), " in widget config for online requests."] }))] }))] }) }));
83
107
  };
package/dist/index.d.ts CHANGED
@@ -18,5 +18,7 @@ export { submitReenableRequest } from './utils/reenableRequest';
18
18
  export type { ReenableRequestPayload } from './utils/reenableRequest';
19
19
  export { loadLocalConfig, fetchRemoteChatData } from './config';
20
20
  export { mergeTheme, darken } from './utils/theme';
21
+ export { loadPresenceStatus, savePresenceStatus, resolveInitialPresence, syncPresenceToServer } from './utils/presenceStatus';
22
+ export type { PresenceSyncPayload } from './utils/presenceStatus';
21
23
  export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
22
- export type { ChatWidgetProps, ChatWidgetTheme, ChatWidgetViewer, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, } from './types';
24
+ export type { ChatWidgetProps, ChatWidgetTheme, ChatWidgetViewer, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, PresenceStatus, } from './types';
package/dist/index.js CHANGED
@@ -17,4 +17,5 @@ export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt }
17
17
  export { submitReenableRequest } from './utils/reenableRequest';
18
18
  export { loadLocalConfig, fetchRemoteChatData } from './config';
19
19
  export { mergeTheme, darken } from './utils/theme';
20
+ export { loadPresenceStatus, savePresenceStatus, resolveInitialPresence, syncPresenceToServer } from './utils/presenceStatus';
20
21
  export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
@@ -58,6 +58,16 @@ export interface WidgetConfig {
58
58
  * @example https://api.example.com/widgets/reenable-request
59
59
  */
60
60
  reenableRequestUrl?: string;
61
+ /**
62
+ * Current presence from your API/DB (include in chatData or a session payload).
63
+ * When set, it initializes the status control and overrides session-only cache.
64
+ */
65
+ presenceStatus?: PresenceStatus;
66
+ /**
67
+ * Production: `POST` JSON `{ widgetId, apiKey, viewerUid?, status }` to save presence in your database.
68
+ * The client still mirrors to sessionStorage as a local fallback.
69
+ */
70
+ presenceUpdateUrl?: string;
61
71
  }
62
72
  export interface RemoteChatData {
63
73
  widget: WidgetConfig;
@@ -75,6 +85,8 @@ export type BottomTab = 'home' | 'chats' | 'tickets';
75
85
  export type Screen = 'home' | 'user-list' | 'chat' | 'recent-chats' | 'tickets' | 'ticket-new' | 'ticket-detail' | 'block-list' | 'call';
76
86
  export type UserListContext = 'support' | 'conversation';
77
87
  export type MessageType = 'text' | 'voice' | 'attachment' | 'emoji';
88
+ /** Home status selector; persist via `presenceUpdateUrl` in production */
89
+ export type PresenceStatus = 'ACTIVE' | 'AWAY' | 'DND';
78
90
  export interface ChatUser {
79
91
  uid: string;
80
92
  name: string;
@@ -0,0 +1,13 @@
1
+ import type { PresenceStatus } from '../types';
2
+ export declare function loadPresenceStatus(widgetId: string): PresenceStatus;
3
+ export declare function savePresenceStatus(widgetId: string, status: PresenceStatus): void;
4
+ /** Prefer server value from DB when the host includes it in config */
5
+ export declare function resolveInitialPresence(widgetId: string, serverStatus: PresenceStatus | undefined): PresenceStatus;
6
+ export interface PresenceSyncPayload {
7
+ widgetId: string;
8
+ apiKey: string;
9
+ viewerUid?: string;
10
+ status: PresenceStatus;
11
+ }
12
+ /** Call your backend to persist presence (production DB). */
13
+ export declare function syncPresenceToServer(url: string, payload: PresenceSyncPayload): Promise<void>;