@clivly/chat-widget 0.12.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -307,6 +307,25 @@ The default browser mount behavior is:
307
307
  2. otherwise prefer `session.socketUrl`
308
308
  3. otherwise fall back to `socketUrlTemplate` when provided
309
309
 
310
+ ### Read receipts
311
+
312
+ While the panel is open, connected and on screen, the widget tells the server
313
+ the visitor has seen the thread. It sends again when new messages arrive and when
314
+ the tab becomes visible, so a teammate's reply the visitor already read isn't also
315
+ emailed to them. The built-in transports send this as a
316
+ `{ type: "conversation.read" }` client event (`WidgetClientReadEvent`).
317
+
318
+ A **custom transport** opts in by implementing the optional
319
+ `ChatTransportConnection.markRead()`. Without it the widget still works; its
320
+ visitors just receive email copies of replies they saw in the widget.
321
+
322
+ ### Waiting notice
323
+
324
+ If the visitor's latest message has gone unanswered for two minutes, the widget
325
+ shows a short note that the team has been
326
+ notified, and that the reply will also go to the visitor's email when one is
327
+ known. A failed send doesn't start the clock, because nobody was notified about it.
328
+
310
329
  ## Build outputs
311
330
 
312
331
  The package currently produces:
@@ -364,6 +364,14 @@ const WIDGET_STYLES = `
364
364
  font-size: 0.875rem;
365
365
  padding: 0.75rem;
366
366
  }
367
+ .clivly-chat-widget__notice {
368
+ background: var(--clivly-chat-operator);
369
+ border-radius: 0.8rem;
370
+ color: var(--clivly-chat-muted);
371
+ font-size: 0.8125rem;
372
+ margin: 0;
373
+ padding: 0.625rem 0.75rem;
374
+ }
367
375
  .clivly-chat-widget__empty {
368
376
  color: var(--clivly-chat-muted);
369
377
  font-size: 0.875rem;
@@ -516,6 +524,56 @@ function MessageList({ greeting, hasSession, messages }) {
516
524
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, { message }, message.id)) });
517
525
  }
518
526
  /**
527
+ * How often an open, visible widget re-sends its read receipt. The room drops
528
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
529
+ * window would otherwise stay "unread" and be emailed to the visitor.
530
+ */
531
+ const READ_RECEIPT_REFRESH_MS = 15e3;
532
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
533
+ const WAITING_NOTICE_AFTER_MS = 12e4;
534
+ /**
535
+ * When the visitor's latest message was sent, if nobody has answered it yet;
536
+ * null when the latest message is a reply or there is nothing waiting. Failed
537
+ * sends don't count: nobody was notified about them.
538
+ */
539
+ function awaitingReplySince(messages) {
540
+ for (let index = messages.length - 1; index >= 0; index--) {
541
+ const message = messages[index];
542
+ if (!message || message.delivery === "failed") continue;
543
+ if (message.senderType !== "visitor") return null;
544
+ return new Date(message.createdAt).getTime();
545
+ }
546
+ return null;
547
+ }
548
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
549
+ function useWaitingNotice(messages) {
550
+ const [due, setDue] = (0, react.useState)(false);
551
+ (0, react.useEffect)(() => {
552
+ const since = awaitingReplySince(messages);
553
+ if (since === null) {
554
+ setDue(false);
555
+ return;
556
+ }
557
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
558
+ if (remaining <= 0) {
559
+ setDue(true);
560
+ return;
561
+ }
562
+ setDue(false);
563
+ const timer = setTimeout(() => {
564
+ setDue(true);
565
+ }, remaining);
566
+ return () => {
567
+ clearTimeout(timer);
568
+ };
569
+ }, [messages]);
570
+ return due;
571
+ }
572
+ function waitingNoticeText(email) {
573
+ const address = email?.trim();
574
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
575
+ }
576
+ /**
519
577
  * Whether a seeded visitor is complete enough to skip the form.
520
578
  *
521
579
  * Both fields have to be present and non-blank: the server requires each, so
@@ -553,6 +611,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
553
611
  if (createTransport) return;
554
612
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
555
613
  }, []);
614
+ const showWaitingNotice = useWaitingNotice(messages);
556
615
  const themeStyle = useThemeStyle(session, style);
557
616
  const resolvedTheme = useResolvedTheme(theme);
558
617
  (0, react.useEffect)(() => () => {
@@ -595,6 +654,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
595
654
  if (transportRef.current === connection) transportRef.current = null;
596
655
  };
597
656
  }, [createTransport, session]);
657
+ (0, react.useEffect)(() => {
658
+ if (!(isOpen && isReady)) return;
659
+ const markReadIfVisible = () => {
660
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
661
+ };
662
+ markReadIfVisible();
663
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
664
+ document.addEventListener("visibilitychange", markReadIfVisible);
665
+ return () => {
666
+ window.clearInterval(interval);
667
+ document.removeEventListener("visibilitychange", markReadIfVisible);
668
+ };
669
+ }, [
670
+ isOpen,
671
+ isReady,
672
+ messages
673
+ ]);
598
674
  const startSession = (0, react.useCallback)(async (identity) => {
599
675
  setError(null);
600
676
  setIsConnecting(true);
@@ -711,14 +787,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
711
787
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
712
788
  className: "clivly-chat-widget__body",
713
789
  ref: listRef,
714
- children: [error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
715
- className: "clivly-chat-widget__error",
716
- children: error
717
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
718
- greeting,
719
- hasSession: Boolean(session),
720
- messages
721
- })]
790
+ children: [
791
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
792
+ className: "clivly-chat-widget__error",
793
+ children: error
794
+ }) : null,
795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
796
+ greeting,
797
+ hasSession: Boolean(session),
798
+ messages
799
+ }),
800
+ session && showWaitingNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
801
+ className: "clivly-chat-widget__notice",
802
+ role: "status",
803
+ children: waitingNoticeText(session.contact?.email)
804
+ }) : null
805
+ ]
722
806
  }),
723
807
  session ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
724
808
  className: "clivly-chat-widget__composer",
@@ -861,6 +945,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
861
945
  close: () => {
862
946
  socket.close();
863
947
  },
948
+ markRead: () => {
949
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
950
+ },
864
951
  sendMessage: (message) => {
865
952
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
866
953
  socket.send(JSON.stringify(message));
@@ -98,6 +98,12 @@ interface ChatTransportHandlers {
98
98
  }
99
99
  interface ChatTransportConnection {
100
100
  close: () => void;
101
+ /**
102
+ * Tell the server the visitor has seen the thread, so a teammate's reply
103
+ * they've read isn't also emailed to them. Optional: a custom transport
104
+ * without it still works, its visitors are just emailed replies they saw.
105
+ */
106
+ markRead?: () => void;
101
107
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
102
108
  }
103
109
  interface CreateChatTransportOptions {
@@ -98,6 +98,12 @@ interface ChatTransportHandlers {
98
98
  }
99
99
  interface ChatTransportConnection {
100
100
  close: () => void;
101
+ /**
102
+ * Tell the server the visitor has seen the thread, so a teammate's reply
103
+ * they've read isn't also emailed to them. Optional: a custom transport
104
+ * without it still works, its visitors are just emailed replies they saw.
105
+ */
106
+ markRead?: () => void;
101
107
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
102
108
  }
103
109
  interface CreateChatTransportOptions {
@@ -363,6 +363,14 @@ const WIDGET_STYLES = `
363
363
  font-size: 0.875rem;
364
364
  padding: 0.75rem;
365
365
  }
