simple-support-chat 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.cjs +157 -21
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +79 -2
- package/dist/client/index.d.ts +79 -2
- package/dist/client/index.js +157 -22
- package/dist/client/index.js.map +1 -1
- package/package.json +1 -1
package/dist/client/index.d.cts
CHANGED
|
@@ -31,6 +31,36 @@ interface ChatBubbleProps {
|
|
|
31
31
|
user?: ChatUser;
|
|
32
32
|
/** URL to poll for team replies (e.g., '/api/support/replies'). Enables bidirectional chat. */
|
|
33
33
|
repliesUrl?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Controlled mode: when provided, overrides internal open/close state.
|
|
36
|
+
* The chat panel is open when `isOpen` is true, closed when false.
|
|
37
|
+
* Use together with `onOpenChange` to control the panel externally.
|
|
38
|
+
*
|
|
39
|
+
* When not provided, ChatBubble manages its own open/close state (uncontrolled mode).
|
|
40
|
+
*
|
|
41
|
+
* Note: `show` controls visibility of the floating button, `isOpen` controls the panel state.
|
|
42
|
+
* Both can be used together (e.g., `show={false}` + `isOpen={true}` = no bubble button but panel is open).
|
|
43
|
+
*/
|
|
44
|
+
isOpen?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Callback fired when the user attempts to change the open/close state.
|
|
47
|
+
* Called when the user clicks the bubble button, close button, or presses Escape.
|
|
48
|
+
* In controlled mode, you must update your state in response to this callback.
|
|
49
|
+
* In uncontrolled mode, this is called as a notification after the state changes internally.
|
|
50
|
+
*/
|
|
51
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Color for the unread badge on the floating bubble button.
|
|
54
|
+
* Only visible when there are unread replies and the chat is closed.
|
|
55
|
+
* Default: '#eab308' (yellow).
|
|
56
|
+
*/
|
|
57
|
+
badgeColor?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Number of unread replies to display on the badge.
|
|
60
|
+
* When provided, a circular badge is shown on the floating bubble button.
|
|
61
|
+
* Typically driven by the `useUnreadCount` hook.
|
|
62
|
+
*/
|
|
63
|
+
unreadCount?: number;
|
|
34
64
|
}
|
|
35
65
|
/** A single chat message */
|
|
36
66
|
interface ChatMessage {
|
|
@@ -90,7 +120,7 @@ interface AnonymousContext {
|
|
|
90
120
|
* <ChatBubble apiUrl="/api/support" />
|
|
91
121
|
* ```
|
|
92
122
|
*/
|
|
93
|
-
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
123
|
+
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
94
124
|
|
|
95
125
|
/**
|
|
96
126
|
* SupportChatModal -- modal-based support chat.
|
|
@@ -168,9 +198,56 @@ interface ChatEngineState {
|
|
|
168
198
|
*/
|
|
169
199
|
declare function useChatEngine({ apiUrl, user, repliesUrl, isOpen, }: ChatEngineOptions): ChatEngineState;
|
|
170
200
|
|
|
201
|
+
/** Options for the useUnreadCount hook */
|
|
202
|
+
interface UnreadCountOptions {
|
|
203
|
+
/** URL to poll for team replies (e.g., '/api/support/replies') */
|
|
204
|
+
repliesUrl: string;
|
|
205
|
+
/** Authenticated user info (optional). When provided, user.id is used as sessionId. */
|
|
206
|
+
user?: ChatUser;
|
|
207
|
+
/** Whether the chat panel is currently open. Polling pauses when true. */
|
|
208
|
+
isOpen: boolean;
|
|
209
|
+
}
|
|
210
|
+
/** Return value from useUnreadCount */
|
|
211
|
+
interface UnreadCountState {
|
|
212
|
+
/** Number of unread reply messages */
|
|
213
|
+
unreadCount: number;
|
|
214
|
+
/** Convenience boolean: true when unreadCount > 0 */
|
|
215
|
+
hasUnread: boolean;
|
|
216
|
+
/** Pre-fetched reply messages ready to display instantly when chat opens */
|
|
217
|
+
pendingReplies: ChatMessage[];
|
|
218
|
+
/** Reset unreadCount to 0 and clear pendingReplies */
|
|
219
|
+
markAsRead: () => void;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Hook that tracks unread reply count in the background via polling.
|
|
223
|
+
*
|
|
224
|
+
* Polls the replies endpoint every 4 seconds when `isOpen` is false.
|
|
225
|
+
* Stops background polling when `isOpen` is true (useChatEngine handles it at that point).
|
|
226
|
+
* Pre-fetches reply messages so they appear instantly when the chat opens.
|
|
227
|
+
*
|
|
228
|
+
* Can be used independently of ChatBubble -- any component can call useUnreadCount
|
|
229
|
+
* to build custom unread UX.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```tsx
|
|
233
|
+
* const { unreadCount, hasUnread, pendingReplies, markAsRead } = useUnreadCount({
|
|
234
|
+
* repliesUrl: '/api/support/replies',
|
|
235
|
+
* user: currentUser,
|
|
236
|
+
* isOpen: false,
|
|
237
|
+
* });
|
|
238
|
+
*
|
|
239
|
+
* return (
|
|
240
|
+
* <button onClick={openChat}>
|
|
241
|
+
* Support {hasUnread && <span className="badge">{unreadCount}</span>}
|
|
242
|
+
* </button>
|
|
243
|
+
* );
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare function useUnreadCount({ repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
|
|
247
|
+
|
|
171
248
|
/** Get or create a persistent session ID from localStorage */
|
|
172
249
|
declare function getSessionId(): string;
|
|
173
250
|
/** Collect anonymous browser context */
|
|
174
251
|
declare function collectAnonymousContext(): AnonymousContext;
|
|
175
252
|
|
|
176
|
-
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, SupportChatModal, type SupportChatModalProps, type SupportChatState, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat };
|
|
253
|
+
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, SupportChatModal, type SupportChatModalProps, type SupportChatState, type UnreadCountOptions, type UnreadCountState, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -31,6 +31,36 @@ interface ChatBubbleProps {
|
|
|
31
31
|
user?: ChatUser;
|
|
32
32
|
/** URL to poll for team replies (e.g., '/api/support/replies'). Enables bidirectional chat. */
|
|
33
33
|
repliesUrl?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Controlled mode: when provided, overrides internal open/close state.
|
|
36
|
+
* The chat panel is open when `isOpen` is true, closed when false.
|
|
37
|
+
* Use together with `onOpenChange` to control the panel externally.
|
|
38
|
+
*
|
|
39
|
+
* When not provided, ChatBubble manages its own open/close state (uncontrolled mode).
|
|
40
|
+
*
|
|
41
|
+
* Note: `show` controls visibility of the floating button, `isOpen` controls the panel state.
|
|
42
|
+
* Both can be used together (e.g., `show={false}` + `isOpen={true}` = no bubble button but panel is open).
|
|
43
|
+
*/
|
|
44
|
+
isOpen?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Callback fired when the user attempts to change the open/close state.
|
|
47
|
+
* Called when the user clicks the bubble button, close button, or presses Escape.
|
|
48
|
+
* In controlled mode, you must update your state in response to this callback.
|
|
49
|
+
* In uncontrolled mode, this is called as a notification after the state changes internally.
|
|
50
|
+
*/
|
|
51
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Color for the unread badge on the floating bubble button.
|
|
54
|
+
* Only visible when there are unread replies and the chat is closed.
|
|
55
|
+
* Default: '#eab308' (yellow).
|
|
56
|
+
*/
|
|
57
|
+
badgeColor?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Number of unread replies to display on the badge.
|
|
60
|
+
* When provided, a circular badge is shown on the floating bubble button.
|
|
61
|
+
* Typically driven by the `useUnreadCount` hook.
|
|
62
|
+
*/
|
|
63
|
+
unreadCount?: number;
|
|
34
64
|
}
|
|
35
65
|
/** A single chat message */
|
|
36
66
|
interface ChatMessage {
|
|
@@ -90,7 +120,7 @@ interface AnonymousContext {
|
|
|
90
120
|
* <ChatBubble apiUrl="/api/support" />
|
|
91
121
|
* ```
|
|
92
122
|
*/
|
|
93
|
-
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
123
|
+
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
94
124
|
|
|
95
125
|
/**
|
|
96
126
|
* SupportChatModal -- modal-based support chat.
|
|
@@ -168,9 +198,56 @@ interface ChatEngineState {
|
|
|
168
198
|
*/
|
|
169
199
|
declare function useChatEngine({ apiUrl, user, repliesUrl, isOpen, }: ChatEngineOptions): ChatEngineState;
|
|
170
200
|
|
|
201
|
+
/** Options for the useUnreadCount hook */
|
|
202
|
+
interface UnreadCountOptions {
|
|
203
|
+
/** URL to poll for team replies (e.g., '/api/support/replies') */
|
|
204
|
+
repliesUrl: string;
|
|
205
|
+
/** Authenticated user info (optional). When provided, user.id is used as sessionId. */
|
|
206
|
+
user?: ChatUser;
|
|
207
|
+
/** Whether the chat panel is currently open. Polling pauses when true. */
|
|
208
|
+
isOpen: boolean;
|
|
209
|
+
}
|
|
210
|
+
/** Return value from useUnreadCount */
|
|
211
|
+
interface UnreadCountState {
|
|
212
|
+
/** Number of unread reply messages */
|
|
213
|
+
unreadCount: number;
|
|
214
|
+
/** Convenience boolean: true when unreadCount > 0 */
|
|
215
|
+
hasUnread: boolean;
|
|
216
|
+
/** Pre-fetched reply messages ready to display instantly when chat opens */
|
|
217
|
+
pendingReplies: ChatMessage[];
|
|
218
|
+
/** Reset unreadCount to 0 and clear pendingReplies */
|
|
219
|
+
markAsRead: () => void;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Hook that tracks unread reply count in the background via polling.
|
|
223
|
+
*
|
|
224
|
+
* Polls the replies endpoint every 4 seconds when `isOpen` is false.
|
|
225
|
+
* Stops background polling when `isOpen` is true (useChatEngine handles it at that point).
|
|
226
|
+
* Pre-fetches reply messages so they appear instantly when the chat opens.
|
|
227
|
+
*
|
|
228
|
+
* Can be used independently of ChatBubble -- any component can call useUnreadCount
|
|
229
|
+
* to build custom unread UX.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```tsx
|
|
233
|
+
* const { unreadCount, hasUnread, pendingReplies, markAsRead } = useUnreadCount({
|
|
234
|
+
* repliesUrl: '/api/support/replies',
|
|
235
|
+
* user: currentUser,
|
|
236
|
+
* isOpen: false,
|
|
237
|
+
* });
|
|
238
|
+
*
|
|
239
|
+
* return (
|
|
240
|
+
* <button onClick={openChat}>
|
|
241
|
+
* Support {hasUnread && <span className="badge">{unreadCount}</span>}
|
|
242
|
+
* </button>
|
|
243
|
+
* );
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare function useUnreadCount({ repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
|
|
247
|
+
|
|
171
248
|
/** Get or create a persistent session ID from localStorage */
|
|
172
249
|
declare function getSessionId(): string;
|
|
173
250
|
/** Collect anonymous browser context */
|
|
174
251
|
declare function collectAnonymousContext(): AnonymousContext;
|
|
175
252
|
|
|
176
|
-
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, SupportChatModal, type SupportChatModalProps, type SupportChatState, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat };
|
|
253
|
+
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, SupportChatModal, type SupportChatModalProps, type SupportChatState, type UnreadCountOptions, type UnreadCountState, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
|
package/dist/client/index.js
CHANGED
|
@@ -215,6 +215,10 @@ function injectKeyframes() {
|
|
|
215
215
|
from { opacity: 1; transform: translateY(0) scale(1); }
|
|
216
216
|
to { opacity: 0; transform: translateY(12px) scale(0.96); }
|
|
217
217
|
}
|
|
218
|
+
@keyframes sc-badge-scale-in {
|
|
219
|
+
from { transform: scale(0); }
|
|
220
|
+
to { transform: scale(1); }
|
|
221
|
+
}
|
|
218
222
|
`;
|
|
219
223
|
document.head.appendChild(style);
|
|
220
224
|
}
|
|
@@ -226,9 +230,27 @@ function ChatBubble({
|
|
|
226
230
|
placeholder = "Type a message...",
|
|
227
231
|
show = true,
|
|
228
232
|
user,
|
|
229
|
-
repliesUrl
|
|
233
|
+
repliesUrl,
|
|
234
|
+
isOpen: isOpenProp,
|
|
235
|
+
onOpenChange,
|
|
236
|
+
badgeColor = "#eab308",
|
|
237
|
+
unreadCount = 0
|
|
230
238
|
}) {
|
|
231
|
-
const
|
|
239
|
+
const isControlled = isOpenProp !== void 0;
|
|
240
|
+
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
|
241
|
+
const isOpen = isControlled ? isOpenProp : isOpenInternal;
|
|
242
|
+
const setIsOpen = useCallback(
|
|
243
|
+
(valueOrUpdater) => {
|
|
244
|
+
const newValue = typeof valueOrUpdater === "function" ? valueOrUpdater(isOpen) : valueOrUpdater;
|
|
245
|
+
if (isControlled) {
|
|
246
|
+
onOpenChange?.(newValue);
|
|
247
|
+
} else {
|
|
248
|
+
setIsOpenInternal(newValue);
|
|
249
|
+
onOpenChange?.(newValue);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[isControlled, isOpen, onOpenChange]
|
|
253
|
+
);
|
|
232
254
|
const colorScheme = useColorScheme();
|
|
233
255
|
const theme = useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
|
|
234
256
|
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
|
|
@@ -290,8 +312,8 @@ function ChatBubble({
|
|
|
290
312
|
};
|
|
291
313
|
document.addEventListener("keydown", handleTrapKeyDown);
|
|
292
314
|
return () => document.removeEventListener("keydown", handleTrapKeyDown);
|
|
293
|
-
}, [panelState]);
|
|
294
|
-
const toggle = useCallback(() => setIsOpen((o) => !o), []);
|
|
315
|
+
}, [panelState, setIsOpen]);
|
|
316
|
+
const toggle = useCallback(() => setIsOpen((o) => !o), [setIsOpen]);
|
|
295
317
|
const positionStyles = {
|
|
296
318
|
position: "fixed",
|
|
297
319
|
zIndex: 99999,
|
|
@@ -324,7 +346,7 @@ function ChatBubble({
|
|
|
324
346
|
}
|
|
325
347
|
}
|
|
326
348
|
` }),
|
|
327
|
-
show && /* @__PURE__ */
|
|
349
|
+
show && /* @__PURE__ */ jsxs(
|
|
328
350
|
"button",
|
|
329
351
|
{
|
|
330
352
|
onClick: toggle,
|
|
@@ -343,7 +365,8 @@ function ChatBubble({
|
|
|
343
365
|
alignItems: "center",
|
|
344
366
|
justifyContent: "center",
|
|
345
367
|
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
346
|
-
transition: "transform 0.2s ease, box-shadow 0.2s ease"
|
|
368
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
369
|
+
overflow: "visible"
|
|
347
370
|
},
|
|
348
371
|
onMouseEnter: (e) => {
|
|
349
372
|
e.currentTarget.style.transform = "scale(1.1)";
|
|
@@ -353,21 +376,50 @@ function ChatBubble({
|
|
|
353
376
|
e.currentTarget.style.transform = "scale(1)";
|
|
354
377
|
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
|
355
378
|
},
|
|
356
|
-
children:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
379
|
+
children: [
|
|
380
|
+
/* @__PURE__ */ jsx(
|
|
381
|
+
"svg",
|
|
382
|
+
{
|
|
383
|
+
width: "24",
|
|
384
|
+
height: "24",
|
|
385
|
+
viewBox: "0 0 24 24",
|
|
386
|
+
fill: "none",
|
|
387
|
+
stroke: "white",
|
|
388
|
+
strokeWidth: "2",
|
|
389
|
+
strokeLinecap: "round",
|
|
390
|
+
strokeLinejoin: "round",
|
|
391
|
+
"aria-hidden": "true",
|
|
392
|
+
children: isOpen ? /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" }) : /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
393
|
+
}
|
|
394
|
+
),
|
|
395
|
+
!isOpen && unreadCount > 0 && /* @__PURE__ */ jsx(
|
|
396
|
+
"span",
|
|
397
|
+
{
|
|
398
|
+
"data-testid": "unread-badge",
|
|
399
|
+
"aria-label": `${unreadCount > 9 ? "9+" : unreadCount} unread messages`,
|
|
400
|
+
style: {
|
|
401
|
+
position: "absolute",
|
|
402
|
+
top: "-4px",
|
|
403
|
+
right: "-4px",
|
|
404
|
+
minWidth: "20px",
|
|
405
|
+
height: "20px",
|
|
406
|
+
borderRadius: "10px",
|
|
407
|
+
backgroundColor: badgeColor,
|
|
408
|
+
color: "#fff",
|
|
409
|
+
fontSize: "11px",
|
|
410
|
+
fontWeight: 700,
|
|
411
|
+
display: "flex",
|
|
412
|
+
alignItems: "center",
|
|
413
|
+
justifyContent: "center",
|
|
414
|
+
padding: "0 5px",
|
|
415
|
+
lineHeight: 1,
|
|
416
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
|
|
417
|
+
animation: "sc-badge-scale-in 0.2s ease-out forwards"
|
|
418
|
+
},
|
|
419
|
+
children: unreadCount > 9 ? "9+" : unreadCount
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
]
|
|
371
423
|
}
|
|
372
424
|
),
|
|
373
425
|
showPanel && /* @__PURE__ */ jsxs(
|
|
@@ -856,7 +908,90 @@ function useSupportChat() {
|
|
|
856
908
|
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
|
|
857
909
|
return { open, close, toggle, isOpen };
|
|
858
910
|
}
|
|
911
|
+
var POLL_INTERVAL2 = 4e3;
|
|
912
|
+
function useUnreadCount({
|
|
913
|
+
repliesUrl,
|
|
914
|
+
user,
|
|
915
|
+
isOpen
|
|
916
|
+
}) {
|
|
917
|
+
const [pendingReplies, setPendingReplies] = useState([]);
|
|
918
|
+
const lastReplyTimestampRef = useRef(null);
|
|
919
|
+
const knownReplyIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
920
|
+
const prevIsOpenRef = useRef(isOpen);
|
|
921
|
+
const baselineSetRef = useRef(false);
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
if (isOpen && !prevIsOpenRef.current) {
|
|
924
|
+
setPendingReplies([]);
|
|
925
|
+
knownReplyIdsRef.current.clear();
|
|
926
|
+
}
|
|
927
|
+
prevIsOpenRef.current = isOpen;
|
|
928
|
+
}, [isOpen]);
|
|
929
|
+
const markAsRead = useCallback(() => {
|
|
930
|
+
setPendingReplies([]);
|
|
931
|
+
knownReplyIdsRef.current.clear();
|
|
932
|
+
}, []);
|
|
933
|
+
useEffect(() => {
|
|
934
|
+
if (!repliesUrl || isOpen) return;
|
|
935
|
+
const sessionId = user?.id ?? getSessionId();
|
|
936
|
+
const fetchReplies = async () => {
|
|
937
|
+
try {
|
|
938
|
+
const params = new URLSearchParams({ sessionId });
|
|
939
|
+
if (lastReplyTimestampRef.current) {
|
|
940
|
+
params.set("since", lastReplyTimestampRef.current);
|
|
941
|
+
}
|
|
942
|
+
const response = await fetch(`${repliesUrl}?${params.toString()}`);
|
|
943
|
+
if (!response.ok) return;
|
|
944
|
+
const data = await response.json();
|
|
945
|
+
if (!baselineSetRef.current) {
|
|
946
|
+
baselineSetRef.current = true;
|
|
947
|
+
if (data.replies.length > 0) {
|
|
948
|
+
for (const r of data.replies) {
|
|
949
|
+
knownReplyIdsRef.current.add(r.id);
|
|
950
|
+
}
|
|
951
|
+
const latestTimestamp2 = data.replies.reduce((latest, r) => {
|
|
952
|
+
return r.timestamp > latest ? r.timestamp : latest;
|
|
953
|
+
}, "");
|
|
954
|
+
lastReplyTimestampRef.current = latestTimestamp2;
|
|
955
|
+
} else if (data.lastChecked) {
|
|
956
|
+
lastReplyTimestampRef.current = data.lastChecked;
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (data.replies.length === 0) return;
|
|
961
|
+
const newReplies = data.replies.filter(
|
|
962
|
+
(r) => !knownReplyIdsRef.current.has(r.id)
|
|
963
|
+
);
|
|
964
|
+
if (newReplies.length === 0) return;
|
|
965
|
+
for (const r of newReplies) {
|
|
966
|
+
knownReplyIdsRef.current.add(r.id);
|
|
967
|
+
}
|
|
968
|
+
const latestTimestamp = newReplies.reduce((latest, r) => {
|
|
969
|
+
return r.timestamp > latest ? r.timestamp : latest;
|
|
970
|
+
}, lastReplyTimestampRef.current ?? "");
|
|
971
|
+
lastReplyTimestampRef.current = latestTimestamp;
|
|
972
|
+
const replyMessages = newReplies.map((r) => ({
|
|
973
|
+
id: r.id,
|
|
974
|
+
text: r.text,
|
|
975
|
+
sender: "received",
|
|
976
|
+
timestamp: new Date(r.timestamp).getTime()
|
|
977
|
+
}));
|
|
978
|
+
setPendingReplies((prev) => [...prev, ...replyMessages]);
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
void fetchReplies();
|
|
983
|
+
const intervalId = setInterval(() => void fetchReplies(), POLL_INTERVAL2);
|
|
984
|
+
return () => clearInterval(intervalId);
|
|
985
|
+
}, [repliesUrl, isOpen, user]);
|
|
986
|
+
const unreadCount = pendingReplies.length;
|
|
987
|
+
return {
|
|
988
|
+
unreadCount,
|
|
989
|
+
hasUnread: unreadCount > 0,
|
|
990
|
+
pendingReplies,
|
|
991
|
+
markAsRead
|
|
992
|
+
};
|
|
993
|
+
}
|
|
859
994
|
|
|
860
|
-
export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat };
|
|
995
|
+
export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
|
|
861
996
|
//# sourceMappingURL=index.js.map
|
|
862
997
|
//# sourceMappingURL=index.js.map
|