simple-support-chat 0.3.3 → 0.4.0

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.
@@ -31,6 +31,8 @@ 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
+ /** URL for SSE endpoint (optional — enables real-time delivery when provided). */
35
+ sseUrl?: string;
34
36
  /**
35
37
  * Controlled mode: when provided, overrides internal open/close state.
36
38
  * The chat panel is open when `isOpen` is true, closed when false.
@@ -98,6 +100,8 @@ interface SupportChatModalProps {
98
100
  user?: ChatUser;
99
101
  /** URL to poll for team replies (e.g., '/api/support/replies'). Enables bidirectional chat. */
100
102
  repliesUrl?: string;
103
+ /** URL for SSE endpoint (optional — enables real-time delivery when provided). */
104
+ sseUrl?: string;
101
105
  }
102
106
  /** Anonymous context collected automatically from the browser */
103
107
  interface AnonymousContext {
@@ -120,7 +124,7 @@ interface AnonymousContext {
120
124
  * <ChatBubble apiUrl="/api/support" />
121
125
  * ```
122
126
  */
123
- declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
127
+ declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, sseUrl, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
124
128
 
125
129
  /**
126
130
  * SupportChatModal -- modal-based support chat.
@@ -152,7 +156,7 @@ declare function ChatBubble({ apiUrl, position, color, title, placeholder, show,
152
156
  * }
153
157
  * ```
154
158
  */
155
- declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, placeholder, user, repliesUrl, }: SupportChatModalProps): react_jsx_runtime.JSX.Element | null;
159
+ declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, placeholder, user, repliesUrl, sseUrl, }: SupportChatModalProps): react_jsx_runtime.JSX.Element | null;
156
160
 
157
161
  /**
158
162
  * Hook that returns controls for opening/closing the support chat.
@@ -166,6 +170,48 @@ declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, place
166
170
  */
167
171
  declare function useSupportChat(): SupportChatState;
168
172
 
173
+ /** Transport mode for reply delivery */
174
+ type TransportMode = "sse" | "polling" | "disconnected";
175
+ /** Options for useReplyTransport */
176
+ interface ReplyTransportOptions {
177
+ /** URL for SSE endpoint (optional — enables SSE transport when provided) */
178
+ sseUrl?: string;
179
+ /** URL for polling replies (optional — used as fallback or primary when no sseUrl) */
180
+ repliesUrl?: string;
181
+ /** Session identifier for the chat session */
182
+ sessionId: string;
183
+ /** Whether the transport should be active (connected/polling) */
184
+ isActive: boolean;
185
+ }
186
+ /** Return value from useReplyTransport */
187
+ interface ReplyTransportState {
188
+ /** Accumulated replies from the transport */
189
+ replies: ChatMessage[];
190
+ /** Current transport mode */
191
+ transport: TransportMode;
192
+ }
193
+ /**
194
+ * Internal hook that manages reply delivery via SSE or polling fallback.
195
+ *
196
+ * When `sseUrl` is provided and `isActive` is true, connects via EventSource
197
+ * for real-time reply delivery. If the EventSource connection fails, automatically
198
+ * falls back to polling via `repliesUrl`.
199
+ *
200
+ * When only `repliesUrl` is provided (no `sseUrl`), uses polling directly,
201
+ * preserving full backward compatibility with existing setups.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * const { replies, transport } = useReplyTransport({
206
+ * sseUrl: '/api/support/sse',
207
+ * repliesUrl: '/api/support/replies',
208
+ * sessionId: 'user-123',
209
+ * isActive: true,
210
+ * });
211
+ * ```
212
+ */
213
+ declare function useReplyTransport({ sseUrl, repliesUrl, sessionId, isActive, }: ReplyTransportOptions): ReplyTransportState;
214
+
169
215
  /** Options for initializing the chat engine */