366
+ .clivly-chat-widget__notice {
367
+ background: var(--clivly-chat-operator);
368
+ border-radius: 0.8rem;
369
+ color: var(--clivly-chat-muted);
370
+ font-size: 0.8125rem;
371
+ margin: 0;
372
+ padding: 0.625rem 0.75rem;
373
+ }
366
374
  .clivly-chat-widget__empty {
367
375
  color: var(--clivly-chat-muted);
368
376
  font-size: 0.875rem;
@@ -515,6 +523,56 @@ function MessageList({ greeting, hasSession, messages }) {
515
523
  return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
516
524
  }
517
525
  /**
526
+ * How often an open, visible widget re-sends its read receipt. The room drops
527
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
528
+ * window would otherwise stay "unread" and be emailed to the visitor.
529
+ */
530
+ const READ_RECEIPT_REFRESH_MS = 15e3;
531
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
532
+ const WAITING_NOTICE_AFTER_MS = 12e4;
533
+ /**
534
+ * When the visitor's latest message was sent, if nobody has answered it yet;
535
+ * null when the latest message is a reply or there is nothing waiting. Failed
536
+ * sends don't count: nobody was notified about them.
537
+ */
538
+ function awaitingReplySince(messages) {
539
+ for (let index = messages.length - 1; index >= 0; index--) {
540
+ const message = messages[index];
541
+ if (!message || message.delivery === "failed") continue;
542
+ if (message.senderType !== "visitor") return null;
543
+ return new Date(message.createdAt).getTime();
544
+ }
545
+ return null;
546
+ }
547
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
548
+ function useWaitingNotice(messages) {
549
+ const [due, setDue] = useState(false);
550
+ useEffect(() => {
551
+ const since = awaitingReplySince(messages);
552
+ if (since === null) {
553
+ setDue(false);
554
+ return;
555
+ }
556
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
557
+ if (remaining <= 0) {
558
+ setDue(true);
559
+ return;
560
+ }
561
+ setDue(false);
562
+ const timer = setTimeout(() => {
563
+ setDue(true);
564
+ }, remaining);
565
+ return () => {
566
+ clearTimeout(timer);
567
+ };
568
+ }, [messages]);
569
+ return due;
570
+ }
571
+ function waitingNoticeText(email) {
572
+ const address = email?.trim();
573
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
574
+ }
575
+ /**
518
576
  * Whether a seeded visitor is complete enough to skip the form.
519
577
  *
520
578
  * Both fields have to be present and non-blank: the server requires each, so
@@ -552,6 +610,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
552
610
  if (createTransport) return;
553
611
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
554
612
  }, []);
613
+ const showWaitingNotice = useWaitingNotice(messages);
555
614
  const themeStyle = useThemeStyle(session, style);
556
615
  const resolvedTheme = useResolvedTheme(theme);
557
616
  useEffect(() => () => {
@@ -594,6 +653,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
594
653
  if (transportRef.current === connection) transportRef.current = null;
595
654
  };
596
655
  }, [createTransport, session]);
656
+ useEffect(() => {
657
+ if (!(isOpen && isReady)) return;
658
+ const markReadIfVisible = () => {
659
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
660
+ };
661
+ markReadIfVisible();
662
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
663
+ document.addEventListener("visibilitychange", markReadIfVisible);
664
+ return () => {
665
+ window.clearInterval(interval);
666
+ document.removeEventListener("visibilitychange", markReadIfVisible);
667
+ };
668
+ }, [
669
+ isOpen,
670
+ isReady,
671
+ messages
672
+ ]);
597
673
  const startSession = useCallback(async (identity) => {
598
674
  setError(null);
599
675
  setIsConnecting(true);
@@ -710,14 +786,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
710
786
  /* @__PURE__ */ jsxs("div", {
711
787
  className: "clivly-chat-widget__body",
712
788
  ref: listRef,
713
- children: [error ? /* @__PURE__ */ jsx("div", {
714
- className: "clivly-chat-widget__error",
715
- children: error
716
- }) : null, /* @__PURE__ */ jsx(MessageList, {
717
- greeting,
718
- hasSession: Boolean(session),
719
- messages
720
- })]
789
+ children: [
790
+ error ? /* @__PURE__ */ jsx("div", {
791
+ className: "clivly-chat-widget__error",
792
+ children: error
793
+ }) : null,
794
+ /* @__PURE__ */ jsx(MessageList, {
795
+ greeting,
796
+ hasSession: Boolean(session),
797
+ messages
798
+ }),
799
+ session && showWaitingNotice ? /* @__PURE__ */ jsx("p", {
800
+ className: "clivly-chat-widget__notice",
801
+ role: "status",
802
+ children: waitingNoticeText(session.contact?.email)
803
+ }) : null
804
+ ]
721
805
  }),
722
806
  session ? /* @__PURE__ */ jsxs("form", {
723
807
  className: "clivly-chat-widget__composer",
@@ -860,6 +944,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
860
944
  close: () => {
861
945
  socket.close();
862
946
  },
947
+ markRead: () => {
948
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
949
+ },
863
950
  sendMessage: (message) => {
864
951
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
865
952
  socket.send(JSON.stringify(message));
package/dist/browser.cjs CHANGED
@@ -364,6 +364,14 @@ const WIDGET_STYLES = `
364
364
  font-size: 0.875rem;
365
365
  padding: 0.75rem;
366
366
  }
367
+ .clivly-chat-widget__notice {
368
+ background: var(--clivly-chat-operator);
369
+ border-radius: 0.8rem;
370
+ color: var(--clivly-chat-muted);
371
+ font-size: 0.8125rem;
372
+ margin: 0;
373
+ padding: 0.625rem 0.75rem;
374
+ }
367
375
  .clivly-chat-widget__empty {
368
376
  color: var(--clivly-chat-muted);
369
377
  font-size: 0.875rem;
@@ -516,6 +524,56 @@ function MessageList({ greeting, hasSession, messages }) {
516
524
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, { message }, message.id)) });
517
525
  }
518
526
  /**
527
+ * How often an open, visible widget re-sends its read receipt. The room drops
528
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
529
+ * window would otherwise stay "unread" and be emailed to the visitor.
530
+ */
531
+ const READ_RECEIPT_REFRESH_MS = 15e3;
532
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
533
+ const WAITING_NOTICE_AFTER_MS = 12e4;
534
+ /**
535
+ * When the visitor's latest message was sent, if nobody has answered it yet;
536
+ * null when the latest message is a reply or there is nothing waiting. Failed
537
+ * sends don't count: nobody was notified about them.
538
+ */
539
+ function awaitingReplySince(messages) {
540
+ for (let index = messages.length - 1; index >= 0; index--) {
541
+ const message = messages[index];
542
+ if (!message || message.delivery === "failed") continue;
543
+ if (message.senderType !== "visitor") return null;
544
+ return new Date(message.createdAt).getTime();
545
+ }
546
+ return null;
547
+ }
548
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
549
+ function useWaitingNotice(messages) {
550
+ const [due, setDue] = (0, react.useState)(false);
551
+ (0, react.useEffect)(() => {
552
+ const since = awaitingReplySince(messages);
553
+ if (since === null) {
554
+ setDue(false);
555
+ return;
556
+ }
557
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
558
+ if (remaining <= 0) {
559
+ setDue(true);
560
+ return;
561
+ }
562
+ setDue(false);
563
+ const timer = setTimeout(() => {
564
+ setDue(true);
565
+ }, remaining);
566
+ return () => {
567
+ clearTimeout(timer);
568
+ };
569
+ }, [messages]);
570
+ return due;
571
+ }
572
+ function waitingNoticeText(email) {
573
+ const address = email?.trim();
574
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
575
+ }
576
+ /**
519
577
  * Whether a seeded visitor is complete enough to skip the form.
520
578
  *
521
579
  * Both fields have to be present and non-blank: the server requires each, so
@@ -553,6 +611,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
553
611
  if (createTransport) return;
554
612
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
555
613
  }, []);
614
+ const showWaitingNotice = useWaitingNotice(messages);
556
615
  const themeStyle = useThemeStyle(session, style);
557
616
  const resolvedTheme = useResolvedTheme(theme);
558
617
  (0, react.useEffect)(() => () => {
@@ -595,6 +654,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
595
654
  if (transportRef.current === connection) transportRef.current = null;
596
655
  };
597
656
  }, [createTransport, session]);
657
+ (0, react.useEffect)(() => {
658
+ if (!(isOpen && isReady)) return;
659
+ const markReadIfVisible = () => {
660
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
661
+ };
662
+ markReadIfVisible();
663
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
664
+ document.addEventListener("visibilitychange", markReadIfVisible);
665
+ return () => {
666
+ window.clearInterval(interval);
667
+ document.removeEventListener("visibilitychange", markReadIfVisible);
668
+ };
669
+ }, [
670
+ isOpen,
671
+ isReady,
672
+ messages
673
+ ]);
598
674
  const startSession = (0, react.useCallback)(async (identity) => {
599
675
  setError(null);
600
676
  setIsConnecting(true);
@@ -711,14 +787,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
711
787
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
712
788
  className: "clivly-chat-widget__body",
713
789
  ref: listRef,
714
- children: [error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
715
- className: "clivly-chat-widget__error",
716
- children: error
717
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
718
- greeting,
719
- hasSession: Boolean(session),
720
- messages
721
- })]
790
+ children: [
791
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
792
+ className: "clivly-chat-widget__error",
793
+ children: error
794
+ }) : null,
795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
796
+ greeting,
797
+ hasSession: Boolean(session),
798
+ messages
799
+ }),
800
+ session && showWaitingNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
801
+ className: "clivly-chat-widget__notice",
802
+ role: "status",
803
+ children: waitingNoticeText(session.contact?.email)
804
+ }) : null
805
+ ]
722
806
  }),
723
807
  session ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
724
808
  className: "clivly-chat-widget__composer",
@@ -861,6 +945,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
861
945
  close: () => {
862
946
  socket.close();
863
947
  },
948
+ markRead: () => {
949
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
950
+ },
864
951
  sendMessage: (message) => {
865
952
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
866
953
  socket.send(JSON.stringify(message));
@@ -98,6 +98,12 @@ interface ChatTransportHandlers {
98
98
  }
99
99
  interface ChatTransportConnection {
100
100
  close: () => void;
101
+ /**
102
+ * Tell the server the visitor has seen the thread, so a teammate's reply
103
+ * they've read isn't also emailed to them. Optional: a custom transport
104
+ * without it still works, its visitors are just emailed replies they saw.
105
+ */
106
+ markRead?: () => void;
101
107
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
102
108
  }
103
109
  interface CreateChatTransportOptions {
@@ -98,6 +98,12 @@ interface ChatTransportHandlers {
98
98
  }
99
99
  interface ChatTransportConnection {
100
100
  close: () => void;
101
+ /**
102
+ * Tell the server the visitor has seen the thread, so a teammate's reply
103
+ * they've read isn't also emailed to them. Optional: a custom transport
104
+ * without it still works, its visitors are just emailed replies they saw.
105
+ */
106
+ markRead?: () => void;
101
107
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
102
108
  }
103
109
  interface CreateChatTransportOptions {
package/dist/browser.mjs CHANGED
@@ -363,6 +363,14 @@ const WIDGET_STYLES = `
363
363
  font-size: 0.875rem;
364
364
  padding: 0.75rem;
365
365
  }
366
+ .clivly-chat-widget__notice {
367
+ background: var(--clivly-chat-operator);
368
+ border-radius: 0.8rem;
369
+ color: var(--clivly-chat-muted);
370
+ font-size: 0.8125rem;
371
+ margin: 0;
372
+ padding: 0.625rem 0.75rem;
373
+ }
366
374
  .clivly-chat-widget__empty {
367
375
  color: var(--clivly-chat-muted);
368
376
  font-size: 0.875rem;
@@ -515,6 +523,56 @@ function MessageList({ greeting, hasSession, messages }) {
515
523
  return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
516
524
  }
517
525
  /**
526
+ * How often an open, visible widget re-sends its read receipt. The room drops
527
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
528
+ * window would otherwise stay "unread" and be emailed to the visitor.
529
+ */
530
+ const READ_RECEIPT_REFRESH_MS = 15e3;
531
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
532
+ const WAITING_NOTICE_AFTER_MS = 12e4;
533
+ /**
534
+ * When the visitor's latest message was sent, if nobody has answered it yet;
535
+ * null when the latest message is a reply or there is nothing waiting. Failed
536
+ * sends don't count: nobody was notified about them.
537
+ */
538
+ function awaitingReplySince(messages) {
539
+ for (let index = messages.length - 1; index >= 0; index--) {
540
+ const message = messages[index];
541
+ if (!message || message.delivery === "failed") continue;
542
+ if (message.senderType !== "visitor") return null;
543
+ return new Date(message.createdAt).getTime();
544
+ }
545
+ return null;
546
+ }
547
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
548
+ function useWaitingNotice(messages) {
549
+ const [due, setDue] = useState(false);
550
+ useEffect(() => {
551
+ const since = awaitingReplySince(messages);
552
+ if (since === null) {
553
+ setDue(false);
554
+ return;
555
+ }
556
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
557
+ if (remaining <= 0) {
558
+ setDue(true);
559
+ return;
560
+ }
561
+ setDue(false);
562
+ const timer = setTimeout(() => {
563
+ setDue(true);
564
+ }, remaining);
565
+ return () => {
566
+ clearTimeout(timer);
567
+ };
568
+ }, [messages]);
569
+ return due;
570
+ }
571
+ function waitingNoticeText(email) {
572
+ const address = email?.trim();
573
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
574
+ }
575
+ /**
518
576
  * Whether a seeded visitor is complete enough to skip the form.
519
577
  *
520
578
  * Both fields have to be present and non-blank: the server requires each, so
@@ -552,6 +610,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
552
610
  if (createTransport) return;
553
611
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
554
612
  }, []);
613
+ const showWaitingNotice = useWaitingNotice(messages);
555
614
  const themeStyle = useThemeStyle(session, style);
556
615
  const resolvedTheme = useResolvedTheme(theme);
557
616
  useEffect(() => () => {
@@ -594,6 +653,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
594
653
  if (transportRef.current === connection) transportRef.current = null;
595
654
  };
596
655
  }, [createTransport, session]);
656
+ useEffect(() => {
657
+ if (!(isOpen && isReady)) return;
658
+ const markReadIfVisible = () => {
659
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
660
+ };
661
+ markReadIfVisible();
662
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
663
+ document.addEventListener("visibilitychange", markReadIfVisible);
664
+ return () => {
665
+ window.clearInterval(interval);
666
+ document.removeEventListener("visibilitychange", markReadIfVisible);
667
+ };
668
+ }, [
669
+ isOpen,
670
+ isReady,
671
+ messages
672
+ ]);
597
673
  const startSession = useCallback(async (identity) => {
598
674
  setError(null);
599
675
  setIsConnecting(true);
@@ -710,14 +786,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
710
786
  /* @__PURE__ */ jsxs("div", {
711
787
  className: "clivly-chat-widget__body",
712
788
  ref: listRef,
713
- children: [error ? /* @__PURE__ */ jsx("div", {
714
- className: "clivly-chat-widget__error",
715
- children: error
716
- }) : null, /* @__PURE__ */ jsx(MessageList, {
717
- greeting,
718
- hasSession: Boolean(session),
719
- messages
720
- })]
789
+ children: [
790
+ error ? /* @__PURE__ */ jsx("div", {
791
+ className: "clivly-chat-widget__error",
792
+ children: error
793
+ }) : null,
794
+ /* @__PURE__ */ jsx(MessageList, {
795
+ greeting,
796
+ hasSession: Boolean(session),
797
+ messages
798
+ }),
799
+ session && showWaitingNotice ? /* @__PURE__ */ jsx("p", {
800
+ className: "clivly-chat-widget__notice",
801
+ role: "status",
802
+ children: waitingNoticeText(session.contact?.email)
803
+ }) : null
804
+ ]
721
805
  }),
722
806
  session ? /* @__PURE__ */ jsxs("form", {
723
807
  className: "clivly-chat-widget__composer",
@@ -860,6 +944,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
860
944
  close: () => {
861
945
  socket.close();
862
946
  },
947
+ markRead: () => {
948
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
949
+ },
863
950
  sendMessage: (message) => {
864
951
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
865
952
  socket.send(JSON.stringify(message));
package/dist/index.cjs CHANGED
@@ -364,6 +364,14 @@ const WIDGET_STYLES = `
364
364
  font-size: 0.875rem;
365
365
  padding: 0.75rem;
366
366
  }
367
+ .clivly-chat-widget__notice {
368
+ background: var(--clivly-chat-operator);
369
+ border-radius: 0.8rem;
370
+ color: var(--clivly-chat-muted);
371
+ font-size: 0.8125rem;
372
+ margin: 0;
373
+ padding: 0.625rem 0.75rem;
374
+ }
367
375
  .clivly-chat-widget__empty {
368
376
  color: var(--clivly-chat-muted);
369
377
  font-size: 0.875rem;
@@ -516,6 +524,56 @@ function MessageList({ greeting, hasSession, messages }) {
516
524
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, { message }, message.id)) });
517
525
  }
518
526
  /**
527
+ * How often an open, visible widget re-sends its read receipt. The room drops
528
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
529
+ * window would otherwise stay "unread" and be emailed to the visitor.
530
+ */
531
+ const READ_RECEIPT_REFRESH_MS = 15e3;
532
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
533
+ const WAITING_NOTICE_AFTER_MS = 12e4;
534
+ /**
535
+ * When the visitor's latest message was sent, if nobody has answered it yet;
536
+ * null when the latest message is a reply or there is nothing waiting. Failed
537
+ * sends don't count: nobody was notified about them.
538
+ */
539
+ function awaitingReplySince(messages) {
540
+ for (let index = messages.length - 1; index >= 0; index--) {
541
+ const message = messages[index];
542
+ if (!message || message.delivery === "failed") continue;
543
+ if (message.senderType !== "visitor") return null;
544
+ return new Date(message.createdAt).getTime();
545
+ }
546
+ return null;
547
+ }
548
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
549
+ function useWaitingNotice(messages) {
550
+ const [due, setDue] = (0, react.useState)(false);
551
+ (0, react.useEffect)(() => {
552
+ const since = awaitingReplySince(messages);
553
+ if (since === null) {
554
+ setDue(false);
555
+ return;
556
+ }
557
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
558
+ if (remaining <= 0) {
559
+ setDue(true);
560
+ return;
561
+ }
562
+ setDue(false);
563
+ const timer = setTimeout(() => {
564
+ setDue(true);
565
+ }, remaining);
566
+ return () => {
567
+ clearTimeout(timer);
568
+ };
569
+ }, [messages]);
570
+ return due;
571
+ }
572
+ function waitingNoticeText(email) {
573
+ const address = email?.trim();
574
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
575
+ }
576
+ /**
519
577
  * Whether a seeded visitor is complete enough to skip the form.
520
578
  *
521
579
  * Both fields have to be present and non-blank: the server requires each, so
@@ -553,6 +611,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
553
611
  if (createTransport) return;
554
612
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
555
613
  }, []);
614
+ const showWaitingNotice = useWaitingNotice(messages);
556
615
  const themeStyle = useThemeStyle(session, style);
557
616
  const resolvedTheme = useResolvedTheme(theme);
558
617
  (0, react.useEffect)(() => () => {
@@ -595,6 +654,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
595
654
  if (transportRef.current === connection) transportRef.current = null;
596
655
  };
597
656
  }, [createTransport, session]);
657
+ (0, react.useEffect)(() => {
658
+ if (!(isOpen && isReady)) return;
659
+ const markReadIfVisible = () => {
660
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
661
+ };
662
+ markReadIfVisible();
663
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
664
+ document.addEventListener("visibilitychange", markReadIfVisible);
665
+ return () => {
666
+ window.clearInterval(interval);
667
+ document.removeEventListener("visibilitychange", markReadIfVisible);
668
+ };
669
+ }, [
670
+ isOpen,
671
+ isReady,
672
+ messages
673
+ ]);
598
674
  const startSession = (0, react.useCallback)(async (identity) => {
599
675
  setError(null);
600
676
  setIsConnecting(true);
@@ -711,14 +787,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
711
787
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
712
788
  className: "clivly-chat-widget__body",
713
789
  ref: listRef,
714
- children: [error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
715
- className: "clivly-chat-widget__error",
716
- children: error
717
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
718
- greeting,
719
- hasSession: Boolean(session),
720
- messages
721
- })]
790
+ children: [
791
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
792
+ className: "clivly-chat-widget__error",
793
+ children: error
794
+ }) : null,
795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
796
+ greeting,
797
+ hasSession: Boolean(session),
798
+ messages
799
+ }),
800
+ session && showWaitingNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
801
+ className: "clivly-chat-widget__notice",
802
+ role: "status",
803
+ children: waitingNoticeText(session.contact?.email)
804
+ }) : null
805
+ ]
722
806
  }),
723
807
  session ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
724
808
  className: "clivly-chat-widget__composer",
@@ -876,6 +960,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
876
960
  close: () => {
877
961
  socket.close();
878
962
  },
963
+ markRead: () => {
964
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
965
+ },
879
966
  sendMessage: (message) => {
880
967
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
881
968
  socket.send(JSON.stringify(message));
package/dist/index.d.cts CHANGED
@@ -79,7 +79,11 @@ interface WidgetClientCreateMessageEvent {
79
79
  id: string;
80
80
  type: "message.create";
81
81
  }
82
- type WidgetClientEvent = WidgetClientCreateMessageEvent;
82
+ /** The visitor has seen the thread (see `ChatTransportConnection.markRead`). */
83
+ interface WidgetClientReadEvent {
84
+ type: "conversation.read";
85
+ }
86
+ type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent;
83
87
  interface WidgetSessionReadyEvent {
84
88
  conversationId: string;
85
89
  type: "session.ready";
@@ -113,6 +117,12 @@ interface ChatTransportHandlers {
113
117
  }
114
118
  interface ChatTransportConnection {
115
119
  close: () => void;
120
+ /**
121
+ * Tell the server the visitor has seen the thread, so a teammate's reply
122
+ * they've read isn't also emailed to them. Optional: a custom transport
123
+ * without it still works, its visitors are just emailed replies they saw.
124
+ */
125
+ markRead?: () => void;
116
126
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
117
127
  }
118
128
  interface CreateChatTransportOptions {
@@ -236,4 +246,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
236
246
  export declare const mountChatWidget: typeof mountChatWidget$1;
237
247
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
238
248
  //#endregion
239
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
249
+ export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
package/dist/index.d.mts CHANGED
@@ -79,7 +79,11 @@ interface WidgetClientCreateMessageEvent {
79
79
  id: string;
80
80
  type: "message.create";
81
81
  }
82
- type WidgetClientEvent = WidgetClientCreateMessageEvent;
82
+ /** The visitor has seen the thread (see `ChatTransportConnection.markRead`). */
83
+ interface WidgetClientReadEvent {
84
+ type: "conversation.read";
85
+ }
86
+ type WidgetClientEvent = WidgetClientCreateMessageEvent | WidgetClientReadEvent;
83
87
  interface WidgetSessionReadyEvent {
84
88
  conversationId: string;
85
89
  type: "session.ready";
@@ -113,6 +117,12 @@ interface ChatTransportHandlers {
113
117
  }
114
118
  interface ChatTransportConnection {
115
119
  close: () => void;
120
+ /**
121
+ * Tell the server the visitor has seen the thread, so a teammate's reply
122
+ * they've read isn't also emailed to them. Optional: a custom transport
123
+ * without it still works, its visitors are just emailed replies they saw.
124
+ */
125
+ markRead?: () => void;
116
126
  sendMessage: (event: WidgetClientCreateMessageEvent) => void;
117
127
  }
118
128
  interface CreateChatTransportOptions {
@@ -236,4 +246,4 @@ export declare const createClivlyTransport: typeof createClivlyTransport$1;
236
246
  export declare const mountChatWidget: typeof mountChatWidget$1;
237
247
  export declare const mountChatWidgetScript: typeof mountChatWidgetScript$1;
238
248
  //#endregion
239
- export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
249
+ export type { ChatTransportConnection, ChatTransportHandlers, ChatWidgetProps, ChatWidgetTheme, ChatWidgetVisitor, CreateChatTransport, CreateChatTransportOptions, CreateLoopbackTransportOptions, CreateWebSocketTransportOptions, GetSession, GetSessionOptions, ResolvedTheme, WidgetClientCreateMessageEvent, WidgetClientEvent, WidgetClientReadEvent, WidgetConfig, WidgetContact, WidgetConversation, WidgetConversationSnapshotEvent, WidgetErrorEvent, WidgetMessage, WidgetMessageAckEvent, WidgetMessageCreatedEvent, WidgetSenderType, WidgetServerEvent, WidgetSessionPayload, WidgetSessionReadyEvent, WidgetSummary };
package/dist/index.mjs CHANGED
@@ -363,6 +363,14 @@ const WIDGET_STYLES = `
363
363
  font-size: 0.875rem;
364
364
  padding: 0.75rem;
365
365
  }
366
+ .clivly-chat-widget__notice {
367
+ background: var(--clivly-chat-operator);
368
+ border-radius: 0.8rem;
369
+ color: var(--clivly-chat-muted);
370
+ font-size: 0.8125rem;
371
+ margin: 0;
372
+ padding: 0.625rem 0.75rem;
373
+ }
366
374
  .clivly-chat-widget__empty {
367
375
  color: var(--clivly-chat-muted);
368
376
  font-size: 0.875rem;
@@ -515,6 +523,56 @@ function MessageList({ greeting, hasSession, messages }) {
515
523
  return /* @__PURE__ */ jsx(Fragment, { children: messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)) });
516
524
  }
517
525
  /**
526
+ * How often an open, visible widget re-sends its read receipt. The room drops
527
+ * a receipt that arrives within 5 s of the last one, so a reply read in that
528
+ * window would otherwise stay "unread" and be emailed to the visitor.
529
+ */
530
+ const READ_RECEIPT_REFRESH_MS = 15e3;
531
+ /** How long a visitor waits unanswered before the widget reassures them (D20a). */
532
+ const WAITING_NOTICE_AFTER_MS = 12e4;
533
+ /**
534
+ * When the visitor's latest message was sent, if nobody has answered it yet;
535
+ * null when the latest message is a reply or there is nothing waiting. Failed
536
+ * sends don't count: nobody was notified about them.
537
+ */
538
+ function awaitingReplySince(messages) {
539
+ for (let index = messages.length - 1; index >= 0; index--) {
540
+ const message = messages[index];
541
+ if (!message || message.delivery === "failed") continue;
542
+ if (message.senderType !== "visitor") return null;
543
+ return new Date(message.createdAt).getTime();
544
+ }
545
+ return null;
546
+ }
547
+ /** True once the visitor has waited `WAITING_NOTICE_AFTER_MS` unanswered. */
548
+ function useWaitingNotice(messages) {
549
+ const [due, setDue] = useState(false);
550
+ useEffect(() => {
551
+ const since = awaitingReplySince(messages);
552
+ if (since === null) {
553
+ setDue(false);
554
+ return;
555
+ }
556
+ const remaining = since + WAITING_NOTICE_AFTER_MS - Date.now();
557
+ if (remaining <= 0) {
558
+ setDue(true);
559
+ return;
560
+ }
561
+ setDue(false);
562
+ const timer = setTimeout(() => {
563
+ setDue(true);
564
+ }, remaining);
565
+ return () => {
566
+ clearTimeout(timer);
567
+ };
568
+ }, [messages]);
569
+ return due;
570
+ }
571
+ function waitingNoticeText(email) {
572
+ const address = email?.trim();
573
+ return address ? `We've notified the team — we'll reply here or by email at ${address}.` : "We've notified the team — we'll reply here as soon as we can.";
574
+ }
575
+ /**
518
576
  * Whether a seeded visitor is complete enough to skip the form.
519
577
  *
520
578
  * Both fields have to be present and non-blank: the server requires each, so
@@ -552,6 +610,7 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
552
610
  if (createTransport) return;
553
611
  console.warn("[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start");
554
612
  }, []);
613
+ const showWaitingNotice = useWaitingNotice(messages);
555
614
  const themeStyle = useThemeStyle(session, style);
556
615
  const resolvedTheme = useResolvedTheme(theme);
557
616
  useEffect(() => () => {
@@ -594,6 +653,23 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
594
653
  if (transportRef.current === connection) transportRef.current = null;
595
654
  };
596
655
  }, [createTransport, session]);
656
+ useEffect(() => {
657
+ if (!(isOpen && isReady)) return;
658
+ const markReadIfVisible = () => {
659
+ if (document.visibilityState === "visible") transportRef.current?.markRead?.();
660
+ };
661
+ markReadIfVisible();
662
+ const interval = window.setInterval(markReadIfVisible, READ_RECEIPT_REFRESH_MS);
663
+ document.addEventListener("visibilitychange", markReadIfVisible);
664
+ return () => {
665
+ window.clearInterval(interval);
666
+ document.removeEventListener("visibilitychange", markReadIfVisible);
667
+ };
668
+ }, [
669
+ isOpen,
670
+ isReady,
671
+ messages
672
+ ]);
597
673
  const startSession = useCallback(async (identity) => {
598
674
  setError(null);
599
675
  setIsConnecting(true);
@@ -710,14 +786,22 @@ function ChatWidget({ className, createTransport, defaultVisitor, getSession, in
710
786
  /* @__PURE__ */ jsxs("div", {
711
787
  className: "clivly-chat-widget__body",
712
788
  ref: listRef,
713
- children: [error ? /* @__PURE__ */ jsx("div", {
714
- className: "clivly-chat-widget__error",
715
- children: error
716
- }) : null, /* @__PURE__ */ jsx(MessageList, {
717
- greeting,
718
- hasSession: Boolean(session),
719
- messages
720
- })]
789
+ children: [
790
+ error ? /* @__PURE__ */ jsx("div", {
791
+ className: "clivly-chat-widget__error",
792
+ children: error
793
+ }) : null,
794
+ /* @__PURE__ */ jsx(MessageList, {
795
+ greeting,
796
+ hasSession: Boolean(session),
797
+ messages
798
+ }),
799
+ session && showWaitingNotice ? /* @__PURE__ */ jsx("p", {
800
+ className: "clivly-chat-widget__notice",
801
+ role: "status",
802
+ children: waitingNoticeText(session.contact?.email)
803
+ }) : null
804
+ ]
721
805
  }),
722
806
  session ? /* @__PURE__ */ jsxs("form", {
723
807
  className: "clivly-chat-widget__composer",
@@ -875,6 +959,9 @@ function createWebSocketTransport({ createSocket = (url, protocols) => new WebSo
875
959
  close: () => {
876
960
  socket.close();
877
961
  },
962
+ markRead: () => {
963
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "conversation.read" }));
964
+ },
878
965
  sendMessage: (message) => {
879
966
  if (socket.readyState !== WebSocket.OPEN) throw new Error("Chat socket is not connected.");
880
967
  socket.send(JSON.stringify(message));
@@ -344,7 +344,7 @@ process.env.NODE_ENV!==`production`&&(function(){function t(e){if(e==null)return
344
344
  <%s {...props} />
345
345
  React keys must be passed directly to JSX without using spread:
346
346
  let props = %s;
347
- <%s key={someKey} {...props} />`,o,f,m,f),le[f+o]=!0)}if(f=null,i!==void 0&&(r(i),f=``+i),c(n)&&(r(n.key),f=``+n.key),`key`in n)for(var h in i={},n)h!==`key`&&(i[h]=n[h]);else i=n;return f&&l(i,typeof e==`function`?e.displayName||e.name||`Unknown`:e),d(e,f,i,a(),s,u)}function p(e){m(e)?e._store&&(e._store.validated=1):typeof e==`object`&&e&&e.$$typeof===re&&(e._payload.status===`fulfilled`?m(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function m(e){return typeof e==`object`&&!!e&&e.$$typeof===g}var h=o(),g=Symbol.for(`react.transitional.element`),_=Symbol.for(`react.portal`),v=Symbol.for(`react.fragment`),y=Symbol.for(`react.strict_mode`),b=Symbol.for(`react.profiler`),x=Symbol.for(`react.consumer`),ee=Symbol.for(`react.context`),S=Symbol.for(`react.forward_ref`),C=Symbol.for(`react.suspense`),te=Symbol.for(`react.suspense_list`),ne=Symbol.for(`react.memo`),re=Symbol.for(`react.lazy`),w=Symbol.for(`react.activity`),ie=Symbol.for(`react.client.reference`),T=h.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ae=Object.prototype.hasOwnProperty,oe=Array.isArray,E=console.createTask?console.createTask:function(){return null};h={react_stack_bottom_frame:function(e){return e()}};var se,ce={},D=h.react_stack_bottom_frame.bind(h,s)(),O=E(i(s)),le={};e.Fragment=v,e.jsx=function(e,t,n){var r=1e4>T.recentlyCreatedOwnerStacks++;return f(e,t,n,!1,r?Error(`react-stack-top-frame`):D,r?E(i(e)):O)},e.jsxs=function(e,t,n){var r=1e4>T.recentlyCreatedOwnerStacks++;return f(e,t,n,!0,r?Error(`react-stack-top-frame`):D,r?E(i(e)):O)}})()})),w=e(((e,t)=>{t.exports=process.env.NODE_ENV===`production`?ne():re()}))();let ie={failed:`Failed to send`,pending:`Sending…`,sent:null},T=typeof window>`u`?void 0:window.localStorage;function ae(e){return e.contact?.name??`You`}function oe(e,t){return e?e.contact?`${e.contact.name} • ${e.contact.email}`:`Verified visitor`:t}function E(e,t=`sent`){return{...e,delivery:t}}function se(e){return`clivly-chat-widget:${e}:visitor-token`}function ce(e){let t=new Date(e);return Number.isNaN(t.getTime())?``:new Intl.DateTimeFormat(void 0,{hour:`numeric`,minute:`2-digit`,month:`short`,day:`numeric`}).format(t)}function D(e){return e.key===`Enter`&&!(e.shiftKey||e.nativeEvent.isComposing)}function O(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function le(e,t){let n=e.findIndex(e=>e.id===t.id);if(n===-1)return[...e,E(t)];let r=[...e];return r[n]=E(t),r}function ue(e,t,n){return e.map(e=>e.id===t?E(n):e)}function de(e,t){return e.map(e=>e.id===t?{...e,delivery:`failed`}:e)}function fe(e){return[...e].sort((e,t)=>e.createdAt.localeCompare(t.createdAt))}function pe(e,t){return(0,p.useMemo)(()=>{let n=e?.widget.config.brandColor??null;return n?{"--clivly-chat-accent":n,...t}:t},[e?.widget.config.brandColor,t])}function me(e){let[t,n]=(0,p.useState)(()=>S(e));return(0,p.useEffect)(()=>(n(S(e)),C(e,n)),[e]),t}function he(e,{setError:t,setIsReady:n,setMessages:r}){switch(e.type){case`conversation.snapshot`:r(fe(e.messages.map(e=>E(e))));return;case`error`:t(e.message);return;case`message.ack`:r(t=>fe(ue(t,e.clientMessageId,e.message)));return;case`message.created`:{let t=e.clientMessageId;r(n=>fe(t?ue(n,t,e.message):le(n,e.message)));return}case`session.ready`:n(!0);return;default:return}}function ge({message:e}){return(0,w.jsxs)(`article`,{className:`clivly-chat-widget__message`,"data-sender":e.senderType,children:[(0,w.jsxs)(`div`,{className:`clivly-chat-widget__message-meta`,children:[(0,w.jsx)(`span`,{children:e.author??`Clivly`}),(0,w.jsx)(`span`,{children:ce(e.createdAt)})]}),(0,w.jsx)(`div`,{children:e.body}),e.delivery===`sent`?null:(0,w.jsx)(`div`,{className:`clivly-chat-widget__status`,children:ie[e.delivery]})]})}function _e({greeting:e,hasSession:t,messages:n}){return t?n.length===0?(0,w.jsx)(`p`,{className:`clivly-chat-widget__empty`,children:`No messages yet. Say hello to get the conversation started.`}):(0,w.jsx)(w.Fragment,{children:n.map(e=>(0,w.jsx)(ge,{message:e},e.id))}):(0,w.jsx)(`p`,{className:`clivly-chat-widget__empty`,children:e})}function ve(e){return!!(e?.name?.trim()&&e?.email?.trim())}function ye(e,t){return e?t?`Conversation ready`:`Connecting…`:`Chat is unavailable`}function be({className:e,createTransport:t,defaultVisitor:n,getSession:r,initiallyOpen:i=!1,launcherLabel:a=`Chat with us`,storage:o=T,style:s,theme:c=`auto`,widgetId:l}){let[u,d]=(0,p.useState)(i),[f,m]=(0,p.useState)(null),[h,g]=(0,p.useState)(!1),[_,v]=(0,p.useState)(!1),[y,b]=(0,p.useState)(``),[x,ee]=(0,p.useState)([]),[S,C]=(0,p.useState)(null),[te,ne]=(0,p.useState)({email:n?.email??``,name:n?.name??``}),re=(0,p.useRef)(null),ie=(0,p.useRef)(null);(0,p.useEffect)(()=>{t||console.warn(`[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start`)},[]);let ce=pe(S,s),le=me(c);(0,p.useEffect)(()=>()=>{re.current?.close(),re.current=null},[]),(0,p.useEffect)(()=>{ie.current&&(ie.current.scrollTop=ie.current.scrollHeight)},[x]),(0,p.useEffect)(()=>{if(re.current?.close(),re.current=null,v(!1),!(S&&t))return;let e=t({handlers:{onClose:()=>{v(!1)},onError:e=>{m(e.message)},onEvent:e=>{he(e,{setError:m,setIsReady:v,setMessages:ee})},onOpen:()=>{v(!0)}},session:S});return re.current=e,()=>{e.close(),re.current===e&&(re.current=null)}},[t,S]);let ue=(0,p.useCallback)(async e=>{m(null),g(!0);try{let t=await r({visitor:e,visitorToken:o?.getItem(se(l))??null,widgetId:l});o?.setItem(se(l),t.visitorToken),C(t),ee(fe(t.messages.map(e=>E(e))))}catch(e){let t=e instanceof Error?e:Error(`Failed to start the conversation.`);m(t.message)}finally{g(!1)}},[r,o,l]),ge=async e=>{e.preventDefault(),await ue(te)};(0,p.useEffect)(()=>{u&&ve(n)&&(S||h||f||ue({email:n.email.trim(),name:n.name.trim()}).catch(()=>{}))},[n,f,h,u,S,ue]);let be=e=>{if(e?.preventDefault(),!(S&&re.current)){m(`Chat is not connected yet.`);return}let t=y.trim();if(!t)return;let n=O(),r={author:ae(S),body:t,createdAt:new Date().toISOString(),id:n,senderType:`visitor`};ee(e=>fe([...e,E(r,`pending`)])),b(``),m(null);try{re.current.sendMessage({body:t,id:n,type:`message.create`})}catch(e){let t=e instanceof Error?e:Error(`Failed to send your message.`);ee(e=>de(e,n)),m(t.message)}},xe=!!t&&_,k=S?.widget.config.greeting??`Tell us a little about yourself and we’ll pick up the conversation from there.`,Se=S?.widget.name??`Start a conversation`,Ce=oe(S,k);return(0,w.jsxs)(`div`,{className:`clivly-chat-widget ${e??``}`.trim(),"data-clivly-theme":le,style:ce,children:[(0,w.jsx)(`style`,{children:`
347
+ <%s key={someKey} {...props} />`,o,f,m,f),le[f+o]=!0)}if(f=null,i!==void 0&&(r(i),f=``+i),c(n)&&(r(n.key),f=``+n.key),`key`in n)for(var h in i={},n)h!==`key`&&(i[h]=n[h]);else i=n;return f&&l(i,typeof e==`function`?e.displayName||e.name||`Unknown`:e),d(e,f,i,a(),s,u)}function p(e){m(e)?e._store&&(e._store.validated=1):typeof e==`object`&&e&&e.$$typeof===re&&(e._payload.status===`fulfilled`?m(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function m(e){return typeof e==`object`&&!!e&&e.$$typeof===g}var h=o(),g=Symbol.for(`react.transitional.element`),_=Symbol.for(`react.portal`),v=Symbol.for(`react.fragment`),y=Symbol.for(`react.strict_mode`),b=Symbol.for(`react.profiler`),x=Symbol.for(`react.consumer`),ee=Symbol.for(`react.context`),S=Symbol.for(`react.forward_ref`),C=Symbol.for(`react.suspense`),te=Symbol.for(`react.suspense_list`),ne=Symbol.for(`react.memo`),re=Symbol.for(`react.lazy`),w=Symbol.for(`react.activity`),ie=Symbol.for(`react.client.reference`),T=h.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ae=Object.prototype.hasOwnProperty,oe=Array.isArray,E=console.createTask?console.createTask:function(){return null};h={react_stack_bottom_frame:function(e){return e()}};var se,ce={},D=h.react_stack_bottom_frame.bind(h,s)(),O=E(i(s)),le={};e.Fragment=v,e.jsx=function(e,t,n){var r=1e4>T.recentlyCreatedOwnerStacks++;return f(e,t,n,!1,r?Error(`react-stack-top-frame`):D,r?E(i(e)):O)},e.jsxs=function(e,t,n){var r=1e4>T.recentlyCreatedOwnerStacks++;return f(e,t,n,!0,r?Error(`react-stack-top-frame`):D,r?E(i(e)):O)}})()})),w=e(((e,t)=>{t.exports=process.env.NODE_ENV===`production`?ne():re()}))();let ie={failed:`Failed to send`,pending:`Sending…`,sent:null},T=typeof window>`u`?void 0:window.localStorage;function ae(e){return e.contact?.name??`You`}function oe(e,t){return e?e.contact?`${e.contact.name} • ${e.contact.email}`:`Verified visitor`:t}function E(e,t=`sent`){return{...e,delivery:t}}function se(e){return`clivly-chat-widget:${e}:visitor-token`}function ce(e){let t=new Date(e);return Number.isNaN(t.getTime())?``:new Intl.DateTimeFormat(void 0,{hour:`numeric`,minute:`2-digit`,month:`short`,day:`numeric`}).format(t)}function D(e){return e.key===`Enter`&&!(e.shiftKey||e.nativeEvent.isComposing)}function O(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function le(e,t){let n=e.findIndex(e=>e.id===t.id);if(n===-1)return[...e,E(t)];let r=[...e];return r[n]=E(t),r}function ue(e,t,n){return e.map(e=>e.id===t?E(n):e)}function de(e,t){return e.map(e=>e.id===t?{...e,delivery:`failed`}:e)}function fe(e){return[...e].sort((e,t)=>e.createdAt.localeCompare(t.createdAt))}function pe(e,t){return(0,p.useMemo)(()=>{let n=e?.widget.config.brandColor??null;return n?{"--clivly-chat-accent":n,...t}:t},[e?.widget.config.brandColor,t])}function me(e){let[t,n]=(0,p.useState)(()=>S(e));return(0,p.useEffect)(()=>(n(S(e)),C(e,n)),[e]),t}function he(e,{setError:t,setIsReady:n,setMessages:r}){switch(e.type){case`conversation.snapshot`:r(fe(e.messages.map(e=>E(e))));return;case`error`:t(e.message);return;case`message.ack`:r(t=>fe(ue(t,e.clientMessageId,e.message)));return;case`message.created`:{let t=e.clientMessageId;r(n=>fe(t?ue(n,t,e.message):le(n,e.message)));return}case`session.ready`:n(!0);return;default:return}}function ge({message:e}){return(0,w.jsxs)(`article`,{className:`clivly-chat-widget__message`,"data-sender":e.senderType,children:[(0,w.jsxs)(`div`,{className:`clivly-chat-widget__message-meta`,children:[(0,w.jsx)(`span`,{children:e.author??`Clivly`}),(0,w.jsx)(`span`,{children:ce(e.createdAt)})]}),(0,w.jsx)(`div`,{children:e.body}),e.delivery===`sent`?null:(0,w.jsx)(`div`,{className:`clivly-chat-widget__status`,children:ie[e.delivery]})]})}function _e({greeting:e,hasSession:t,messages:n}){return t?n.length===0?(0,w.jsx)(`p`,{className:`clivly-chat-widget__empty`,children:`No messages yet. Say hello to get the conversation started.`}):(0,w.jsx)(w.Fragment,{children:n.map(e=>(0,w.jsx)(ge,{message:e},e.id))}):(0,w.jsx)(`p`,{className:`clivly-chat-widget__empty`,children:e})}function ve(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(n&&n.delivery!==`failed`)return n.senderType===`visitor`?new Date(n.createdAt).getTime():null}return null}function ye(e){let[t,n]=(0,p.useState)(!1);return(0,p.useEffect)(()=>{let t=ve(e);if(t===null){n(!1);return}let r=t+12e4-Date.now();if(r<=0){n(!0);return}n(!1);let i=setTimeout(()=>{n(!0)},r);return()=>{clearTimeout(i)}},[e]),t}function be(e){let t=e?.trim();return t?`We've notified the team — we'll reply here or by email at ${t}.`:`We've notified the team — we'll reply here as soon as we can.`}function xe(e){return!!(e?.name?.trim()&&e?.email?.trim())}function k(e,t){return e?t?`Conversation ready`:`Connecting…`:`Chat is unavailable`}function Se({className:e,createTransport:t,defaultVisitor:n,getSession:r,initiallyOpen:i=!1,launcherLabel:a=`Chat with us`,storage:o=T,style:s,theme:c=`auto`,widgetId:l}){let[u,d]=(0,p.useState)(i),[f,m]=(0,p.useState)(null),[h,g]=(0,p.useState)(!1),[_,v]=(0,p.useState)(!1),[y,b]=(0,p.useState)(``),[x,ee]=(0,p.useState)([]),[S,C]=(0,p.useState)(null),[te,ne]=(0,p.useState)({email:n?.email??``,name:n?.name??``}),re=(0,p.useRef)(null),ie=(0,p.useRef)(null);(0,p.useEffect)(()=>{t||console.warn(`[clivly] <ChatWidget> was given no createTransport, so messages cannot be sent. Pass createTransport={createClivlyTransport()} — see https://www.npmjs.com/package/@clivly/chat-widget#five-minute-quick-start`)},[]);let ce=ye(x),le=pe(S,s),ue=me(c);(0,p.useEffect)(()=>()=>{re.current?.close(),re.current=null},[]),(0,p.useEffect)(()=>{ie.current&&(ie.current.scrollTop=ie.current.scrollHeight)},[x]),(0,p.useEffect)(()=>{if(re.current?.close(),re.current=null,v(!1),!(S&&t))return;let e=t({handlers:{onClose:()=>{v(!1)},onError:e=>{m(e.message)},onEvent:e=>{he(e,{setError:m,setIsReady:v,setMessages:ee})},onOpen:()=>{v(!0)}},session:S});return re.current=e,()=>{e.close(),re.current===e&&(re.current=null)}},[t,S]),(0,p.useEffect)(()=>{if(!(u&&_))return;let e=()=>{document.visibilityState===`visible`&&re.current?.markRead?.()};e();let t=window.setInterval(e,15e3);return document.addEventListener(`visibilitychange`,e),()=>{window.clearInterval(t),document.removeEventListener(`visibilitychange`,e)}},[u,_,x]);let ge=(0,p.useCallback)(async e=>{m(null),g(!0);try{let t=await r({visitor:e,visitorToken:o?.getItem(se(l))??null,widgetId:l});o?.setItem(se(l),t.visitorToken),C(t),ee(fe(t.messages.map(e=>E(e))))}catch(e){let t=e instanceof Error?e:Error(`Failed to start the conversation.`);m(t.message)}finally{g(!1)}},[r,o,l]),ve=async e=>{e.preventDefault(),await ge(te)};(0,p.useEffect)(()=>{u&&xe(n)&&(S||h||f||ge({email:n.email.trim(),name:n.name.trim()}).catch(()=>{}))},[n,f,h,u,S,ge]);let Se=e=>{if(e?.preventDefault(),!(S&&re.current)){m(`Chat is not connected yet.`);return}let t=y.trim();if(!t)return;let n=O(),r={author:ae(S),body:t,createdAt:new Date().toISOString(),id:n,senderType:`visitor`};ee(e=>fe([...e,E(r,`pending`)])),b(``),m(null);try{re.current.sendMessage({body:t,id:n,type:`message.create`})}catch(e){let t=e instanceof Error?e:Error(`Failed to send your message.`);ee(e=>de(e,n)),m(t.message)}},Ce=!!t&&_,we=S?.widget.config.greeting??`Tell us a little about yourself and we’ll pick up the conversation from there.`,Te=S?.widget.name??`Start a conversation`,A=oe(S,we);return(0,w.jsxs)(`div`,{className:`clivly-chat-widget ${e??``}`.trim(),"data-clivly-theme":ue,style:le,children:[(0,w.jsx)(`style`,{children:`
348
348
  .clivly-chat-widget, .clivly-chat-widget * { box-sizing: border-box; }
349
349
  .clivly-chat-widget {
350
350
  --clivly-chat-accent: #185adb;
@@ -591,10 +591,18 @@ React keys must be passed directly to JSX without using spread:
591
591
  font-size: 0.875rem;
592
592
  padding: 0.75rem;
593
593
  }
594
+ .clivly-chat-widget__notice {
595
+ background: var(--clivly-chat-operator);
596
+ border-radius: 0.8rem;
597
+ color: var(--clivly-chat-muted);
598
+ font-size: 0.8125rem;
599
+ margin: 0;
600
+ padding: 0.625rem 0.75rem;
601
+ }
594
602
  .clivly-chat-widget__empty {
595
603
  color: var(--clivly-chat-muted);
596
604
  font-size: 0.875rem;
597
605
  margin: auto 0;
598
606
  text-align: center;
599
607
  }
600
- `}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__launcher`,onClick:()=>{d(e=>!e)},type:`button`,children:(0,w.jsx)(`span`,{children:a})}),u?(0,w.jsxs)(`section`,{"aria-label":Se,className:`clivly-chat-widget__panel`,children:[(0,w.jsxs)(`header`,{className:`clivly-chat-widget__header`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`h2`,{className:`clivly-chat-widget__title`,children:Se}),(0,w.jsx)(`p`,{className:`clivly-chat-widget__subtitle`,children:Ce})]}),(0,w.jsx)(`button`,{"aria-label":`Close chat`,className:`clivly-chat-widget__close`,onClick:()=>{d(!1)},type:`button`,children:`Close`})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__body`,ref:ie,children:[f?(0,w.jsx)(`div`,{className:`clivly-chat-widget__error`,children:f}):null,(0,w.jsx)(_e,{greeting:k,hasSession:!!S,messages:x})]}),S?(0,w.jsxs)(`form`,{className:`clivly-chat-widget__composer`,onSubmit:be,children:[(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Message`}),(0,w.jsx)(`textarea`,{className:`clivly-chat-widget__textarea`,onChange:e=>{b(e.target.value)},onKeyDown:e=>{D(e)&&(e.preventDefault(),y.trim()&&xe&&be())},placeholder:`Write your message`,value:y})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__actions`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__subtitle`,children:ye(!!t,_)}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__button`,disabled:!(y.trim()&&xe),type:`submit`,children:`Send`})]})]}):(0,w.jsxs)(`form`,{className:`clivly-chat-widget__form`,onSubmit:ge,children:[(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Name`}),(0,w.jsx)(`input`,{autoComplete:`name`,className:`clivly-chat-widget__input`,onChange:e=>{ne(t=>({...t,name:e.target.value}))},required:!0,value:te.name})]}),(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Email`}),(0,w.jsx)(`input`,{autoComplete:`email`,className:`clivly-chat-widget__input`,onChange:e=>{ne(t=>({...t,email:e.target.value}))},required:!0,type:`email`,value:te.email})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__actions`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__subtitle`,children:`We use this to start the conversation.`}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__button`,disabled:h||!te.name.trim()||!te.email.trim(),type:`submit`,children:h?`Starting…`:`Start chat`})]})]}),(0,w.jsx)(`p`,{className:`clivly-chat-widget__powered`,children:(0,w.jsxs)(`a`,{href:`https://clivly.com`,rel:`noopener`,target:`_blank`,children:[`Powered by`,` `,(0,w.jsx)(`span`,{className:`clivly-chat-widget__powered-mark`,children:`Clivly`})]})})]}):null]})}function xe(e){return typeof e!=`object`||!e?!1:typeof e.type==`string`}function k({createSocket:e=(e,t)=>new WebSocket(e,t),getProtocols:t,resolveUrl:n}){return({handlers:r,session:i})=>{let a=e(n(i),t?.(i));return a.addEventListener(`open`,()=>{r.onOpen?.()}),a.addEventListener(`close`,()=>{r.onClose()}),a.addEventListener(`error`,()=>{r.onError(Error(`Chat socket error.`))}),a.addEventListener(`message`,e=>{try{let t=JSON.parse(String(e.data));if(!xe(t))throw Error(`Chat socket event had an invalid shape.`);r.onEvent(t)}catch(e){let t=e instanceof Error?e:Error(`Chat socket event could not be parsed.`);r.onError(t)}}),{close:()=>{a.close()},sendMessage:e=>{if(a.readyState!==WebSocket.OPEN)throw Error(`Chat socket is not connected.`);a.send(JSON.stringify(e))}}}}function Se(e={}){return k({...e,resolveUrl:e=>{if(!e.socketUrl)throw Error(`Chat session response is missing socketUrl. The Clivly server is older than this widget.`);return e.socketUrl}})}function Ce(e){return typeof e==`object`&&!!e}function we({fetchImpl:e=fetch,sessionUrl:t}){return async({visitor:n,visitorToken:r,widgetId:i})=>{let a=await e(t,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({visitor:n,visitorToken:r,widgetId:i})});if(!a.ok)throw Error(`Chat session request failed with ${a.status}.`);let o=await a.json();if(!Ce(o))throw Error(`Chat session response was not an object.`);return`session`in o?o.session:o}}let Te=`clivlyMounted`;function A(e){let t=e.visitorEmail?.trim(),n=e.visitorName?.trim();if(t||n)return{...t?{email:t}:{},...n?{name:n}:{}}}function Ee(e,t,n){return e.replaceAll(`{conversationId}`,encodeURIComponent(t)).replaceAll(`{token}`,encodeURIComponent(n))}function De({container:e,createTransport:t,sessionUrl:n,socketUrlTemplate:r,theme:i=`auto`,widgetId:a,...o}){let s=t??(r?k({resolveUrl:e=>e.socketUrl??Ee(r,e.conversation.id,e.token)}):Se()),c=we({sessionUrl:n}),l=(0,m.createRoot)(e),u=e=>{l.render((0,w.jsx)(be,{...o,createTransport:s,getSession:c,theme:e,widgetId:a}))};return u(i),{root:l,setTheme:u}}function Oe(e){e.dataset[Te]=`true`}function ke(e){return e.dataset[Te]===`true`}function Ae(e=document.currentScript){if(ke(e))throw Error(`Widget script has already been mounted.`);let t=e.dataset.widgetId,n=e.dataset.sessionUrl;if(!t)throw Error(`Widget script is missing data-widget-id.`);if(!n)throw Error(`Widget script is missing data-session-url.`);let r=document.createElement(`div`);e.insertAdjacentElement(`afterend`,r);let i=r.attachShadow({mode:`open`}),a=document.createElement(`div`);i.append(a);let{root:o,setTheme:s}=De({container:a,defaultVisitor:A(e.dataset),sessionUrl:n,socketUrlTemplate:e.dataset.socketUrlTemplate,theme:te(e.dataset.theme),widgetId:t});return Oe(e),{root:o,setTheme:s,shadowRoot:i}}function je(e=document.currentScript){return!e||ke(e)?null:Ae(e)}je()})();
608
+ `}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__launcher`,onClick:()=>{d(e=>!e)},type:`button`,children:(0,w.jsx)(`span`,{children:a})}),u?(0,w.jsxs)(`section`,{"aria-label":Te,className:`clivly-chat-widget__panel`,children:[(0,w.jsxs)(`header`,{className:`clivly-chat-widget__header`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`h2`,{className:`clivly-chat-widget__title`,children:Te}),(0,w.jsx)(`p`,{className:`clivly-chat-widget__subtitle`,children:A})]}),(0,w.jsx)(`button`,{"aria-label":`Close chat`,className:`clivly-chat-widget__close`,onClick:()=>{d(!1)},type:`button`,children:`Close`})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__body`,ref:ie,children:[f?(0,w.jsx)(`div`,{className:`clivly-chat-widget__error`,children:f}):null,(0,w.jsx)(_e,{greeting:we,hasSession:!!S,messages:x}),S&&ce?(0,w.jsx)(`p`,{className:`clivly-chat-widget__notice`,role:`status`,children:be(S.contact?.email)}):null]}),S?(0,w.jsxs)(`form`,{className:`clivly-chat-widget__composer`,onSubmit:Se,children:[(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Message`}),(0,w.jsx)(`textarea`,{className:`clivly-chat-widget__textarea`,onChange:e=>{b(e.target.value)},onKeyDown:e=>{D(e)&&(e.preventDefault(),y.trim()&&Ce&&Se())},placeholder:`Write your message`,value:y})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__actions`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__subtitle`,children:k(!!t,_)}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__button`,disabled:!(y.trim()&&Ce),type:`submit`,children:`Send`})]})]}):(0,w.jsxs)(`form`,{className:`clivly-chat-widget__form`,onSubmit:ve,children:[(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Name`}),(0,w.jsx)(`input`,{autoComplete:`name`,className:`clivly-chat-widget__input`,onChange:e=>{ne(t=>({...t,name:e.target.value}))},required:!0,value:te.name})]}),(0,w.jsxs)(`label`,{className:`clivly-chat-widget__field`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__label`,children:`Email`}),(0,w.jsx)(`input`,{autoComplete:`email`,className:`clivly-chat-widget__input`,onChange:e=>{ne(t=>({...t,email:e.target.value}))},required:!0,type:`email`,value:te.email})]}),(0,w.jsxs)(`div`,{className:`clivly-chat-widget__actions`,children:[(0,w.jsx)(`span`,{className:`clivly-chat-widget__subtitle`,children:`We use this to start the conversation.`}),(0,w.jsx)(`button`,{className:`clivly-chat-widget__button`,disabled:h||!te.name.trim()||!te.email.trim(),type:`submit`,children:h?`Starting…`:`Start chat`})]})]}),(0,w.jsx)(`p`,{className:`clivly-chat-widget__powered`,children:(0,w.jsxs)(`a`,{href:`https://clivly.com`,rel:`noopener`,target:`_blank`,children:[`Powered by`,` `,(0,w.jsx)(`span`,{className:`clivly-chat-widget__powered-mark`,children:`Clivly`})]})})]}):null]})}function Ce(e){return typeof e!=`object`||!e?!1:typeof e.type==`string`}function we({createSocket:e=(e,t)=>new WebSocket(e,t),getProtocols:t,resolveUrl:n}){return({handlers:r,session:i})=>{let a=e(n(i),t?.(i));return a.addEventListener(`open`,()=>{r.onOpen?.()}),a.addEventListener(`close`,()=>{r.onClose()}),a.addEventListener(`error`,()=>{r.onError(Error(`Chat socket error.`))}),a.addEventListener(`message`,e=>{try{let t=JSON.parse(String(e.data));if(!Ce(t))throw Error(`Chat socket event had an invalid shape.`);r.onEvent(t)}catch(e){let t=e instanceof Error?e:Error(`Chat socket event could not be parsed.`);r.onError(t)}}),{close:()=>{a.close()},markRead:()=>{a.readyState===WebSocket.OPEN&&a.send(JSON.stringify({type:`conversation.read`}))},sendMessage:e=>{if(a.readyState!==WebSocket.OPEN)throw Error(`Chat socket is not connected.`);a.send(JSON.stringify(e))}}}}function Te(e={}){return we({...e,resolveUrl:e=>{if(!e.socketUrl)throw Error(`Chat session response is missing socketUrl. The Clivly server is older than this widget.`);return e.socketUrl}})}function A(e){return typeof e==`object`&&!!e}function Ee({fetchImpl:e=fetch,sessionUrl:t}){return async({visitor:n,visitorToken:r,widgetId:i})=>{let a=await e(t,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({visitor:n,visitorToken:r,widgetId:i})});if(!a.ok)throw Error(`Chat session request failed with ${a.status}.`);let o=await a.json();if(!A(o))throw Error(`Chat session response was not an object.`);return`session`in o?o.session:o}}let De=`clivlyMounted`;function Oe(e){let t=e.visitorEmail?.trim(),n=e.visitorName?.trim();if(t||n)return{...t?{email:t}:{},...n?{name:n}:{}}}function ke(e,t,n){return e.replaceAll(`{conversationId}`,encodeURIComponent(t)).replaceAll(`{token}`,encodeURIComponent(n))}function Ae({container:e,createTransport:t,sessionUrl:n,socketUrlTemplate:r,theme:i=`auto`,widgetId:a,...o}){let s=t??(r?we({resolveUrl:e=>e.socketUrl??ke(r,e.conversation.id,e.token)}):Te()),c=Ee({sessionUrl:n}),l=(0,m.createRoot)(e),u=e=>{l.render((0,w.jsx)(Se,{...o,createTransport:s,getSession:c,theme:e,widgetId:a}))};return u(i),{root:l,setTheme:u}}function je(e){e.dataset[De]=`true`}function Me(e){return e.dataset[De]===`true`}function Ne(e=document.currentScript){if(Me(e))throw Error(`Widget script has already been mounted.`);let t=e.dataset.widgetId,n=e.dataset.sessionUrl;if(!t)throw Error(`Widget script is missing data-widget-id.`);if(!n)throw Error(`Widget script is missing data-session-url.`);let r=document.createElement(`div`);e.insertAdjacentElement(`afterend`,r);let i=r.attachShadow({mode:`open`}),a=document.createElement(`div`);i.append(a);let{root:o,setTheme:s}=Ae({container:a,defaultVisitor:Oe(e.dataset),sessionUrl:n,socketUrlTemplate:e.dataset.socketUrlTemplate,theme:te(e.dataset.theme),widgetId:t});return je(e),{root:o,setTheme:s,shadowRoot:i}}function Pe(e=document.currentScript){return!e||Me(e)?null:Ne(e)}Pe()})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/chat-widget",
3
- "version": "0.12.0",
3
+ "version": "0.14.1",
4
4
  "private": false,
5
5
  "description": "Clivly chat widget — embeddable React and script-tag entrypoints for Clivly conversations.",
6
6
  "license": "MIT",