@adatechnology/conversations-ui 0.8.0 → 0.9.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.
Files changed (49) hide show
  1. package/dist/{chunk-HMCOTZAX.js → chunk-NSOAUYNE.js} +33 -28
  2. package/dist/chunk-ZSOD5AQT.js +19 -0
  3. package/dist/flows/index.js +4 -2
  4. package/dist/index.js +5 -3
  5. package/dist/participant/index.d.ts +3 -0
  6. package/dist/participant/index.js +536 -276
  7. package/dist/styles.css +99 -23
  8. package/package.json +2 -1
  9. package/src/Tooltip.tsx +5 -12
  10. package/src/TooltipDom.test.tsx +78 -0
  11. package/src/domHarness.test-helper.ts +53 -0
  12. package/src/participant/ParticipantBackButton.tsx +1 -1
  13. package/src/participant/ParticipantBubbleBody.tsx +54 -0
  14. package/src/participant/ParticipantBubbleStatus.tsx +22 -28
  15. package/src/participant/ParticipantChannelBadges.tsx +10 -2
  16. package/src/participant/ParticipantComposer.tsx +1 -1
  17. package/src/participant/ParticipantComposerControls.tsx +2 -0
  18. package/src/participant/ParticipantCopyableToken.tsx +1 -0
  19. package/src/participant/ParticipantFailedMenu.tsx +40 -0
  20. package/src/participant/ParticipantFailedMenuList.tsx +41 -0
  21. package/src/participant/ParticipantMessageBubble.test.tsx +6 -6
  22. package/src/participant/ParticipantMessageBubble.tsx +20 -52
  23. package/src/participant/ParticipantMessageText.test.tsx +1 -1
  24. package/src/participant/ParticipantMessageText.tsx +4 -2
  25. package/src/participant/ParticipantProtocolBadge.tsx +1 -0
  26. package/src/participant/ParticipantScrollToLatest.tsx +1 -1
  27. package/src/participant/ParticipantThread.tsx +23 -12
  28. package/src/participant/ParticipantThreadHeader.test.tsx +1 -1
  29. package/src/participant/failedMenuKeyboard.ts +29 -0
  30. package/src/participant/participantFailedMenuDom.test.tsx +154 -0
  31. package/src/participant/participantFailedMenuState.test.ts +61 -0
  32. package/src/participant/participantFailedMenuState.ts +57 -0
  33. package/src/participant/participantFailedMessage.test.tsx +83 -0
  34. package/src/participant/participantLabels.ts +6 -0
  35. package/src/participant/participantThemeContrast.test.ts +8 -0
  36. package/src/participant/participantTooltips.test.tsx +74 -0
  37. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -1
  38. package/src/participant/test-fixtures/thread-closed.golden.html +1 -1
  39. package/src/participant/test-fixtures/thread-default.golden.html +1 -1
  40. package/src/participant/test-fixtures/thread-error.golden.html +1 -1
  41. package/src/participant/test-fixtures/thread-loading.golden.html +1 -1
  42. package/src/participant/test-fixtures/thread-plain.golden.html +1 -1
  43. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -1
  44. package/src/participant/useFailedMenuEffects.ts +59 -0
  45. package/src/participant/useParticipantFailedMenu.ts +60 -0
  46. package/src/participant/useSuppressTouchTooltips.ts +38 -0
  47. package/src/styles.css +19 -5
  48. package/src/tooltipLayerRegistry.test.ts +56 -0
  49. package/src/tooltipLayerRegistry.ts +34 -0