170
216
  interface ChatEngineOptions {
171
217
  /** URL of the support chat API endpoint */
@@ -174,6 +220,8 @@ interface ChatEngineOptions {
174
220
  user?: ChatUser;
175
221
  /** URL to poll for team replies. When set, polling starts while chat is active. */
176
222
  repliesUrl?: string;
223
+ /** URL for SSE endpoint (optional — enables real-time delivery when provided). */
224
+ sseUrl?: string;
177
225
  /** Whether the chat panel is currently visible (controls polling lifecycle). Defaults to true. */
178
226
  isOpen?: boolean;
179
227
  }
@@ -191,56 +239,64 @@ interface ChatEngineState {
191
239
  sendMessage: () => Promise<void>;
192
240
  /** Handle keydown on the input (Enter to send) */
193
241
  handleKeyDown: (e: React.KeyboardEvent) => void;
242
+ /** Current transport mode */
243
+ transport: TransportMode;
194
244
  }
195
245
  /**
196
246
  * Shared chat engine hook used by both ChatBubble and SupportChatModal.
197
- * Manages message state, input, API communication, and reply polling.
247
+ * Manages message state, input, API communication, and reply delivery
248
+ * via useReplyTransport (SSE with polling fallback).
198
249
  */
199
- declare function useChatEngine({ apiUrl, user, repliesUrl, isOpen, }: ChatEngineOptions): ChatEngineState;
250
+ declare function useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen, }: ChatEngineOptions): ChatEngineState;
200
251
 
201
- /** Options for the useUnreadCount hook */
252
+ /** Options for useUnreadCount */
202
253
  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. */
254
+ /** URL for SSE endpoint (optional — enables real-time delivery when provided). */
255
+ sseUrl?: string;
256
+ /** URL to poll for team replies (e.g., '/api/support/replies'). */
257
+ repliesUrl?: string;
258
+ /** Authenticated user info (optional — uses user.id as sessionId if provided). */
206
259
  user?: ChatUser;
207
- /** Whether the chat panel is currently open. Polling pauses when true. */
260
+ /** Whether the chat panel is currently open. When open, unread count resets to 0. */
208
261
  isOpen: boolean;
209
262
  }
210
263
  /** Return value from useUnreadCount */
211
264
  interface UnreadCountState {
212
- /** Number of unread reply messages */
265
+ /** Number of unread replies received while the chat was closed */
213
266
  unreadCount: number;
214
267
  /** Convenience boolean: true when unreadCount > 0 */
215
268
  hasUnread: boolean;
216
- /** Pre-fetched reply messages ready to display instantly when chat opens */
269
+ /** Pre-cached pending replies for instant display when chat opens */
217
270
  pendingReplies: ChatMessage[];
218
271
  /** Reset unreadCount to 0 and clear pendingReplies */
219
272
  markAsRead: () => void;
273
+ /** Current transport mode */
274
+ transport: TransportMode;
220
275
  }
221
276
  /**
222
- * Hook that tracks unread reply count in the background via polling.
277
+ * Hook that tracks unread reply count when the chat is closed.
223
278
  *
224
- * Simple count-based model:
225
- * - On mount, fetches total reply count and records it as the "baseline"
226
- * - Unread = (current total) - baseline
227
- * - Page refresh resets to 0 (new baseline)
228
- * - Opening the chat or calling markAsRead resets baseline to current total
279
+ * Uses useReplyTransport internally to receive replies via SSE or polling.
280
+ * The transport stays active even when the chat is closed so replies arrive
281
+ * in real-time and the unread badge updates immediately.
282
+ *
283
+ * When the chat opens (isOpen becomes true), the unread count resets to 0.
284
+ * The pendingReplies array provides pre-cached replies for instant display.
229
285
  *
230
286
  * @example
231
- * ```tsx
232
- * const { unreadCount, hasUnread, markAsRead } = useUnreadCount({
287
+ * ```ts
288
+ * const { unreadCount, hasUnread, pendingReplies, markAsRead } = useUnreadCount({
289
+ * sseUrl: '/api/support/sse',
233
290
  * repliesUrl: '/api/support/replies',
234
- * user: currentUser,
235
291
  * isOpen: false,
236
292
  * });
237
293
  * ```
238
294
  */
239
- declare function useUnreadCount({ repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
295
+ declare function useUnreadCount({ sseUrl, repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
240
296
 
241
297
  /** Get or create a persistent session ID from localStorage */
242
298
  declare function getSessionId(): string;
243
299
  /** Collect anonymous browser context */
244
300
  declare function collectAnonymousContext(): AnonymousContext;
245
301
 
246
- 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 };
302
+ export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, type ReplyTransportOptions, type ReplyTransportState, SupportChatModal, type SupportChatModalProps, type SupportChatState, type TransportMode, type UnreadCountOptions, type UnreadCountState, collectAnonymousContext, getSessionId, useChatEngine, useReplyTransport, useSupportChat, useUnreadCount };
@@ -46,20 +46,180 @@ function collectAnonymousContext() {
46
46
  sessionId
47
47
  };
48
48
  }
49
+ var POLL_INTERVAL = 4e3;
50
+ function useReplyTransport({
51
+ sseUrl,
52
+ repliesUrl,
53
+ sessionId,
54
+ isActive
55
+ }) {
56
+ const [replies, setReplies] = useState([]);
57
+ const [transport, setTransport] = useState("disconnected");
58
+ const knownReplyIdsRef = useRef(/* @__PURE__ */ new Set());
59
+ const lastReplyTimestampRef = useRef(null);
60
+ const sseFailedRef = useRef(false);
61
+ const eventSourceRef = useRef(null);
62
+ const pollIntervalRef = useRef(null);
63
+ const needsCatchUpRef = useRef(false);
64
+ const addReplies = useCallback(
65
+ (newReplies) => {
66
+ const deduplicated = newReplies.filter(
67
+ (r) => !knownReplyIdsRef.current.has(r.id)
68
+ );
69
+ if (deduplicated.length === 0) return;
70
+ for (const r of deduplicated) {
71
+ knownReplyIdsRef.current.add(r.id);
72
+ }
73
+ const latestTimestamp = deduplicated.reduce((latest, r) => {
74
+ return r.timestamp > latest ? r.timestamp : latest;
75
+ }, lastReplyTimestampRef.current ?? "");
76
+ lastReplyTimestampRef.current = latestTimestamp;
77
+ const replyMessages = deduplicated.map((r) => ({
78
+ id: r.id,
79
+ text: r.text,
80
+ sender: "received",
81
+ timestamp: new Date(r.timestamp).getTime()
82
+ }));
83
+ setReplies((prev) => [...prev, ...replyMessages]);
84
+ },
85
+ []
86
+ );
87
+ const fetchReplies = useCallback(async () => {
88
+ if (!repliesUrl) return;
89
+ try {
90
+ const params = new URLSearchParams({ sessionId });
91
+ if (lastReplyTimestampRef.current) {
92
+ params.set("since", lastReplyTimestampRef.current);
93
+ }
94
+ const response = await fetch(`${repliesUrl}?${params.toString()}`);
95
+ if (!response.ok) return;
96
+ const data = await response.json();
97
+ if (data.replies.length === 0) return;
98
+ addReplies(data.replies);
99
+ } catch {
100
+ }
101
+ }, [repliesUrl, sessionId, addReplies]);
102
+ const startPolling = useCallback(() => {
103
+ if (!repliesUrl) return;
104
+ if (pollIntervalRef.current !== null) {
105
+ clearInterval(pollIntervalRef.current);
106
+ }
107
+ setTransport("polling");
108
+ void fetchReplies();
109
+ pollIntervalRef.current = setInterval(
110
+ () => void fetchReplies(),
111
+ POLL_INTERVAL
112
+ );
113
+ }, [repliesUrl, fetchReplies]);
114
+ const stopPolling = useCallback(() => {
115
+ if (pollIntervalRef.current !== null) {
116
+ clearInterval(pollIntervalRef.current);
117
+ pollIntervalRef.current = null;
118
+ }
119
+ }, []);
120
+ const connectSSE = useCallback(() => {
121
+ if (!sseUrl || sseFailedRef.current) return;
122
+ if (eventSourceRef.current) {
123
+ eventSourceRef.current.close();
124
+ eventSourceRef.current = null;
125
+ }
126
+ const url = `${sseUrl}?sessionId=${encodeURIComponent(sessionId)}`;
127
+ const es = new EventSource(url);
128
+ eventSourceRef.current = es;
129
+ es.addEventListener("reply", (event) => {
130
+ try {
131
+ const data = JSON.parse(event.data);
132
+ addReplies([data.reply]);
133
+ } catch {
134
+ }
135
+ });
136
+ es.addEventListener("heartbeat", () => {
137
+ });
138
+ es.onopen = () => {
139
+ setTransport("sse");
140
+ if (needsCatchUpRef.current) {
141
+ needsCatchUpRef.current = false;
142
+ void fetchReplies();
143
+ }
144
+ };
145
+ es.onerror = () => {
146
+ if (es.readyState === EventSource.CLOSED) {
147
+ sseFailedRef.current = true;
148
+ es.close();
149
+ eventSourceRef.current = null;
150
+ startPolling();
151
+ } else {
152
+ needsCatchUpRef.current = true;
153
+ }
154
+ };
155
+ }, [sseUrl, sessionId, addReplies, fetchReplies, startPolling]);
156
+ const disconnectSSE = useCallback(() => {
157
+ if (eventSourceRef.current) {
158
+ eventSourceRef.current.close();
159
+ eventSourceRef.current = null;
160
+ }
161
+ }, []);
162
+ useEffect(() => {
163
+ if (!isActive) {
164
+ disconnectSSE();
165
+ stopPolling();
166
+ setTransport("disconnected");
167
+ return;
168
+ }
169
+ if (!sseUrl && !repliesUrl) {
170
+ setTransport("disconnected");
171
+ return;
172
+ }
173
+ if (sseUrl && !sseFailedRef.current) {
174
+ connectSSE();
175
+ } else if (repliesUrl) {
176
+ startPolling();
177
+ }
178
+ return () => {
179
+ disconnectSSE();
180
+ stopPolling();
181
+ };
182
+ }, [
183
+ isActive,
184
+ sseUrl,
185
+ repliesUrl,
186
+ connectSSE,
187
+ disconnectSSE,
188
+ startPolling,
189
+ stopPolling
190
+ ]);
191
+ useEffect(() => {
192
+ sseFailedRef.current = false;
193
+ }, [sseUrl]);
194
+ return { replies, transport };
195
+ }
49
196
 
50
197
  // src/client/useChatEngine.ts
51
- var POLL_INTERVAL = 4e3;
52
198
  function useChatEngine({
53
199
  apiUrl,
54
200
  user,
55
201
  repliesUrl,
202
+ sseUrl,
56
203
  isOpen = true
57
204
  }) {
58
205
  const [messages, setMessages] = useState([]);
59
206
  const [input, setInput] = useState("");
60
207
  const [sending, setSending] = useState(false);
61
- const lastReplyTimestampRef = useRef(null);
62
- const knownReplyIdsRef = useRef(/* @__PURE__ */ new Set());
208
+ const sessionId = user?.id ?? getSessionId();
209
+ const { replies, transport } = useReplyTransport({
210
+ sseUrl,
211
+ repliesUrl,
212
+ sessionId,
213
+ isActive: isOpen
214
+ });
215
+ const processedReplyCountRef = useRef(0);
216
+ useEffect(() => {
217
+ if (replies.length > processedReplyCountRef.current) {
218
+ const newReplies = replies.slice(processedReplyCountRef.current);
219
+ processedReplyCountRef.current = replies.length;
220
+ setMessages((prev) => [...prev, ...newReplies]);
221
+ }
222
+ }, [replies]);
63
223
  const sendMessage = useCallback(async () => {
64
224
  const text = input.trim();
65
225
  if (!text || sending) return;
@@ -80,7 +240,7 @@ function useChatEngine({
80
240
  body: JSON.stringify({
81
241
  message: text,
82
242
  user: user ?? void 0,
83
- sessionId: user?.id ?? getSessionId(),
243
+ sessionId,
84
244
  context
85
245
  })
86
246
  });
@@ -108,7 +268,7 @@ function useChatEngine({
108
268
  } finally {
109
269
  setSending(false);
110
270
  }
111
- }, [input, sending, apiUrl, user]);
271
+ }, [input, sending, apiUrl, user, sessionId]);
112
272
  const handleKeyDown = useCallback(
113
273
  (e) => {
114
274
  if (e.key === "Enter" && !e.shiftKey) {
@@ -118,45 +278,7 @@ function useChatEngine({
118
278
  },
119
279
  [sendMessage]
120
280
  );
121
- useEffect(() => {
122
- if (!repliesUrl || !isOpen) return;
123
- const sessionId = user?.id ?? getSessionId();
124
- const fetchReplies = async () => {
125
- try {
126
- const params = new URLSearchParams({ sessionId });
127
- if (lastReplyTimestampRef.current) {
128
- params.set("since", lastReplyTimestampRef.current);
129
- }
130
- const response = await fetch(`${repliesUrl}?${params.toString()}`);
131
- if (!response.ok) return;
132
- const data = await response.json();
133
- if (data.replies.length === 0) return;
134
- const newReplies = data.replies.filter(
135
- (r) => !knownReplyIdsRef.current.has(r.id)
136
- );
137
- if (newReplies.length === 0) return;
138
- for (const r of newReplies) {
139
- knownReplyIdsRef.current.add(r.id);
140
- }
141
- const latestTimestamp = newReplies.reduce((latest, r) => {
142
- return r.timestamp > latest ? r.timestamp : latest;
143
- }, lastReplyTimestampRef.current ?? "");
144
- lastReplyTimestampRef.current = latestTimestamp;
145
- const replyMessages = newReplies.map((r) => ({
146
- id: r.id,
147
- text: r.text,
148
- sender: "received",
149
- timestamp: new Date(r.timestamp).getTime()
150
- }));
151
- setMessages((prev) => [...prev, ...replyMessages]);
152
- } catch {
153
- }
154
- };
155
- void fetchReplies();
156
- const intervalId = setInterval(() => void fetchReplies(), POLL_INTERVAL);
157
- return () => clearInterval(intervalId);
158
- }, [repliesUrl, isOpen, user]);
159
- return { messages, input, setInput, sending, sendMessage, handleKeyDown };
281
+ return { messages, input, setInput, sending, sendMessage, handleKeyDown, transport };
160
282
  }
161
283
  function useColorScheme() {
162
284
  const [scheme, setScheme] = useState(() => {
@@ -231,6 +353,7 @@ function ChatBubble({
231
353
  show = true,
232
354
  user,
233
355
  repliesUrl,
356
+ sseUrl,
234
357
  isOpen: isOpenProp,
235
358
  onOpenChange,
236
359
  badgeColor = "#eab308",
@@ -253,7 +376,7 @@ function ChatBubble({
253
376
  );
254
377
  const colorScheme = useColorScheme();
255
378
  const theme = useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
256
- const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
379
+ const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen });
257
380
  const [panelState, setPanelState] = useState(
258
381
  "closed"
259
382
  );
@@ -623,9 +746,10 @@ function SupportChatModal({
623
746
  title = "Contact Us",
624
747
  placeholder = "Type a message...",
625
748
  user,
626
- repliesUrl
749
+ repliesUrl,
750
+ sseUrl
627
751
  }) {
628
- const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
752
+ const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen });
629
753
  const colorScheme = useColorScheme();
630
754
  const theme = useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
631
755
  const modalRef = useRef(null);
@@ -908,75 +1032,47 @@ function useSupportChat() {
908
1032
  const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
909
1033
  return { open, close, toggle, isOpen };
910
1034
  }
911
- var POLL_INTERVAL2 = 4e3;
912
1035
  function useUnreadCount({
1036
+ sseUrl,
913
1037
  repliesUrl,
914
1038
  user,
915
1039
  isOpen
916
1040
  }) {
917
- const baselineCountRef = useRef(null);
918
- const [allReplies, setAllReplies] = useState([]);
919
- const totalCountRef = useRef(0);
920
- const prevIsOpenRef = useRef(isOpen);
1041
+ const sessionId = user?.id ?? getSessionId();
1042
+ const { replies, transport } = useReplyTransport({
1043
+ sseUrl,
1044
+ repliesUrl,
1045
+ sessionId,
1046
+ isActive: true
1047
+ });
1048
+ const [unreadCount, setUnreadCount] = useState(0);
1049
+ const [pendingReplies, setPendingReplies] = useState([]);
1050
+ const countedReplyCountRef = useRef(0);
1051
+ const wasOpenRef = useRef(isOpen);
921
1052
  useEffect(() => {
922
- if (isOpen && !prevIsOpenRef.current) {
923
- baselineCountRef.current = totalCountRef.current;
924
- setAllReplies([]);
1053
+ if (isOpen && !wasOpenRef.current) {
1054
+ setUnreadCount(0);
1055
+ setPendingReplies([]);
925
1056
  }
926
- prevIsOpenRef.current = isOpen;
1057
+ wasOpenRef.current = isOpen;
927
1058
  }, [isOpen]);
928
1059
  const markAsRead = useCallback(() => {
929
- baselineCountRef.current = totalCountRef.current;
930
- setAllReplies([]);
1060
+ setUnreadCount(0);
1061
+ setPendingReplies([]);
931
1062
  }, []);
932
1063
  useEffect(() => {
933
- if (!repliesUrl || isOpen) return;
934
- const sessionId = user?.id ?? getSessionId();
935
- let cancelled = false;
936
- const fetchReplies = async () => {
937
- try {
938
- const params = new URLSearchParams({ sessionId });
939
- const response = await fetch(`${repliesUrl}?${params.toString()}`);
940
- if (!response.ok || cancelled) return;
941
- const data = await response.json();
942
- if (cancelled) return;
943
- const currentTotal = data.replies.length;
944
- totalCountRef.current = currentTotal;
945
- if (baselineCountRef.current === null) {
946
- baselineCountRef.current = currentTotal;
947
- return;
948
- }
949
- const newCount = currentTotal - baselineCountRef.current;
950
- if (newCount > 0) {
951
- const newReplies = data.replies.slice(-newCount);
952
- setAllReplies(
953
- newReplies.map((r) => ({
954
- id: r.id,
955
- text: r.text,
956
- sender: "received",
957
- timestamp: new Date(r.timestamp).getTime()
958
- }))
959
- );
960
- }
961
- } catch {
1064
+ if (replies.length > countedReplyCountRef.current) {
1065
+ const newReplies = replies.slice(countedReplyCountRef.current);
1066
+ countedReplyCountRef.current = replies.length;
1067
+ if (!isOpen) {
1068
+ setUnreadCount((prev) => prev + newReplies.length);
1069
+ setPendingReplies((prev) => [...prev, ...newReplies]);
962
1070
  }
963
- };
964
- void fetchReplies();
965
- const intervalId = setInterval(() => void fetchReplies(), POLL_INTERVAL2);
966
- return () => {
967
- cancelled = true;
968
- clearInterval(intervalId);
969
- };
970
- }, [repliesUrl, isOpen, user]);
971
- const unreadCount = baselineCountRef.current !== null ? Math.max(0, totalCountRef.current - baselineCountRef.current) : 0;
972
- return {
973
- unreadCount,
974
- hasUnread: unreadCount > 0,
975
- pendingReplies: allReplies,
976
- markAsRead
977
- };
1071
+ }
1072
+ }, [replies, isOpen]);
1073
+ return { unreadCount, hasUnread: unreadCount > 0, pendingReplies, markAsRead, transport };
978
1074
  }
979
1075
 
980
- export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
1076
+ export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useReplyTransport, useSupportChat, useUnreadCount };
981
1077
  //# sourceMappingURL=index.js.map
982
1078
  //# sourceMappingURL=index.js.map