@@ -1,3 +1,6 @@
1
+ import {
2
+ TooltipLayer
3
+ } from "../chunk-NSOAUYNE.js";
1
4
  import {
2
5
  StatusTicks,
3
6
  channelCapabilityFor,
@@ -7,7 +10,7 @@ import {
7
10
  } from "../chunk-O6457XHK.js";
8
11
 
9
12
  // src/participant/ParticipantConversationsScreen.tsx
10
- import { useMemo as useMemo5, useReducer as useReducer5, useRef as useRef10, useState as useState8 } from "react";
13
+ import { useMemo as useMemo5, useReducer as useReducer6, useRef as useRef12, useState as useState9 } from "react";
11
14
 
12
15
  // src/participant/participantLoadView.ts
13
16
  function resolveLoadView({ status, hasItems }) {
@@ -19,6 +22,7 @@ function resolveLoadView({ status, hasItems }) {
19
22
  }
20
23
 
21
24
  // src/participant/participantLabels.ts
25
+ var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
22
26
  var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
23
27
  inboxTitle: "Conversations",
24
28
  sectionAwaiting: "Waiting for your reply",
@@ -55,6 +59,7 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
55
59
  retry: "Retry",
56
60
  discard: "Discard",
57
61
  edit: "Edit",
62
+ messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
58
63
  notFound: "Conversation not found",
59
64
  loading: "Loading\u2026",
60
65
  loadError: "Could not load. Check your connection and try again.",
@@ -128,18 +133,34 @@ function ParticipantChannelBadges({
128
133
  if (variant === "inline") {
129
134
  return /* @__PURE__ */ jsxs("span", { className: "cv-p-channels", children: [
130
135
  /* @__PURE__ */ jsx("span", { className: "cv-p-sr-only", children: `${labels.channelsGroup}: ` }),
131
- descriptors.map((descriptor, index) => /* @__PURE__ */ jsx("span", { className: `cv-p-channel cv-p-channel--${descriptor.channel}`, children: /* @__PURE__ */ jsx(
132
- BadgeContent,
136
+ descriptors.map((descriptor, index) => /* @__PURE__ */ jsx(
137
+ "span",
133
138
  {
134
- descriptor,
135
- labels,
136
- separator: index < descriptors.length - 1 ? ", " : "",
137
- iconSize
138
- }
139
- ) }, descriptor.channel))
139
+ className: `cv-p-channel cv-p-channel--${descriptor.channel}`,
140
+ "data-cv-tooltip": resolveParticipantChannelLabel(descriptor, labels),
141
+ children: /* @__PURE__ */ jsx(
142
+ BadgeContent,
143
+ {
144
+ descriptor,
145
+ labels,
146
+ separator: index < descriptors.length - 1 ? ", " : "",
147
+ iconSize
148
+ }
149
+ )
150
+ },
151
+ descriptor.channel
152
+ ))
140
153
  ] });
141
154
  }
142
- return /* @__PURE__ */ jsx("ul", { className: "cv-p-channels", "aria-label": labels.channelsGroup, children: descriptors.map((descriptor) => /* @__PURE__ */ jsx("li", { className: `cv-p-channel cv-p-channel--${descriptor.channel}`, children: /* @__PURE__ */ jsx(BadgeContent, { descriptor, labels, separator: "", iconSize }) }, descriptor.channel)) });
155
+ return /* @__PURE__ */ jsx("ul", { className: "cv-p-channels", "aria-label": labels.channelsGroup, children: descriptors.map((descriptor) => /* @__PURE__ */ jsx(
156
+ "li",
157
+ {
158
+ className: `cv-p-channel cv-p-channel--${descriptor.channel}`,
159
+ "data-cv-tooltip": resolveParticipantChannelLabel(descriptor, labels),
160
+ children: /* @__PURE__ */ jsx(BadgeContent, { descriptor, labels, separator: "", iconSize })
161
+ },
162
+ descriptor.channel
163
+ )) });
143
164
  }
144
165
 
145
166
  // src/participant/participantSubjectIcon.ts
@@ -361,15 +382,18 @@ function ParticipantInbox(props) {
361
382
  }
362
383
 
363
384
  // src/participant/ParticipantThreadScreen.tsx
364
- import { useEffect as useEffect11, useState as useState7 } from "react";
385
+ import { useEffect as useEffect13, useState as useState8 } from "react";
365
386
 
366
387
  // src/participant/ParticipantBackButton.tsx
367
388
  import { ArrowLeft } from "lucide-react";
368
389
  import { jsx as jsx6 } from "react/jsx-runtime";
369
390
  function ParticipantBackButton({ label, onBack }) {
370
- return /* @__PURE__ */ jsx6("button", { type: "button", className: "cv-p-thread__back", "aria-label": label, onClick: onBack, children: /* @__PURE__ */ jsx6(ArrowLeft, { size: 24, "aria-hidden": "true", focusable: "false" }) });
391
+ return /* @__PURE__ */ jsx6("button", { type: "button", className: "cv-p-thread__back", "aria-label": label, "data-cv-tooltip": label, onClick: onBack, children: /* @__PURE__ */ jsx6(ArrowLeft, { size: 24, "aria-hidden": "true", focusable: "false" }) });
371
392
  }
372
393
 
394
+ // src/participant/ParticipantThread.tsx
395
+ import { useRef as useRef5 } from "react";
396
+
373
397
  // src/participant/ParticipantComposer.tsx
374
398
  import { useState } from "react";
375
399
 
@@ -406,6 +430,7 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
406
430
  type: "button",
407
431
  className: "cv-p-composer__attach",
408
432
  "aria-label": labels.attach,
433
+ "data-cv-tooltip": labels.attach,
409
434
  disabled,
410
435
  onClick: () => fileInputRef.current?.click(),
411
436
  children: /* @__PURE__ */ jsx7(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
@@ -420,6 +445,7 @@ function SendButton({ label, disabled }) {
420
445
  type: "submit",
421
446
  className: "cv-p-button cv-p-button--primary cv-p-composer__send",
422
447
  "aria-label": label,
448
+ "data-cv-tooltip": label,
423
449
  disabled,
424
450
  children: /* @__PURE__ */ jsx7(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
425
451
  }
@@ -488,7 +514,7 @@ function FileList({ files, labels, onRemove }) {
488
514
  return /* @__PURE__ */ jsx8("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs7("li", { className: "cv-p-files__item", children: [
489
515
  /* @__PURE__ */ jsx8("span", { className: "cv-p-files__name", children: file.name }),
490
516
  /* @__PURE__ */ jsx8("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
491
- /* @__PURE__ */ jsxs7("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, onClick: () => onRemove(file), children: [
517
+ /* @__PURE__ */ jsxs7("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, "data-cv-tooltip": labels.removeAttachment, onClick: () => onRemove(file), children: [
492
518
  /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\xD7" }),
493
519
  /* @__PURE__ */ jsx8("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
494
520
  ] })
@@ -544,8 +570,40 @@ function ParticipantComposer(props) {
544
570
  ] });
545
571
  }
546
572
 
573
+ // src/participant/useSuppressTouchTooltips.ts
574
+ import { useEffect as useEffect2 } from "react";
575
+ function useSuppressTouchTooltips(rootRef) {
576
+ useEffect2(() => {
577
+ const root = rootRef.current;
578
+ if (!root) return void 0;
579
+ let isTouching = false;
580
+ function handlePointerDown(event) {
581
+ isTouching = event.pointerType === "touch";
582
+ }
583
+ function handlePointerOver(event) {
584
+ if (event.pointerType === "touch") event.stopPropagation();
585
+ }
586
+ function handleFocusIn(event) {
587
+ if (isTouching) event.stopPropagation();
588
+ }
589
+ function handleKeyDown() {
590
+ isTouching = false;
591
+ }
592
+ root.addEventListener("pointerdown", handlePointerDown, true);
593
+ root.addEventListener("pointerover", handlePointerOver);
594
+ root.addEventListener("focusin", handleFocusIn);
595
+ root.addEventListener("keydown", handleKeyDown, true);
596
+ return () => {
597
+ root.removeEventListener("pointerdown", handlePointerDown, true);
598
+ root.removeEventListener("pointerover", handlePointerOver);
599
+ root.removeEventListener("focusin", handleFocusIn);
600
+ root.removeEventListener("keydown", handleKeyDown, true);
601
+ };
602
+ }, [rootRef]);
603
+ }
604
+
547
605
  // src/participant/ParticipantCopyAnnouncer.tsx
548
- import { createContext, useCallback, useContext, useEffect as useEffect2, useReducer, useRef as useRef3 } from "react";
606
+ import { createContext, useCallback, useContext, useEffect as useEffect3, useReducer, useRef as useRef3 } from "react";
549
607
 
550
608
  // src/participant/participantCopyAnnouncement.ts
551
609
  var INITIAL_COPY_ANNOUNCEMENT = { text: "", tick: 0 };
@@ -575,7 +633,7 @@ function useAnnounceCopy() {
575
633
  function ParticipantCopyAnnouncer({ children }) {
576
634
  const [announcement, dispatch] = useReducer(copyAnnouncementReducer, INITIAL_COPY_ANNOUNCEMENT);
577
635
  const announce = useCallback((text) => dispatch({ type: "announce", text }), []);
578
- useEffect2(() => {
636
+ useEffect3(() => {
579
637
  if (announcement.text === "") return void 0;
580
638
  const timer = setTimeout(() => dispatch({ type: "expired" }), ANNOUNCEMENT_MS);
581
639
  return () => clearTimeout(timer);
@@ -616,6 +674,7 @@ function ParticipantScrollToLatest({
616
674
  type: "button",
617
675
  className: "cv-p-thread__latest-button",
618
676
  "aria-label": label,
677
+ "data-cv-tooltip": label,
619
678
  onClick: (event) => {
620
679
  focusScrollerOf(event.currentTarget);
621
680
  onClick();
@@ -692,8 +751,74 @@ function ParticipantDayDivider({ day, locale }) {
692
751
  return /* @__PURE__ */ jsx12("div", { className: "cv-p-day", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx12("time", { className: "cv-p-day__label", dateTime: toDateTimeAttribute(day), children: label }) });
693
752
  }
694
753
 
695
- // src/participant/ParticipantBubbleStatus.tsx
754
+ // src/participant/ParticipantMessageBubble.tsx
755
+ import { useId as useId3 } from "react";
756
+
757
+ // src/participant/ParticipantAttachmentItem.tsx
758
+ import { useEffect as useEffect4, useState as useState2 } from "react";
759
+
760
+ // src/participant/participantAttachmentUrl.ts
761
+ var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
762
+ function isSafeAttachmentUrl(url) {
763
+ try {
764
+ return SAFE_ATTACHMENT_PROTOCOLS.has(new URL(url.trim()).protocol);
765
+ } catch {
766
+ return false;
767
+ }
768
+ }
769
+
770
+ // src/participant/ParticipantAttachmentItem.tsx
696
771
  import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
772
+ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
773
+ const [url, setUrl] = useState2(void 0);
774
+ const disposition = attachment.kind === "document" ? "attachment" : "inline";
775
+ useEffect4(() => {
776
+ let isCurrent = true;
777
+ resolveAttachmentUrl(attachment, disposition).then(
778
+ (resolved) => {
779
+ if (isCurrent) setUrl(isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0);
780
+ },
781
+ () => {
782
+ if (isCurrent) setUrl(void 0);
783
+ }
784
+ );
785
+ return () => {
786
+ isCurrent = false;
787
+ };
788
+ }, [attachment, disposition, resolveAttachmentUrl]);
789
+ return url;
790
+ }
791
+ function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
792
+ const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
793
+ if (attachment.kind === "image") {
794
+ return /* @__PURE__ */ jsxs9("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
795
+ url ? /* @__PURE__ */ jsx13("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
796
+ /* @__PURE__ */ jsx13("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
797
+ ] });
798
+ }
799
+ if (attachment.kind === "audio") {
800
+ return /* @__PURE__ */ jsxs9("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
801
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
802
+ /* @__PURE__ */ jsx13("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
803
+ ] });
804
+ }
805
+ return /* @__PURE__ */ jsxs9(
806
+ "a",
807
+ {
808
+ className: "cv-p-attachment cv-p-attachment--document",
809
+ href: url,
810
+ download: attachment.filename,
811
+ "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
812
+ children: [
813
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
814
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
815
+ ]
816
+ }
817
+ );
818
+ }
819
+
820
+ // src/participant/ParticipantBubbleStatus.tsx
821
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
697
822
  function statusText(status, labels) {
698
823
  const byStatus = {
699
824
  sending: labels.statusSending,
@@ -708,23 +833,204 @@ function statusText(status, labels) {
708
833
  function OwnStatus({ status, labels, onRetry }) {
709
834
  const ticksStatus = status === "sending" ? "queued" : status;
710
835
  const isVisibleText = status === "sending" || status === "queued" || status === "failed";
711
- if (status === "failed" && onRetry) {
712
- return /* @__PURE__ */ jsxs9("button", { type: "button", className: "cv-p-bubble__retry", onClick: onRetry, children: [
713
- /* @__PURE__ */ jsx13(StatusTicks, { status: "failed", appearance: "stylesheet" }),
714
- /* @__PURE__ */ jsx13("span", { children: labels.statusFailed })
715
- ] });
716
- }
717
836
  const text = status === "failed" ? labels.statusFailedShort : statusText(status, labels);
718
- return /* @__PURE__ */ jsxs9("span", { className: "cv-p-bubble__status", children: [
719
- /* @__PURE__ */ jsx13(StatusTicks, { status: ticksStatus, appearance: "stylesheet" }),
720
- /* @__PURE__ */ jsx13("span", { className: isVisibleText ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
837
+ const isTextVisible = isVisibleText && !(status === "failed" && onRetry);
838
+ return /* @__PURE__ */ jsxs10("span", { className: "cv-p-bubble__status", children: [
839
+ /* @__PURE__ */ jsx14(StatusTicks, { status: ticksStatus, appearance: "stylesheet", title: isTextVisible ? void 0 : text }),
840
+ /* @__PURE__ */ jsx14("span", { className: isTextVisible ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
721
841
  ] });
722
842
  }
723
- function FailedActions({ labels, onDiscard, onEdit }) {
724
- if (!onDiscard && !onEdit) return null;
725
- return /* @__PURE__ */ jsxs9("span", { className: "cv-p-bubble__actions", children: [
726
- onEdit ? /* @__PURE__ */ jsx13("button", { type: "button", className: "cv-p-bubble__action", onClick: onEdit, children: labels.edit }) : null,
727
- onDiscard ? /* @__PURE__ */ jsx13("button", { type: "button", className: "cv-p-bubble__action", onClick: onDiscard, children: labels.discard }) : null
843
+ function RetryButton({ labels, onRetry, describedBy }) {
844
+ return /* @__PURE__ */ jsx14(
845
+ "button",
846
+ {
847
+ type: "button",
848
+ className: "cv-p-failed-retry",
849
+ "aria-label": labels.statusFailed,
850
+ "aria-describedby": describedBy,
851
+ "data-cv-tooltip": labels.statusFailed,
852
+ onClick: onRetry,
853
+ children: /* @__PURE__ */ jsx14("span", { className: "cv-p-failed-retry__icon", children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx14("path", { d: "M13 8a5 5 0 1 1-1.6-3.7M13 2.5v3h-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) }) })
854
+ }
855
+ );
856
+ }
857
+
858
+ // src/participant/ParticipantFailedMenu.tsx
859
+ import { useId as useId2 } from "react";
860
+
861
+ // src/participant/ParticipantFailedMenuList.tsx
862
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
863
+ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard }) {
864
+ function runAndClose(action) {
865
+ return () => {
866
+ menu.close();
867
+ action();
868
+ };
869
+ }
870
+ const className = `cv-p-failed-menu__list${menu.placement === "below" ? " cv-p-failed-menu__list--below" : ""}`;
871
+ return /* @__PURE__ */ jsxs11("div", { role: "menu", "aria-labelledby": labelledBy, ref: menu.menuRef, className, onKeyDown: menu.handleMenuKeyDown, children: [
872
+ onEdit ? /* @__PURE__ */ jsx15("button", { type: "button", role: "menuitem", tabIndex: -1, className: "cv-p-failed-menu__item", onClick: runAndClose(onEdit), children: labels.edit }) : null,
873
+ onDiscard ? /* @__PURE__ */ jsx15(
874
+ "button",
875
+ {
876
+ type: "button",
877
+ role: "menuitem",
878
+ tabIndex: -1,
879
+ className: "cv-p-failed-menu__item cv-p-failed-menu__item--danger",
880
+ onClick: runAndClose(onDiscard),
881
+ children: labels.discard
882
+ }
883
+ ) : null
884
+ ] });
885
+ }
886
+
887
+ // src/participant/useParticipantFailedMenu.ts
888
+ import { useReducer as useReducer2, useRef as useRef4 } from "react";
889
+
890
+ // src/participant/participantFailedMenuState.ts
891
+ var CLOSED_FAILED_MENU = { isOpen: false, initialFocus: "first" };
892
+ function failedMenuReducer(state, action) {
893
+ if (action.type === "open") return { isOpen: true, initialFocus: action.initialFocus };
894
+ if (action.type === "close") return state.isOpen ? CLOSED_FAILED_MENU : state;
895
+ return state.isOpen ? CLOSED_FAILED_MENU : { isOpen: true, initialFocus: "first" };
896
+ }
897
+ function resolveFailedMenuKey({ key, index, count }) {
898
+ if (key === "Escape") return { type: "close", shouldRestoreFocus: true };
899
+ if (key === "Tab") return { type: "close", shouldRestoreFocus: false };
900
+ if (key === "ArrowDown") return { type: "focus", index: (index + 1) % count };
901
+ if (key === "ArrowUp") return { type: "focus", index: (index - 1 + count) % count };
902
+ if (key === "Home") return { type: "focus", index: 0 };
903
+ if (key === "End") return { type: "focus", index: count - 1 };
904
+ return void 0;
905
+ }
906
+ function resolveFailedMenuTriggerKey(key) {
907
+ if (key === "ArrowDown") return "first";
908
+ if (key === "ArrowUp") return "last";
909
+ return void 0;
910
+ }
911
+ var MENU_GAP_PX = 8;
912
+ function resolveFailedMenuPlacement({ anchorTop, scrollerTop, menuHeight }) {
913
+ return anchorTop - scrollerTop >= menuHeight + MENU_GAP_PX ? "above" : "below";
914
+ }
915
+
916
+ // src/participant/failedMenuKeyboard.ts
917
+ function handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }) {
918
+ const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
919
+ const index = items.findIndex((item) => item === document.activeElement);
920
+ const result = resolveFailedMenuKey({ key: event.key, index, count: items.length });
921
+ if (!result) return;
922
+ if (result.type === "focus") {
923
+ event.preventDefault();
924
+ items[result.index]?.focus();
925
+ return;
926
+ }
927
+ if (result.shouldRestoreFocus) {
928
+ event.preventDefault();
929
+ event.stopPropagation();
930
+ triggerRef.current?.focus();
931
+ }
932
+ dispatch({ type: "close" });
933
+ }
934
+
935
+ // src/participant/useFailedMenuEffects.ts
936
+ import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect2, useState as useState3 } from "react";
937
+ var SCROLLER_SELECTOR2 = ".cv-p-thread__scroll";
938
+ function useFailedMenuInitialFocus(state, menuRef) {
939
+ useEffect5(() => {
940
+ if (!state.isOpen) return;
941
+ const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
942
+ const target = state.initialFocus === "last" ? items[items.length - 1] : items[0];
943
+ target?.focus();
944
+ }, [state, menuRef]);
945
+ }
946
+ function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
947
+ useEffect5(() => {
948
+ if (!isOpen) return;
949
+ function handlePointerDown(event) {
950
+ if (event.target instanceof Node && containerRef.current?.contains(event.target)) return;
951
+ dispatch({ type: "close" });
952
+ }
953
+ document.addEventListener("pointerdown", handlePointerDown);
954
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
955
+ }, [isOpen, containerRef, dispatch]);
956
+ }
957
+ function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
958
+ const [placement, setPlacement] = useState3("above");
959
+ useLayoutEffect2(() => {
960
+ const trigger = triggerRef.current;
961
+ const menu = menuRef.current;
962
+ if (!isOpen || !trigger || !menu) return;
963
+ const anchor = trigger.closest(".cv-p-bubble") ?? trigger;
964
+ const scroller = trigger.closest(SCROLLER_SELECTOR2);
965
+ setPlacement(
966
+ resolveFailedMenuPlacement({
967
+ anchorTop: anchor.getBoundingClientRect().top,
968
+ scrollerTop: scroller?.getBoundingClientRect().top ?? 0,
969
+ menuHeight: menu.getBoundingClientRect().height
970
+ })
971
+ );
972
+ }, [isOpen, triggerRef, menuRef]);
973
+ return placement;
974
+ }
975
+
976
+ // src/participant/useParticipantFailedMenu.ts
977
+ function useParticipantFailedMenu() {
978
+ const [state, dispatch] = useReducer2(failedMenuReducer, CLOSED_FAILED_MENU);
979
+ const containerRef = useRef4(null);
980
+ const triggerRef = useRef4(null);
981
+ const menuRef = useRef4(null);
982
+ useFailedMenuInitialFocus(state, menuRef);
983
+ useFailedMenuOutsideClose(state.isOpen, containerRef, dispatch);
984
+ const placement = useFailedMenuPlacement(state.isOpen, { triggerRef, menuRef });
985
+ function handleTriggerKeyDown(event) {
986
+ const initialFocus = resolveFailedMenuTriggerKey(event.key);
987
+ if (!initialFocus) return;
988
+ event.preventDefault();
989
+ dispatch({ type: "open", initialFocus });
990
+ }
991
+ function handleBlur(event) {
992
+ const next = event.relatedTarget;
993
+ if (next instanceof Node && !containerRef.current?.contains(next)) dispatch({ type: "close" });
994
+ }
995
+ return {
996
+ state,
997
+ placement,
998
+ containerRef,
999
+ triggerRef,
1000
+ menuRef,
1001
+ handleTriggerClick: () => dispatch({ type: "toggle" }),
1002
+ handleTriggerKeyDown,
1003
+ handleMenuKeyDown: (event) => handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }),
1004
+ handleBlur,
1005
+ close: () => dispatch({ type: "close" })
1006
+ };
1007
+ }
1008
+
1009
+ // src/participant/ParticipantFailedMenu.tsx
1010
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1011
+ function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
1012
+ const menu = useParticipantFailedMenu();
1013
+ const triggerId = useId2();
1014
+ if (!onEdit && !onDiscard) return null;
1015
+ const label = labels.messageOptions ?? DEFAULT_MESSAGE_OPTIONS_LABEL;
1016
+ return /* @__PURE__ */ jsxs12("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
1017
+ /* @__PURE__ */ jsx16(
1018
+ "button",
1019
+ {
1020
+ type: "button",
1021
+ id: triggerId,
1022
+ ref: menu.triggerRef,
1023
+ className: "cv-p-failed-menu__trigger",
1024
+ "aria-haspopup": "menu",
1025
+ "aria-expanded": menu.state.isOpen,
1026
+ "aria-label": label,
1027
+ "data-cv-tooltip": menu.state.isOpen ? void 0 : label,
1028
+ onClick: menu.handleTriggerClick,
1029
+ onKeyDown: menu.handleTriggerKeyDown,
1030
+ children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u22EF" })
1031
+ }
1032
+ ),
1033
+ menu.state.isOpen ? /* @__PURE__ */ jsx16(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
728
1034
  ] });
729
1035
  }
730
1036
 
@@ -735,7 +1041,7 @@ import { useMemo } from "react";
735
1041
  import { Check, Copy } from "lucide-react";
736
1042
 
737
1043
  // src/participant/useParticipantCopyFeedback.ts
738
- import { useEffect as useEffect3, useReducer as useReducer2 } from "react";
1044
+ import { useEffect as useEffect6, useReducer as useReducer3 } from "react";
739
1045
 
740
1046
  // src/participant/participantProtocolCopy.ts
741
1047
  async function copyProtocolToClipboard(protocol, clipboard) {
@@ -756,10 +1062,10 @@ function protocolCopyReducer(state, action) {
756
1062
  // src/participant/useParticipantCopyFeedback.ts
757
1063
  var COPIED_FEEDBACK_MS = 2e3;
758
1064
  function useParticipantCopyFeedback() {
759
- const [state, dispatch] = useReducer2(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1065
+ const [state, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
760
1066
  const announce = useAnnounceCopy();
761
1067
  const { isCopied, copyCount } = state;
762
- useEffect3(() => {
1068
+ useEffect6(() => {
763
1069
  if (!isCopied) return void 0;
764
1070
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_FEEDBACK_MS);
765
1071
  return () => clearTimeout(timer);
@@ -774,10 +1080,10 @@ function useParticipantCopyFeedback() {
774
1080
  }
775
1081
 
776
1082
  // src/participant/ParticipantCopyableToken.tsx
777
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1083
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
778
1084
  function ParticipantCopyableToken({ copyKind, value, labels }) {
779
1085
  const { isCopied, copy } = useParticipantCopyFeedback();
780
- return /* @__PURE__ */ jsxs10(
1086
+ return /* @__PURE__ */ jsxs13(
781
1087
  "button",
782
1088
  {
783
1089
  type: "button",
@@ -786,8 +1092,8 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
786
1092
  onClick: () => copy(value, labels.valueCopied),
787
1093
  children: [
788
1094
  value,
789
- isCopied ? /* @__PURE__ */ jsxs10("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
790
- /* @__PURE__ */ jsx14(Check, { size: 12, "aria-hidden": true }),
1095
+ isCopied ? /* @__PURE__ */ jsxs13("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
1096
+ /* @__PURE__ */ jsx17(Check, { size: 12, "aria-hidden": true }),
791
1097
  " ",
792
1098
  labels.valueCopied
793
1099
  ] }) : null
@@ -797,33 +1103,34 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
797
1103
  }
798
1104
  function ParticipantLinkCopyButton({ value, labels }) {
799
1105
  const { isCopied, copy } = useParticipantCopyFeedback();
800
- return /* @__PURE__ */ jsx14(
1106
+ return /* @__PURE__ */ jsx17(
801
1107
  "button",
802
1108
  {
803
1109
  type: "button",
804
1110
  className: "cv-p-text__link-copy",
805
1111
  "aria-label": `${labels.copyValue} ${value}`,
1112
+ "data-cv-tooltip": labels.copyValue,
806
1113
  onClick: () => copy(value, labels.valueCopied),
807
- children: isCopied ? /* @__PURE__ */ jsx14(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx14(Copy, { size: 14, "aria-hidden": true })
1114
+ children: isCopied ? /* @__PURE__ */ jsx17(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx17(Copy, { size: 14, "aria-hidden": true })
808
1115
  }
809
1116
  );
810
1117
  }
811
1118
 
812
1119
  // src/participant/ParticipantMessageNodes.tsx
813
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1120
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
814
1121
  var LINK_REL = "noopener noreferrer nofollow ugc";
815
1122
  function renderLink(node, labels, key) {
816
1123
  const isExternal = /^https?:/i.test(node.href);
817
- return /* @__PURE__ */ jsxs11("span", { className: "cv-p-text__link-group", children: [
818
- /* @__PURE__ */ jsxs11("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
1124
+ return /* @__PURE__ */ jsxs14("span", { className: "cv-p-text__link-group", children: [
1125
+ /* @__PURE__ */ jsxs14("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
819
1126
  node.label,
820
- isExternal ? /* @__PURE__ */ jsxs11("span", { className: "cv-p-sr-only", children: [
1127
+ isExternal ? /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
821
1128
  " (",
822
1129
  labels.externalLinkHint,
823
1130
  ")"
824
1131
  ] }) : null
825
1132
  ] }),
826
- /* @__PURE__ */ jsx15(ParticipantLinkCopyButton, { value: node.href, labels })
1133
+ /* @__PURE__ */ jsx18(ParticipantLinkCopyButton, { value: node.href, labels })
827
1134
  ] }, key);
828
1135
  }
829
1136
  function renderInlineNodes(nodes, labels) {
@@ -832,17 +1139,17 @@ function renderInlineNodes(nodes, labels) {
832
1139
  case "text":
833
1140
  return node.value;
834
1141
  case "strong":
835
- return /* @__PURE__ */ jsx15("strong", { children: renderInlineNodes(node.children, labels) }, key);
1142
+ return /* @__PURE__ */ jsx18("strong", { children: renderInlineNodes(node.children, labels) }, key);
836
1143
  case "em":
837
- return /* @__PURE__ */ jsx15("em", { children: renderInlineNodes(node.children, labels) }, key);
1144
+ return /* @__PURE__ */ jsx18("em", { children: renderInlineNodes(node.children, labels) }, key);
838
1145
  case "del":
839
- return /* @__PURE__ */ jsx15("del", { children: renderInlineNodes(node.children, labels) }, key);
1146
+ return /* @__PURE__ */ jsx18("del", { children: renderInlineNodes(node.children, labels) }, key);
840
1147
  case "code":
841
- return /* @__PURE__ */ jsx15("code", { className: "cv-p-text__code", children: node.value }, key);
1148
+ return /* @__PURE__ */ jsx18("code", { className: "cv-p-text__code", children: node.value }, key);
842
1149
  case "link":
843
1150
  return renderLink(node, labels, key);
844
1151
  case "copyable":
845
- return /* @__PURE__ */ jsx15(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
1152
+ return /* @__PURE__ */ jsx18(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
846
1153
  }
847
1154
  });
848
1155
  }
@@ -1228,90 +1535,56 @@ function startsFenceLine(line) {
1228
1535
  }
1229
1536
 
1230
1537
  // src/participant/ParticipantMessageText.tsx
1231
- import { jsx as jsx16 } from "react/jsx-runtime";
1538
+ import { jsx as jsx19 } from "react/jsx-runtime";
1232
1539
  function renderBlock(block, labels, key) {
1233
- if (block.kind === "paragraph") return /* @__PURE__ */ jsx16("span", { children: renderInlineNodes(block.children, labels) }, key);
1540
+ if (block.kind === "paragraph") return /* @__PURE__ */ jsx19("span", { children: renderInlineNodes(block.children, labels) }, key);
1234
1541
  if (block.kind === "pre") {
1235
- return /* @__PURE__ */ jsx16("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx16("code", { children: block.value }) }, key);
1542
+ return /* @__PURE__ */ jsx19("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx19("code", { children: block.value }) }, key);
1236
1543
  }
1237
1544
  if (block.kind === "quote") {
1238
- return /* @__PURE__ */ jsx16("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
1545
+ return /* @__PURE__ */ jsx19("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
1239
1546
  }
1240
- const items = block.items.map((item, index) => /* @__PURE__ */ jsx16("li", { children: renderInlineNodes(item, labels) }, index));
1547
+ const items = block.items.map((item, index) => /* @__PURE__ */ jsx19("li", { children: renderInlineNodes(item, labels) }, index));
1241
1548
  if (block.ordered) {
1242
- return /* @__PURE__ */ jsx16("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
1549
+ return /* @__PURE__ */ jsx19("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
1243
1550
  }
1244
- return /* @__PURE__ */ jsx16("ul", { className: "cv-p-text__list", children: items }, key);
1551
+ return /* @__PURE__ */ jsx19("ul", { className: "cv-p-text__list", children: items }, key);
1245
1552
  }
1246
- function ParticipantMessageText({ text, labels }) {
1553
+ function ParticipantMessageText({ text, labels, id }) {
1247
1554
  const blocks = useMemo(() => parseParticipantMessage(text), [text]);
1248
1555
  if (blocks.length === 0) return null;
1249
1556
  const [only] = blocks;
1250
1557
  const children = blocks.length === 1 && only?.kind === "paragraph" ? renderInlineNodes(only.children, labels) : blocks.map((block, key) => renderBlock(block, labels, key));
1251
- return /* @__PURE__ */ jsx16("div", { className: "cv-p-text", children });
1558
+ return /* @__PURE__ */ jsx19("div", { className: "cv-p-text", id, children });
1252
1559
  }
1253
1560
 
1254
- // src/participant/ParticipantAttachmentItem.tsx
1255
- import { useEffect as useEffect4, useState as useState2 } from "react";
1256
-
1257
- // src/participant/participantAttachmentUrl.ts
1258
- var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
1259
- function isSafeAttachmentUrl(url) {
1260
- try {
1261
- return SAFE_ATTACHMENT_PROTOCOLS.has(new URL(url.trim()).protocol);
1262
- } catch {
1263
- return false;
1264
- }
1561
+ // src/participant/ParticipantBubbleBody.tsx
1562
+ import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1563
+ function BubbleHeading({ content, labels }) {
1564
+ if (content.isMine) return /* @__PURE__ */ jsx20("span", { className: "cv-p-sr-only", children: labels.me });
1565
+ if (!content.authorName) return null;
1566
+ return /* @__PURE__ */ jsx20("span", { className: "cv-p-bubble__author", children: content.authorName });
1265
1567
  }
1266
-
1267
- // src/participant/ParticipantAttachmentItem.tsx
1268
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1269
- function useAttachmentUrl(attachment, resolveAttachmentUrl) {
1270
- const [url, setUrl] = useState2(void 0);
1271
- const disposition = attachment.kind === "document" ? "attachment" : "inline";
1272
- useEffect4(() => {
1273
- let isCurrent = true;
1274
- resolveAttachmentUrl(attachment, disposition).then(
1275
- (resolved) => {
1276
- if (isCurrent) setUrl(isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0);
1568
+ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
1569
+ return /* @__PURE__ */ jsxs15(Fragment4, { children: [
1570
+ /* @__PURE__ */ jsx20(BubbleHeading, { content, labels }),
1571
+ content.text ? /* @__PURE__ */ jsx20(ParticipantMessageText, { text: content.text, labels, id: textId }) : null,
1572
+ content.attachments.map((attachment) => /* @__PURE__ */ jsx20(
1573
+ ParticipantAttachmentItem,
1574
+ {
1575
+ attachment,
1576
+ resolveAttachmentUrl,
1577
+ labels
1277
1578
  },
1278
- () => {
1279
- if (isCurrent) setUrl(void 0);
1280
- }
1281
- );
1282
- return () => {
1283
- isCurrent = false;
1284
- };
1285
- }, [attachment, disposition, resolveAttachmentUrl]);
1286
- return url;
1287
- }
1288
- function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
1289
- const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
1290
- if (attachment.kind === "image") {
1291
- return /* @__PURE__ */ jsxs12("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
1292
- url ? /* @__PURE__ */ jsx17("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
1293
- /* @__PURE__ */ jsx17("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
1294
- ] });
1295
- }
1296
- if (attachment.kind === "audio") {
1297
- return /* @__PURE__ */ jsxs12("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
1298
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1299
- /* @__PURE__ */ jsx17("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
1300
- ] });
1301
- }
1302
- return /* @__PURE__ */ jsxs12(
1303
- "a",
1304
- {
1305
- className: "cv-p-attachment cv-p-attachment--document",
1306
- href: url,
1307
- download: attachment.filename,
1308
- "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
1309
- children: [
1310
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1311
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
1312
- ]
1313
- }
1314
- );
1579
+ attachment.id
1580
+ )),
1581
+ content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1582
+ /* @__PURE__ */ jsxs15("span", { className: "cv-p-bubble__meta", children: [
1583
+ /* @__PURE__ */ jsx20("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1584
+ ownStatus ? /* @__PURE__ */ jsx20(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
1585
+ ownStatus === "failed" ? /* @__PURE__ */ jsx20(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
1586
+ ] })
1587
+ ] });
1315
1588
  }
1316
1589
 
1317
1590
  // src/participant/participantPerspective.ts
@@ -1376,7 +1649,7 @@ function resolveOwnMessageStatus(params) {
1376
1649
  }
1377
1650
 
1378
1651
  // src/participant/ParticipantMessageBubble.tsx
1379
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1652
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1380
1653
  function describeItem(item, perspective) {
1381
1654
  if (item.kind === "server") {
1382
1655
  const { message } = item;
@@ -1399,49 +1672,27 @@ function describeItem(item, perspective) {
1399
1672
  displayState: item.displayState
1400
1673
  };
1401
1674
  }
1402
- function bubbleClassName(isMine, hasTail) {
1675
+ function bubbleClassName(isMine, hasTail, hasFailed) {
1403
1676
  const classes = ["cv-p-bubble"];
1404
1677
  if (isMine) classes.push("cv-p-bubble--mine");
1678
+ if (hasFailed) classes.push("cv-p-bubble--failed");
1405
1679
  if (!hasTail) classes.push("cv-p-bubble--no-tail");
1406
1680
  return classes.join(" ");
1407
1681
  }
1408
- function ParticipantMessageBubble({
1409
- item,
1410
- labels,
1411
- confirmsRead,
1412
- resolveAttachmentUrl,
1413
- onRetry,
1414
- onDiscard,
1415
- onEdit,
1416
- avatar,
1417
- tail = true,
1418
- perspective
1419
- }) {
1682
+ function ParticipantMessageBubble(props) {
1683
+ const { item, labels, confirmsRead, onRetry, avatar, tail = true, perspective } = props;
1684
+ const textId = useId3();
1420
1685
  const content = describeItem(item, perspective);
1421
- const { isMine } = content;
1422
- const ownStatus = isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1423
- const bubble = /* @__PURE__ */ jsxs13("div", { className: bubbleClassName(isMine, tail), children: [
1424
- isMine ? /* @__PURE__ */ jsx18("span", { className: "cv-p-sr-only", children: labels.me }) : content.authorName ? /* @__PURE__ */ jsx18("span", { className: "cv-p-bubble__author", children: content.authorName }) : null,
1425
- content.text ? /* @__PURE__ */ jsx18(ParticipantMessageText, { text: content.text, labels }) : null,
1426
- content.attachments.map((attachment) => /* @__PURE__ */ jsx18(
1427
- ParticipantAttachmentItem,
1428
- {
1429
- attachment,
1430
- resolveAttachmentUrl,
1431
- labels
1432
- },
1433
- attachment.id
1434
- )),
1435
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx18("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1436
- /* @__PURE__ */ jsxs13("span", { className: "cv-p-bubble__meta", children: [
1437
- /* @__PURE__ */ jsx18("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1438
- ownStatus ? /* @__PURE__ */ jsx18(OwnStatus, { status: ownStatus, labels, onRetry }) : null
1439
- ] }),
1440
- ownStatus === "failed" ? /* @__PURE__ */ jsx18(FailedActions, { labels, onDiscard, onEdit }) : null
1686
+ const ownStatus = content.isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1687
+ const isFailed = ownStatus === "failed";
1688
+ const canRetry = isFailed && onRetry !== void 0;
1689
+ const bubble = /* @__PURE__ */ jsxs16("div", { className: bubbleClassName(content.isMine, tail, isFailed), children: [
1690
+ canRetry ? /* @__PURE__ */ jsx21(RetryButton, { labels, onRetry, describedBy: textId }) : null,
1691
+ /* @__PURE__ */ jsx21(BubbleBody, { ...props, content, ownStatus, textId: canRetry ? textId : void 0 })
1441
1692
  ] });
1442
- if (avatar === void 0 || isMine) return bubble;
1443
- return /* @__PURE__ */ jsxs13("div", { className: "cv-p-bubble-row", children: [
1444
- /* @__PURE__ */ jsx18("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1693
+ if (avatar === void 0 || content.isMine) return bubble;
1694
+ return /* @__PURE__ */ jsxs16("div", { className: "cv-p-bubble-row", children: [
1695
+ /* @__PURE__ */ jsx21("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1445
1696
  bubble
1446
1697
  ] });
1447
1698
  }
@@ -1463,7 +1714,7 @@ function resolveParticipantBubbleActions(item, handlers) {
1463
1714
  }
1464
1715
 
1465
1716
  // src/participant/ParticipantTimeline.tsx
1466
- import { jsx as jsx19 } from "react/jsx-runtime";
1717
+ import { jsx as jsx22 } from "react/jsx-runtime";
1467
1718
  function createdAtOf2(item) {
1468
1719
  return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1469
1720
  }
@@ -1480,7 +1731,7 @@ function resolveActions(item, pendingActions) {
1480
1731
  }
1481
1732
  function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1482
1733
  if (!isEnabled || isMine) return void 0;
1483
- return showAvatar ? /* @__PURE__ */ jsx19(ParticipantAuthorAvatar, { name: author, render }) : null;
1734
+ return showAvatar ? /* @__PURE__ */ jsx22(ParticipantAuthorAvatar, { name: author, render }) : null;
1484
1735
  }
1485
1736
  function ParticipantTimeline(props) {
1486
1737
  const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
@@ -1492,7 +1743,7 @@ function ParticipantTimeline(props) {
1492
1743
  for (const item of items) {
1493
1744
  const day = new Date(createdAtOf2(item));
1494
1745
  if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1495
- nodes.push(/* @__PURE__ */ jsx19(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1746
+ nodes.push(/* @__PURE__ */ jsx22(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1496
1747
  previousAuthor = void 0;
1497
1748
  }
1498
1749
  previousDay = day;
@@ -1507,7 +1758,7 @@ function ParticipantTimeline(props) {
1507
1758
  render: props.renderAuthorAvatar
1508
1759
  });
1509
1760
  nodes.push(
1510
- /* @__PURE__ */ jsx19(
1761
+ /* @__PURE__ */ jsx22(
1511
1762
  ParticipantMessageBubble,
1512
1763
  {
1513
1764
  item,
@@ -1523,19 +1774,19 @@ function ParticipantTimeline(props) {
1523
1774
  )
1524
1775
  );
1525
1776
  }
1526
- return /* @__PURE__ */ jsx19("div", { className: "cv-p-thread__messages", children: nodes });
1777
+ return /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__messages", children: nodes });
1527
1778
  }
1528
1779
 
1529
1780
  // src/participant/ParticipantConversationAvatar.tsx
1530
- import { jsx as jsx20 } from "react/jsx-runtime";
1781
+ import { jsx as jsx23 } from "react/jsx-runtime";
1531
1782
  function ParticipantConversationAvatar({ icon }) {
1532
- return /* @__PURE__ */ jsx20("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
1783
+ return /* @__PURE__ */ jsx23("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
1533
1784
  }
1534
1785
 
1535
1786
  // src/participant/ParticipantProtocolBadge.tsx
1536
1787
  import { Check as Check2, Copy as Copy2 } from "lucide-react";
1537
- import { useEffect as useEffect5, useReducer as useReducer3 } from "react";
1538
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
1788
+ import { useEffect as useEffect7, useReducer as useReducer4 } from "react";
1789
+ import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1539
1790
  var COPIED_ANNOUNCEMENT_MS = 3e3;
1540
1791
  function ParticipantProtocolBadgeView({
1541
1792
  protocol,
@@ -1544,36 +1795,37 @@ function ParticipantProtocolBadgeView({
1544
1795
  onCopy
1545
1796
  }) {
1546
1797
  const { isCopied, copyCount } = copyState;
1547
- return /* @__PURE__ */ jsxs14(Fragment4, { children: [
1548
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1549
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol__code", children: [
1550
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
1798
+ return /* @__PURE__ */ jsxs17(Fragment5, { children: [
1799
+ /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1800
+ /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol__code", children: [
1801
+ /* @__PURE__ */ jsxs17("span", { className: "cv-p-sr-only", children: [
1551
1802
  labels.protocolPrefix,
1552
1803
  " "
1553
1804
  ] }),
1554
1805
  protocol
1555
1806
  ] }),
1556
- /* @__PURE__ */ jsxs14(
1807
+ /* @__PURE__ */ jsxs17(
1557
1808
  "button",
1558
1809
  {
1559
1810
  type: "button",
1560
1811
  className: "cv-p-protocol__copy",
1561
1812
  "aria-label": `${labels.copyProtocol} ${protocol}`,
1813
+ "data-cv-tooltip": labels.copyProtocol,
1562
1814
  onClick: onCopy,
1563
1815
  children: [
1564
- isCopied ? /* @__PURE__ */ jsx21(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx21(Copy2, { size: 18, "aria-hidden": true }),
1565
- isCopied ? /* @__PURE__ */ jsx21("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
1816
+ isCopied ? /* @__PURE__ */ jsx24(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx24(Copy2, { size: 18, "aria-hidden": true }),
1817
+ isCopied ? /* @__PURE__ */ jsx24("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
1566
1818
  ]
1567
1819
  }
1568
1820
  )
1569
1821
  ] }),
1570
- /* @__PURE__ */ jsx21("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx21("span", { children: labels.protocolCopied }, copyCount) : null })
1822
+ /* @__PURE__ */ jsx24("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx24("span", { children: labels.protocolCopied }, copyCount) : null })
1571
1823
  ] });
1572
1824
  }
1573
1825
  function ParticipantProtocolBadge({ protocol, labels }) {
1574
- const [copyState, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1826
+ const [copyState, dispatch] = useReducer4(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1575
1827
  const { isCopied, copyCount } = copyState;
1576
- useEffect5(() => {
1828
+ useEffect7(() => {
1577
1829
  if (!isCopied) return void 0;
1578
1830
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
1579
1831
  return () => clearTimeout(timer);
@@ -1582,7 +1834,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1582
1834
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1583
1835
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
1584
1836
  }
1585
- return /* @__PURE__ */ jsx21(
1837
+ return /* @__PURE__ */ jsx24(
1586
1838
  ParticipantProtocolBadgeView,
1587
1839
  {
1588
1840
  protocol,
@@ -1594,19 +1846,19 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1594
1846
  }
1595
1847
 
1596
1848
  // src/participant/ParticipantThreadHeader.tsx
1597
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
1849
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1598
1850
  function Title({ conversation, labels, onOpenSubject }) {
1599
1851
  const { subjectType, subjectId, subjectLabel } = conversation;
1600
- return /* @__PURE__ */ jsx22("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx22(
1852
+ return /* @__PURE__ */ jsx25("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx25(
1601
1853
  "button",
1602
1854
  {
1603
1855
  type: "button",
1604
1856
  className: "cv-p-thread__title-button",
1605
1857
  "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1606
1858
  onClick: () => onOpenSubject({ subjectType, subjectId }),
1607
- children: /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1859
+ children: /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1608
1860
  }
1609
- ) : /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1861
+ ) : /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1610
1862
  }
1611
1863
  function ParticipantThreadHeader({
1612
1864
  conversation,
@@ -1621,30 +1873,30 @@ function ParticipantThreadHeader({
1621
1873
  const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1622
1874
  const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1623
1875
  const hasChannels = resolveParticipantChannels(conversation.channels).length > 0;
1624
- return /* @__PURE__ */ jsxs15("header", { className: "cv-p-thread__header", children: [
1625
- onBack ? /* @__PURE__ */ jsx22(ParticipantBackButton, { label: labels.back, onBack }) : null,
1626
- icon !== void 0 ? /* @__PURE__ */ jsx22(ParticipantConversationAvatar, { icon }) : null,
1627
- /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__heading", children: [
1628
- icon === void 0 && group ? /* @__PURE__ */ jsx22("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1629
- /* @__PURE__ */ jsx22(Title, { conversation, labels, onOpenSubject }),
1630
- counterpartLabel ? /* @__PURE__ */ jsx22("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
1631
- conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__meta", children: [
1632
- conversation.protocol ? /* @__PURE__ */ jsx22(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1633
- /* @__PURE__ */ jsx22(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
1876
+ return /* @__PURE__ */ jsxs18("header", { className: "cv-p-thread__header", children: [
1877
+ onBack ? /* @__PURE__ */ jsx25(ParticipantBackButton, { label: labels.back, onBack }) : null,
1878
+ icon !== void 0 ? /* @__PURE__ */ jsx25(ParticipantConversationAvatar, { icon }) : null,
1879
+ /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__heading", children: [
1880
+ icon === void 0 && group ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1881
+ /* @__PURE__ */ jsx25(Title, { conversation, labels, onOpenSubject }),
1882
+ counterpartLabel ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
1883
+ conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__meta", children: [
1884
+ conversation.protocol ? /* @__PURE__ */ jsx25(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1885
+ /* @__PURE__ */ jsx25(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
1634
1886
  ] }) : null
1635
1887
  ] }),
1636
- actions ? /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__actions", children: actions }) : null
1888
+ actions ? /* @__PURE__ */ jsx25("div", { className: "cv-p-thread__actions", children: actions }) : null
1637
1889
  ] });
1638
1890
  }
1639
1891
 
1640
1892
  // src/participant/ParticipantThread.tsx
1641
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
1893
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1642
1894
  function Footer({ props }) {
1643
1895
  const { conversation, labels, draft } = props;
1644
1896
  if (conversation.status === "closed") {
1645
- return /* @__PURE__ */ jsx23("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
1897
+ return /* @__PURE__ */ jsx26("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
1646
1898
  }
1647
- return /* @__PURE__ */ jsx23(
1899
+ return /* @__PURE__ */ jsx26(
1648
1900
  ParticipantComposer,
1649
1901
  {
1650
1902
  value: draft.value,
@@ -1661,31 +1913,36 @@ function Footer({ props }) {
1661
1913
  }
1662
1914
  );
1663
1915
  }
1916
+ function Header({ props }) {
1917
+ return /* @__PURE__ */ jsx26(
1918
+ ParticipantThreadHeader,
1919
+ {
1920
+ conversation: props.conversation,
1921
+ labels: props.labels,
1922
+ subjectGroups: props.subjectGroups,
1923
+ renderSubjectIcon: props.renderSubjectIcon,
1924
+ onBack: props.onBack,
1925
+ onOpenSubject: props.onOpenSubject,
1926
+ counterpartLabel: props.counterpartLabel,
1927
+ actions: props.headerActions
1928
+ }
1929
+ );
1930
+ }
1664
1931
  function ThreadBody(props) {
1665
1932
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
1666
1933
  const subjectCard = renderSubjectCard?.(conversation);
1667
1934
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
1668
- return /* @__PURE__ */ jsxs16("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
1669
- /* @__PURE__ */ jsx23(
1670
- ParticipantThreadHeader,
1671
- {
1672
- conversation,
1673
- labels,
1674
- subjectGroups: props.subjectGroups,
1675
- renderSubjectIcon: props.renderSubjectIcon,
1676
- onBack: props.onBack,
1677
- onOpenSubject: props.onOpenSubject,
1678
- counterpartLabel: props.counterpartLabel,
1679
- actions: props.headerActions
1680
- }
1681
- ),
1682
- subjectCard ? /* @__PURE__ */ jsx23("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1683
- /* @__PURE__ */ jsxs16("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1684
- loadView.isLoading ? /* @__PURE__ */ jsx23(ParticipantLoading, { labels }) : null,
1685
- loadView.hasError ? /* @__PURE__ */ jsx23(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1686
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx23("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1687
- /* @__PURE__ */ jsx23(ParticipantTimeline, { ...props }),
1688
- /* @__PURE__ */ jsx23(
1935
+ const rootRef = useRef5(null);
1936
+ useSuppressTouchTooltips(rootRef);
1937
+ return /* @__PURE__ */ jsxs19("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
1938
+ /* @__PURE__ */ jsx26(Header, { props }),
1939
+ subjectCard ? /* @__PURE__ */ jsx26("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1940
+ /* @__PURE__ */ jsxs19("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1941
+ loadView.isLoading ? /* @__PURE__ */ jsx26(ParticipantLoading, { labels }) : null,
1942
+ loadView.hasError ? /* @__PURE__ */ jsx26(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1943
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx26("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1944
+ /* @__PURE__ */ jsx26(ParticipantTimeline, { ...props }),
1945
+ /* @__PURE__ */ jsx26(
1689
1946
  ParticipantScrollToLatest,
1690
1947
  {
1691
1948
  label: labels.scrollToLatest,
@@ -1696,12 +1953,15 @@ function ThreadBody(props) {
1696
1953
  }
1697
1954
  )
1698
1955
  ] }),
1699
- /* @__PURE__ */ jsx23(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1700
- /* @__PURE__ */ jsx23(Footer, { props })
1956
+ /* @__PURE__ */ jsx26(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1957
+ /* @__PURE__ */ jsx26(Footer, { props })
1701
1958
  ] });
1702
1959
  }
1703
1960
  function ParticipantThread(props) {
1704
- return /* @__PURE__ */ jsx23(ParticipantCopyAnnouncer, { children: /* @__PURE__ */ jsx23(ThreadBody, { ...props }) });
1961
+ return /* @__PURE__ */ jsxs19(ParticipantCopyAnnouncer, { children: [
1962
+ /* @__PURE__ */ jsx26(ThreadBody, { ...props }),
1963
+ /* @__PURE__ */ jsx26(TooltipLayer, {})
1964
+ ] });
1705
1965
  }
1706
1966
 
1707
1967
  // src/participant/useParticipantThreadController.ts
@@ -1833,7 +2093,7 @@ function participantSendStatesReducer(states, { subject, action }) {
1833
2093
  }
1834
2094
 
1835
2095
  // src/participant/useParticipantThread.ts
1836
- import { useCallback as useCallback4, useEffect as useEffect9, useMemo as useMemo2, useReducer as useReducer4, useRef as useRef7 } from "react";
2096
+ import { useCallback as useCallback4, useEffect as useEffect11, useMemo as useMemo2, useReducer as useReducer5, useRef as useRef9 } from "react";
1837
2097
 
1838
2098
  // src/participant/participantThreadState.ts
1839
2099
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -1869,7 +2129,7 @@ function participantThreadReducer(state, action) {
1869
2129
  }
1870
2130
 
1871
2131
  // src/participant/useParticipantMarkRead.ts
1872
- import { useEffect as useEffect7 } from "react";
2132
+ import { useEffect as useEffect9 } from "react";
1873
2133
 
1874
2134
  // src/participant/shouldMarkRead.ts
1875
2135
  function shouldMarkParticipantRead(params) {
@@ -1881,12 +2141,12 @@ function shouldMarkParticipantRead(params) {
1881
2141
  }
1882
2142
 
1883
2143
  // src/participant/useDocumentVisibility.ts
1884
- import { useEffect as useEffect6, useState as useState3 } from "react";
2144
+ import { useEffect as useEffect8, useState as useState4 } from "react";
1885
2145
  function useDocumentVisibility() {
1886
- const [visibilityState, setVisibilityState] = useState3(
2146
+ const [visibilityState, setVisibilityState] = useState4(
1887
2147
  () => typeof document === "undefined" ? "visible" : document.visibilityState
1888
2148
  );
1889
- useEffect6(() => {
2149
+ useEffect8(() => {
1890
2150
  function handleVisibilityChange() {
1891
2151
  setVisibilityState(document.visibilityState);
1892
2152
  }
@@ -1901,18 +2161,18 @@ function useDocumentVisibility() {
1901
2161
  function useParticipantMarkRead(params) {
1902
2162
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
1903
2163
  const visibilityState = useDocumentVisibility();
1904
- useEffect7(() => {
2164
+ useEffect9(() => {
1905
2165
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
1906
2166
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
1907
2167
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
1908
2168
  }
1909
2169
 
1910
2170
  // src/participant/useParticipantRevalidation.ts
1911
- import { useEffect as useEffect8, useRef as useRef4 } from "react";
2171
+ import { useEffect as useEffect10, useRef as useRef6 } from "react";
1912
2172
  function useParticipantRevalidation(api, onRevalidate) {
1913
- const handlerRef = useRef4(onRevalidate);
2173
+ const handlerRef = useRef6(onRevalidate);
1914
2174
  handlerRef.current = onRevalidate;
1915
- useEffect8(() => {
2175
+ useEffect10(() => {
1916
2176
  function handleFocus() {
1917
2177
  handlerRef.current();
1918
2178
  }
@@ -1939,10 +2199,10 @@ function useParticipantRevalidation(api, onRevalidate) {
1939
2199
  }
1940
2200
 
1941
2201
  // src/participant/useParticipantSend.ts
1942
- import { useCallback as useCallback2, useRef as useRef5 } from "react";
2202
+ import { useCallback as useCallback2, useRef as useRef7 } from "react";
1943
2203
  function useParticipantSend(params) {
1944
2204
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
1945
- const entriesRef = useRef5(params.entries);
2205
+ const entriesRef = useRef7(params.entries);
1946
2206
  entriesRef.current = params.entries;
1947
2207
  const deliver = useCallback2(
1948
2208
  async (entry) => {
@@ -1994,11 +2254,11 @@ function useParticipantSend(params) {
1994
2254
  }
1995
2255
 
1996
2256
  // src/participant/useParticipantUnread.ts
1997
- import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
2257
+ import { useCallback as useCallback3, useRef as useRef8, useState as useState5 } from "react";
1998
2258
  function useParticipantUnread(params) {
1999
2259
  const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2000
- const [acknowledgedCount, setAcknowledgedCount] = useState4(0);
2001
- const messagesRef = useRef6(messages);
2260
+ const [acknowledgedCount, setAcknowledgedCount] = useState5(0);
2261
+ const messagesRef = useRef8(messages);
2002
2262
  messagesRef.current = messages;
2003
2263
  const handleMarkedRead = useCallback3(
2004
2264
  (subject) => {
@@ -2024,10 +2284,10 @@ function useParticipantThread(params) {
2024
2284
  const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
2025
2285
  const { subjectType, subjectId } = subject;
2026
2286
  const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2027
- const [state, dispatch] = useReducer4(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2028
- const apiRef = useRef7(api);
2287
+ const [state, dispatch] = useReducer5(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2288
+ const apiRef = useRef9(api);
2029
2289
  apiRef.current = api;
2030
- const activeSubjectKey = useRef7("");
2290
+ const activeSubjectKey = useRef9("");
2031
2291
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
2032
2292
  const refresh = useCallback4(async () => {
2033
2293
  const requestKey = `${subjectType}:${subjectId}`;
@@ -2056,7 +2316,7 @@ function useParticipantThread(params) {
2056
2316
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
2057
2317
  const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
2058
2318
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
2059
- useEffect9(() => {
2319
+ useEffect11(() => {
2060
2320
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
2061
2321
  dispatch({ type: "reset" });
2062
2322
  void refresh();
@@ -2064,7 +2324,7 @@ function useParticipantThread(params) {
2064
2324
  useParticipantRevalidation(api, (event) => {
2065
2325
  if (isForSubject(event, stableSubject)) void refresh();
2066
2326
  });
2067
- useEffect9(() => {
2327
+ useEffect11(() => {
2068
2328
  if (sendEntries.length === 0) return;
2069
2329
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
2070
2330
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
@@ -2113,9 +2373,9 @@ function useParticipantConversationView(params) {
2113
2373
  }
2114
2374
 
2115
2375
  // src/participant/useParticipantDraft.ts
2116
- import { useState as useState5 } from "react";
2376
+ import { useState as useState6 } from "react";
2117
2377
  function useParticipantDraft(draftsRef, subject) {
2118
- const [draft, setDraftState] = useState5(() => getDraft(draftsRef.current, subject));
2378
+ const [draft, setDraftState] = useState6(() => getDraft(draftsRef.current, subject));
2119
2379
  function replaceDrafts(next) {
2120
2380
  draftsRef.current = next;
2121
2381
  setDraftState(getDraft(next, subject));
@@ -2135,7 +2395,7 @@ function useParticipantDraft(draftsRef, subject) {
2135
2395
  }
2136
2396
 
2137
2397
  // src/participant/useParticipantThreadScroll.ts
2138
- import { useEffect as useEffect10, useRef as useRef9 } from "react";
2398
+ import { useEffect as useEffect12, useRef as useRef11 } from "react";
2139
2399
 
2140
2400
  // src/participant/participantNewMessages.ts
2141
2401
  function collectServerMessageIds(items) {
@@ -2173,7 +2433,7 @@ function useScrollAwayState(ref, isNearBottom) {
2173
2433
  }
2174
2434
 
2175
2435
  // src/participant/useStickToBottom.ts
2176
- import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect2, useRef as useRef8, useState as useState6 } from "react";
2436
+ import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
2177
2437
 
2178
2438
  // src/participant/participantScroll.ts
2179
2439
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -2195,16 +2455,16 @@ function resolveScrollAction(params) {
2195
2455
 
2196
2456
  // src/participant/useStickToBottom.ts
2197
2457
  function useStickToBottom({ firstKey, lastKey }) {
2198
- const ref = useRef8(null);
2199
- const snapshot = useRef8({
2458
+ const ref = useRef10(null);
2459
+ const snapshot = useRef10({
2200
2460
  hasPositioned: false,
2201
2461
  firstKey: void 0,
2202
2462
  lastKey: void 0,
2203
2463
  scrollTop: 0,
2204
2464
  scrollHeight: 0
2205
2465
  });
2206
- const [isNearBottom, setIsNearBottom] = useState6(true);
2207
- const isNearBottomRef = useRef8(true);
2466
+ const [isNearBottom, setIsNearBottom] = useState7(true);
2467
+ const isNearBottomRef = useRef10(true);
2208
2468
  const onScroll = useCallback7((event) => {
2209
2469
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
2210
2470
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
@@ -2213,7 +2473,7 @@ function useStickToBottom({ firstKey, lastKey }) {
2213
2473
  isNearBottomRef.current = isNear;
2214
2474
  setIsNearBottom(isNear);
2215
2475
  }, []);
2216
- useLayoutEffect2(() => {
2476
+ useLayoutEffect3(() => {
2217
2477
  const element = ref.current;
2218
2478
  const previous = snapshot.current;
2219
2479
  if (!element) return;
@@ -2260,8 +2520,8 @@ function useParticipantThreadScroll(items, perspective) {
2260
2520
  });
2261
2521
  const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2262
2522
  const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2263
- const seenIds = useRef9(/* @__PURE__ */ new Set());
2264
- useEffect10(() => {
2523
+ const seenIds = useRef11(/* @__PURE__ */ new Set());
2524
+ useEffect12(() => {
2265
2525
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
2266
2526
  }, [isNearBottom, items]);
2267
2527
  return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
@@ -2314,12 +2574,12 @@ function useParticipantThreadController(params) {
2314
2574
  }
2315
2575
 
2316
2576
  // src/participant/ParticipantThreadScreen.tsx
2317
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2577
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
2318
2578
  function useOpenedConversation(api, subject, isNeeded) {
2319
- const [opened, setOpened] = useState7({ status: "idle" });
2579
+ const [opened, setOpened] = useState8({ status: "idle" });
2320
2580
  const { subjectType, subjectId } = subject;
2321
2581
  const canOpen = api.openConversation !== void 0;
2322
- useEffect11(() => {
2582
+ useEffect13(() => {
2323
2583
  if (!isNeeded || !canOpen) return;
2324
2584
  let isCurrent = true;
2325
2585
  setOpened({ status: "loading" });
@@ -2337,9 +2597,9 @@ function NotFound({
2337
2597
  labels,
2338
2598
  onBack
2339
2599
  }) {
2340
- return /* @__PURE__ */ jsxs17("div", { className: "cv-p cv-p-thread", children: [
2341
- /* @__PURE__ */ jsx24("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx24(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2342
- /* @__PURE__ */ jsx24("p", { className: "cv-p-empty", children: labels.notFound })
2600
+ return /* @__PURE__ */ jsxs20("div", { className: "cv-p cv-p-thread", children: [
2601
+ /* @__PURE__ */ jsx27("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx27(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2602
+ /* @__PURE__ */ jsx27("p", { className: "cv-p-empty", children: labels.notFound })
2343
2603
  ] });
2344
2604
  }
2345
2605
  function ParticipantThreadScreen(props) {
@@ -2349,12 +2609,12 @@ function ParticipantThreadScreen(props) {
2349
2609
  );
2350
2610
  const opened = useOpenedConversation(api, selected, listed === void 0);
2351
2611
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
2352
- if (conversation) return /* @__PURE__ */ jsx24(LoadedThread, { ...props, conversation });
2612
+ if (conversation) return /* @__PURE__ */ jsx27(LoadedThread, { ...props, conversation });
2353
2613
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
2354
2614
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
2355
2615
  if (isResolving || api.openConversation === void 0 && isListLoading)
2356
- return /* @__PURE__ */ jsx24("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2357
- return /* @__PURE__ */ jsx24(NotFound, { labels, onBack: props.onBack });
2616
+ return /* @__PURE__ */ jsx27("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2617
+ return /* @__PURE__ */ jsx27(NotFound, { labels, onBack: props.onBack });
2358
2618
  }
2359
2619
  function LoadedThread(props) {
2360
2620
  const core = useParticipantThreadController({
@@ -2362,7 +2622,7 @@ function LoadedThread(props) {
2362
2622
  subject: props.selected,
2363
2623
  onMarkedRead: props.inbox.markSubjectRead
2364
2624
  });
2365
- return /* @__PURE__ */ jsx24(
2625
+ return /* @__PURE__ */ jsx27(
2366
2626
  ParticipantThread,
2367
2627
  {
2368
2628
  ...core,
@@ -2500,14 +2760,14 @@ function buildParticipantThemeStyle(theme) {
2500
2760
  }
2501
2761
 
2502
2762
  // src/participant/ParticipantConversationsScreen.tsx
2503
- import { jsx as jsx25 } from "react/jsx-runtime";
2763
+ import { jsx as jsx28 } from "react/jsx-runtime";
2504
2764
  var ALL_FILTER3 = "all";
2505
2765
  function ParticipantConversationsScreen(props) {
2506
2766
  const { selected, inbox, subjectGroups, classNames } = props;
2507
- const [filter, setFilter] = useState8(ALL_FILTER3);
2508
- const [searchQuery, setSearchQuery] = useState8("");
2509
- const draftsRef = useRef10(/* @__PURE__ */ new Map());
2510
- const [sendStates, dispatchSendStates] = useReducer5(participantSendStatesReducer, /* @__PURE__ */ new Map());
2767
+ const [filter, setFilter] = useState9(ALL_FILTER3);
2768
+ const [searchQuery, setSearchQuery] = useState9("");
2769
+ const draftsRef = useRef12(/* @__PURE__ */ new Map());
2770
+ const [sendStates, dispatchSendStates] = useReducer6(participantSendStatesReducer, /* @__PURE__ */ new Map());
2511
2771
  const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2512
2772
  const view = useMemo5(
2513
2773
  () => groupParticipantConversations({
@@ -2520,7 +2780,7 @@ function ParticipantConversationsScreen(props) {
2520
2780
  );
2521
2781
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
2522
2782
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
2523
- return /* @__PURE__ */ jsx25("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
2783
+ return /* @__PURE__ */ jsx28("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
2524
2784
  ParticipantInbox,
2525
2785
  {
2526
2786
  view,
@@ -2537,7 +2797,7 @@ function ParticipantConversationsScreen(props) {
2537
2797
  renderSubjectIcon: props.renderSubjectIcon,
2538
2798
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
2539
2799
  }
2540
- ) }) : /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
2800
+ ) }) : /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
2541
2801
  ParticipantThreadScreen,
2542
2802
  {
2543
2803
  ...props,
@@ -2552,7 +2812,7 @@ function ParticipantConversationsScreen(props) {
2552
2812
  }
2553
2813
 
2554
2814
  // src/participant/useParticipantInbox.ts
2555
- import { useCallback as useCallback8, useEffect as useEffect12, useReducer as useReducer6 } from "react";
2815
+ import { useCallback as useCallback8, useEffect as useEffect14, useReducer as useReducer7 } from "react";
2556
2816
 
2557
2817
  // src/participant/participantInboxState.ts
2558
2818
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -2602,7 +2862,7 @@ function describeError2(error) {
2602
2862
  return error instanceof Error ? error.message : "unknown error";
2603
2863
  }
2604
2864
  function useParticipantInbox(api) {
2605
- const [state, dispatch] = useReducer6(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2865
+ const [state, dispatch] = useReducer7(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2606
2866
  const refresh = useCallback8(async () => {
2607
2867
  dispatch({ type: "started" });
2608
2868
  try {
@@ -2625,7 +2885,7 @@ function useParticipantInbox(api) {
2625
2885
  const markSubjectRead = useCallback8((subject) => {
2626
2886
  dispatch({ type: "markedRead", subject });
2627
2887
  }, []);
2628
- useEffect12(() => {
2888
+ useEffect14(() => {
2629
2889
  void refresh();
2630
2890
  }, [refresh]);
2631
2891
  useParticipantRevalidation(api, () => {
@@ -2635,11 +2895,11 @@ function useParticipantInbox(api) {
2635
2895
  }
2636
2896
 
2637
2897
  // src/participant/ParticipantConversations.tsx
2638
- import { jsx as jsx26 } from "react/jsx-runtime";
2898
+ import { jsx as jsx29 } from "react/jsx-runtime";
2639
2899
  function ParticipantConversations(props) {
2640
2900
  const inbox = useParticipantInbox(props.api);
2641
2901
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
2642
- return /* @__PURE__ */ jsx26(
2902
+ return /* @__PURE__ */ jsx29(
2643
2903
  ParticipantConversationsScreen,
2644
2904
  {
2645
2905
  ...props,
@@ -2649,7 +2909,7 @@ function ParticipantConversations(props) {
2649
2909
  }
2650
2910
 
2651
2911
  // src/participant/ConversationThread.tsx
2652
- import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer7, useRef as useRef11 } from "react";
2912
+ import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer8, useRef as useRef13 } from "react";
2653
2913
 
2654
2914
  // src/participant/conversationThreadApi.ts
2655
2915
  function toParticipantApi(api) {
@@ -2689,17 +2949,17 @@ function buildThreadSummary({
2689
2949
  }
2690
2950
 
2691
2951
  // src/participant/ConversationThread.tsx
2692
- import { jsx as jsx27 } from "react/jsx-runtime";
2952
+ import { jsx as jsx30 } from "react/jsx-runtime";
2693
2953
  function useConversationThreadCore(props) {
2694
2954
  const { perspective = "participant" } = props;
2695
2955
  const { subjectType, subjectId } = props.subject;
2696
2956
  const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2697
- const onMarkedReadRef = useRef11(props.onMarkedRead);
2957
+ const onMarkedReadRef = useRef13(props.onMarkedRead);
2698
2958
  onMarkedReadRef.current = props.onMarkedRead;
2699
2959
  const handleMarkedRead = useCallback9((marked) => onMarkedReadRef.current?.(marked), []);
2700
2960
  const api = useMemo6(() => toParticipantApi(props.api), [props.api]);
2701
- const draftsRef = useRef11(/* @__PURE__ */ new Map());
2702
- const [sendStates, dispatchSendStates] = useReducer7(participantSendStatesReducer, /* @__PURE__ */ new Map());
2961
+ const draftsRef = useRef13(/* @__PURE__ */ new Map());
2962
+ const [sendStates, dispatchSendStates] = useReducer8(participantSendStatesReducer, /* @__PURE__ */ new Map());
2703
2963
  const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2704
2964
  const { title, protocol, channels, status } = props;
2705
2965
  const conversation = useMemo6(
@@ -2724,12 +2984,12 @@ function useConversationThreadCore(props) {
2724
2984
  }
2725
2985
  function ConversationThreadBody(props) {
2726
2986
  const core = useConversationThreadCore(props);
2727
- return /* @__PURE__ */ jsx27(
2987
+ return /* @__PURE__ */ jsx30(
2728
2988
  "div",
2729
2989
  {
2730
2990
  className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2731
2991
  style: buildParticipantThemeStyle(props.theme),
2732
- children: /* @__PURE__ */ jsx27("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx27(
2992
+ children: /* @__PURE__ */ jsx30("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx30(
2733
2993
  ParticipantThread,
2734
2994
  {
2735
2995
  ...core,
@@ -2748,7 +3008,7 @@ function ConversationThreadBody(props) {
2748
3008
  );
2749
3009
  }
2750
3010
  function ConversationThread(props) {
2751
- return /* @__PURE__ */ jsx27(ConversationThreadBody, { ...props }, draftKey(props.subject));
3011
+ return /* @__PURE__ */ jsx30(ConversationThreadBody, { ...props }, draftKey(props.subject));
2752
3012
  }
2753
3013
  export {
2754
3014
  ConversationThread,