@adatechnology/conversations-ui 0.7.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 (81) hide show
  1. package/README.md +96 -1
  2. package/dist/{ConversationSimulatorPanel-CrfFjiWh.d.ts → ConversationSimulatorPanel-q-wVgBCN.d.ts} +1 -1
  3. package/dist/{chunk-HMCOTZAX.js → chunk-NSOAUYNE.js} +33 -28
  4. package/dist/chunk-ZSOD5AQT.js +19 -0
  5. package/dist/flows/index.js +4 -2
  6. package/dist/index.d.ts +4 -4
  7. package/dist/index.js +5 -3
  8. package/dist/participant/index.d.ts +48 -3
  9. package/dist/participant/index.js +782 -347
  10. package/dist/preview/index.d.ts +3 -3
  11. package/dist/{quickReply.types-B4gmZsSr.d.ts → quickReply.types-DzWH3NYV.d.ts} +1 -1
  12. package/dist/styles.css +173 -52
  13. package/package.json +2 -1
  14. package/src/Tooltip.tsx +5 -12
  15. package/src/TooltipDom.test.tsx +78 -0
  16. package/src/domHarness.test-helper.ts +53 -0
  17. package/src/participant/ConversationThread.test.tsx +165 -0
  18. package/src/participant/ConversationThread.tsx +117 -0
  19. package/src/participant/ParticipantBackButton.tsx +1 -1
  20. package/src/participant/ParticipantBubbleBody.tsx +54 -0
  21. package/src/participant/ParticipantBubbleStatus.tsx +22 -28
  22. package/src/participant/ParticipantChannelBadges.tsx +10 -2
  23. package/src/participant/ParticipantComposer.tsx +1 -1
  24. package/src/participant/ParticipantComposerControls.tsx +2 -0
  25. package/src/participant/ParticipantCopyableToken.tsx +1 -0
  26. package/src/participant/ParticipantFailedMenu.tsx +40 -0
  27. package/src/participant/ParticipantFailedMenuList.tsx +41 -0
  28. package/src/participant/ParticipantMessageBubble.test.tsx +6 -6
  29. package/src/participant/ParticipantMessageBubble.tsx +26 -54
  30. package/src/participant/ParticipantMessageText.test.tsx +1 -1
  31. package/src/participant/ParticipantMessageText.tsx +4 -2
  32. package/src/participant/ParticipantProtocolBadge.tsx +1 -0
  33. package/src/participant/ParticipantScrollToLatest.tsx +1 -1
  34. package/src/participant/ParticipantThread.tsx +30 -10
  35. package/src/participant/ParticipantThreadHeader.test.tsx +1 -1
  36. package/src/participant/ParticipantThreadHeader.tsx +10 -0
  37. package/src/participant/ParticipantThreadScreen.tsx +6 -46
  38. package/src/participant/ParticipantTimeline.tsx +6 -3
  39. package/src/participant/conversationThreadApi.test.ts +27 -0
  40. package/src/participant/conversationThreadApi.ts +24 -0
  41. package/src/participant/conversationThreadCss.test.ts +116 -0
  42. package/src/participant/conversationThreadSummary.ts +34 -0
  43. package/src/participant/failedMenuKeyboard.ts +29 -0
  44. package/src/participant/index.ts +3 -0
  45. package/src/participant/participantContrast.test-helper.ts +116 -0
  46. package/src/participant/participantFailedMenuDom.test.tsx +154 -0
  47. package/src/participant/participantFailedMenuState.test.ts +61 -0
  48. package/src/participant/participantFailedMenuState.ts +57 -0
  49. package/src/participant/participantFailedMessage.test.tsx +83 -0
  50. package/src/participant/participantGolden.test.tsx +46 -0
  51. package/src/participant/participantGoldenScenarios.test-helper.tsx +57 -0
  52. package/src/participant/participantLabels.ts +6 -0
  53. package/src/participant/participantMessages.ts +4 -3
  54. package/src/participant/participantNewMessages.test.ts +21 -0
  55. package/src/participant/participantNewMessages.ts +8 -3
  56. package/src/participant/participantPerspective.test.ts +67 -0
  57. package/src/participant/participantPerspective.ts +50 -0
  58. package/src/participant/participantPerspectiveRender.test.tsx +94 -0
  59. package/src/participant/participantThemeContrast.test.ts +106 -0
  60. package/src/participant/participantTooltips.test.tsx +74 -0
  61. package/src/participant/test-fixtures/conversations-list.golden.html +1 -0
  62. package/src/participant/test-fixtures/conversations-selected.golden.html +1 -0
  63. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -0
  64. package/src/participant/test-fixtures/thread-closed.golden.html +1 -0
  65. package/src/participant/test-fixtures/thread-default.golden.html +1 -0
  66. package/src/participant/test-fixtures/thread-error.golden.html +1 -0
  67. package/src/participant/test-fixtures/thread-loading.golden.html +1 -0
  68. package/src/participant/test-fixtures/thread-plain.golden.html +1 -0
  69. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -0
  70. package/src/participant/useFailedMenuEffects.ts +59 -0
  71. package/src/participant/useParticipantConversationView.ts +3 -0
  72. package/src/participant/useParticipantFailedMenu.ts +60 -0
  73. package/src/participant/useParticipantThread.ts +13 -2
  74. package/src/participant/useParticipantThreadController.ts +112 -0
  75. package/src/participant/useParticipantThreadScroll.ts +3 -1
  76. package/src/participant/useParticipantUnread.test.tsx +40 -0
  77. package/src/participant/useParticipantUnread.ts +38 -0
  78. package/src/participant/useSuppressTouchTooltips.ts +38 -0
  79. package/src/styles.css +43 -17
  80. package/src/tooltipLayerRegistry.test.ts +56 -0
  81. 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 useRef9, useState as useState7 } 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, useMemo as useMemo4, useState as useState6 } 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 })
841
+ ] });
842
+ }
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
721
884
  ] });
722
885
  }
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
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,80 @@ 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;
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
+ ] });
1287
1588
  }
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
- );
1589
+
1590
+ // src/participant/participantPerspective.ts
1591
+ var DEFAULT_PARTICIPANT_PERSPECTIVE = "participant";
1592
+ var OWN_DIRECTION = {
1593
+ participant: "inbound",
1594
+ operator: "outbound"
1595
+ };
1596
+ function isMessageMine(message, perspective = DEFAULT_PARTICIPANT_PERSPECTIVE) {
1597
+ return message.direction === OWN_DIRECTION[perspective];
1598
+ }
1599
+ function isUnreadByViewer(message, perspective) {
1600
+ if (isMessageMine(message, perspective)) return false;
1601
+ return !message.readAt && message.status !== "read";
1602
+ }
1603
+ function countOtherSideMessages(messages, perspective) {
1604
+ return messages.filter((message) => !isMessageMine(message, perspective)).length;
1605
+ }
1606
+ function countUnreadByViewer(messages, perspective) {
1607
+ return messages.filter((message) => isUnreadByViewer(message, perspective)).length;
1608
+ }
1609
+ function resolveViewerUnread({ messages, perspective, acknowledgedCount }) {
1610
+ if (countOtherSideMessages(messages, perspective) <= acknowledgedCount) return 0;
1611
+ return countUnreadByViewer(messages, perspective);
1315
1612
  }
1316
1613
 
1317
1614
  // src/participant/participantMessages.ts
@@ -1339,8 +1636,8 @@ function mergeParticipantMessages(params) {
1339
1636
  }
1340
1637
  return items.sort((left, right) => createdAtOf(left) - createdAtOf(right));
1341
1638
  }
1342
- function isOwnMessage(message) {
1343
- return message.direction === "inbound";
1639
+ function isOwnMessage(message, perspective) {
1640
+ return isMessageMine(message, perspective);
1344
1641
  }
1345
1642
  function resolveOwnMessageStatus(params) {
1346
1643
  const { displayState, serverStatus, confirmsRead } = params;
@@ -1352,12 +1649,12 @@ function resolveOwnMessageStatus(params) {
1352
1649
  }
1353
1650
 
1354
1651
  // src/participant/ParticipantMessageBubble.tsx
1355
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1356
- function describeItem(item) {
1652
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1653
+ function describeItem(item, perspective) {
1357
1654
  if (item.kind === "server") {
1358
1655
  const { message } = item;
1359
1656
  return {
1360
- isMine: isOwnMessage(message),
1657
+ isMine: isOwnMessage(message, perspective),
1361
1658
  authorName: message.authorName ?? void 0,
1362
1659
  text: message.text ?? void 0,
1363
1660
  createdAt: message.createdAt,
@@ -1375,48 +1672,27 @@ function describeItem(item) {
1375
1672
  displayState: item.displayState
1376
1673
  };
1377
1674
  }
1378
- function bubbleClassName(isMine, hasTail) {
1675
+ function bubbleClassName(isMine, hasTail, hasFailed) {
1379
1676
  const classes = ["cv-p-bubble"];
1380
1677
  if (isMine) classes.push("cv-p-bubble--mine");
1678
+ if (hasFailed) classes.push("cv-p-bubble--failed");
1381
1679
  if (!hasTail) classes.push("cv-p-bubble--no-tail");
1382
1680
  return classes.join(" ");
1383
1681
  }
1384
- function ParticipantMessageBubble({
1385
- item,
1386
- labels,
1387
- confirmsRead,
1388
- resolveAttachmentUrl,
1389
- onRetry,
1390
- onDiscard,
1391
- onEdit,
1392
- avatar,
1393
- tail = true
1394
- }) {
1395
- const content = describeItem(item);
1396
- const { isMine } = content;
1397
- const ownStatus = isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1398
- const bubble = /* @__PURE__ */ jsxs13("div", { className: bubbleClassName(isMine, tail), children: [
1399
- 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,
1400
- content.text ? /* @__PURE__ */ jsx18(ParticipantMessageText, { text: content.text, labels }) : null,
1401
- content.attachments.map((attachment) => /* @__PURE__ */ jsx18(
1402
- ParticipantAttachmentItem,
1403
- {
1404
- attachment,
1405
- resolveAttachmentUrl,
1406
- labels
1407
- },
1408
- attachment.id
1409
- )),
1410
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx18("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1411
- /* @__PURE__ */ jsxs13("span", { className: "cv-p-bubble__meta", children: [
1412
- /* @__PURE__ */ jsx18("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1413
- ownStatus ? /* @__PURE__ */ jsx18(OwnStatus, { status: ownStatus, labels, onRetry }) : null
1414
- ] }),
1415
- ownStatus === "failed" ? /* @__PURE__ */ jsx18(FailedActions, { labels, onDiscard, onEdit }) : null
1682
+ function ParticipantMessageBubble(props) {
1683
+ const { item, labels, confirmsRead, onRetry, avatar, tail = true, perspective } = props;
1684
+ const textId = useId3();
1685
+ const content = describeItem(item, perspective);
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 })
1416
1692
  ] });
1417
- if (avatar === void 0 || isMine) return bubble;
1418
- return /* @__PURE__ */ jsxs13("div", { className: "cv-p-bubble-row", children: [
1419
- /* @__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 }),
1420
1696
  bubble
1421
1697
  ] });
1422
1698
  }
@@ -1438,16 +1714,16 @@ function resolveParticipantBubbleActions(item, handlers) {
1438
1714
  }
1439
1715
 
1440
1716
  // src/participant/ParticipantTimeline.tsx
1441
- import { jsx as jsx19 } from "react/jsx-runtime";
1717
+ import { jsx as jsx22 } from "react/jsx-runtime";
1442
1718
  function createdAtOf2(item) {
1443
1719
  return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1444
1720
  }
1445
1721
  function keyOf(item) {
1446
1722
  return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
1447
1723
  }
1448
- function authorOf(item) {
1724
+ function authorOf(item, perspective) {
1449
1725
  if (item.kind === "pending") return { isMine: true, author: null };
1450
- return { isMine: isOwnMessage(item.message), author: item.message.authorName ?? null };
1726
+ return { isMine: isOwnMessage(item.message, perspective), author: item.message.authorName ?? null };
1451
1727
  }
1452
1728
  function resolveActions(item, pendingActions) {
1453
1729
  if (!pendingActions || item.kind === "server") return {};
@@ -1455,7 +1731,7 @@ function resolveActions(item, pendingActions) {
1455
1731
  }
1456
1732
  function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1457
1733
  if (!isEnabled || isMine) return void 0;
1458
- return showAvatar ? /* @__PURE__ */ jsx19(ParticipantAuthorAvatar, { name: author, render }) : null;
1734
+ return showAvatar ? /* @__PURE__ */ jsx22(ParticipantAuthorAvatar, { name: author, render }) : null;
1459
1735
  }
1460
1736
  function ParticipantTimeline(props) {
1461
1737
  const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
@@ -1467,11 +1743,11 @@ function ParticipantTimeline(props) {
1467
1743
  for (const item of items) {
1468
1744
  const day = new Date(createdAtOf2(item));
1469
1745
  if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1470
- nodes.push(/* @__PURE__ */ jsx19(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1746
+ nodes.push(/* @__PURE__ */ jsx22(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1471
1747
  previousAuthor = void 0;
1472
1748
  }
1473
1749
  previousDay = day;
1474
- const { isMine, author } = authorOf(item);
1750
+ const { isMine, author } = authorOf(item, props.perspective);
1475
1751
  const showAvatar = shouldShowAvatar({ previousAuthor, author, isMine });
1476
1752
  previousAuthor = isMine ? void 0 : author;
1477
1753
  const avatar = resolveAvatarSlot({
@@ -1482,7 +1758,7 @@ function ParticipantTimeline(props) {
1482
1758
  render: props.renderAuthorAvatar
1483
1759
  });
1484
1760
  nodes.push(
1485
- /* @__PURE__ */ jsx19(
1761
+ /* @__PURE__ */ jsx22(
1486
1762
  ParticipantMessageBubble,
1487
1763
  {
1488
1764
  item,
@@ -1490,6 +1766,7 @@ function ParticipantTimeline(props) {
1490
1766
  confirmsRead,
1491
1767
  resolveAttachmentUrl,
1492
1768
  tail: props.tail,
1769
+ perspective: props.perspective,
1493
1770
  avatar,
1494
1771
  ...resolveActions(item, pendingActions)
1495
1772
  },
@@ -1497,19 +1774,19 @@ function ParticipantTimeline(props) {
1497
1774
  )
1498
1775
  );
1499
1776
  }
1500
- return /* @__PURE__ */ jsx19("div", { className: "cv-p-thread__messages", children: nodes });
1777
+ return /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__messages", children: nodes });
1501
1778
  }
1502
1779
 
1503
1780
  // src/participant/ParticipantConversationAvatar.tsx
1504
- import { jsx as jsx20 } from "react/jsx-runtime";
1781
+ import { jsx as jsx23 } from "react/jsx-runtime";
1505
1782
  function ParticipantConversationAvatar({ icon }) {
1506
- 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 });
1507
1784
  }
1508
1785
 
1509
1786
  // src/participant/ParticipantProtocolBadge.tsx
1510
1787
  import { Check as Check2, Copy as Copy2 } from "lucide-react";
1511
- import { useEffect as useEffect5, useReducer as useReducer3 } from "react";
1512
- 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";
1513
1790
  var COPIED_ANNOUNCEMENT_MS = 3e3;
1514
1791
  function ParticipantProtocolBadgeView({
1515
1792
  protocol,
@@ -1518,36 +1795,37 @@ function ParticipantProtocolBadgeView({
1518
1795
  onCopy
1519
1796
  }) {
1520
1797
  const { isCopied, copyCount } = copyState;
1521
- return /* @__PURE__ */ jsxs14(Fragment4, { children: [
1522
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1523
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol__code", children: [
1524
- /* @__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: [
1525
1802
  labels.protocolPrefix,
1526
1803
  " "
1527
1804
  ] }),
1528
1805
  protocol
1529
1806
  ] }),
1530
- /* @__PURE__ */ jsxs14(
1807
+ /* @__PURE__ */ jsxs17(
1531
1808
  "button",
1532
1809
  {
1533
1810
  type: "button",
1534
1811
  className: "cv-p-protocol__copy",
1535
1812
  "aria-label": `${labels.copyProtocol} ${protocol}`,
1813
+ "data-cv-tooltip": labels.copyProtocol,
1536
1814
  onClick: onCopy,
1537
1815
  children: [
1538
- isCopied ? /* @__PURE__ */ jsx21(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx21(Copy2, { size: 18, "aria-hidden": true }),
1539
- 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
1540
1818
  ]
1541
1819
  }
1542
1820
  )
1543
1821
  ] }),
1544
- /* @__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 })
1545
1823
  ] });
1546
1824
  }
1547
1825
  function ParticipantProtocolBadge({ protocol, labels }) {
1548
- const [copyState, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1826
+ const [copyState, dispatch] = useReducer4(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1549
1827
  const { isCopied, copyCount } = copyState;
1550
- useEffect5(() => {
1828
+ useEffect7(() => {
1551
1829
  if (!isCopied) return void 0;
1552
1830
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
1553
1831
  return () => clearTimeout(timer);
@@ -1556,7 +1834,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1556
1834
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1557
1835
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
1558
1836
  }
1559
- return /* @__PURE__ */ jsx21(
1837
+ return /* @__PURE__ */ jsx24(
1560
1838
  ParticipantProtocolBadgeView,
1561
1839
  {
1562
1840
  protocol,
@@ -1568,19 +1846,19 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1568
1846
  }
1569
1847
 
1570
1848
  // src/participant/ParticipantThreadHeader.tsx
1571
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
1849
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1572
1850
  function Title({ conversation, labels, onOpenSubject }) {
1573
1851
  const { subjectType, subjectId, subjectLabel } = conversation;
1574
- 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(
1575
1853
  "button",
1576
1854
  {
1577
1855
  type: "button",
1578
1856
  className: "cv-p-thread__title-button",
1579
1857
  "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1580
1858
  onClick: () => onOpenSubject({ subjectType, subjectId }),
1581
- 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 })
1582
1860
  }
1583
- ) : /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1861
+ ) : /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1584
1862
  }
1585
1863
  function ParticipantThreadHeader({
1586
1864
  conversation,
@@ -1588,33 +1866,37 @@ function ParticipantThreadHeader({
1588
1866
  subjectGroups,
1589
1867
  renderSubjectIcon,
1590
1868
  onBack,
1591
- onOpenSubject
1869
+ onOpenSubject,
1870
+ counterpartLabel,
1871
+ actions
1592
1872
  }) {
1593
1873
  const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1594
1874
  const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1595
1875
  const hasChannels = resolveParticipantChannels(conversation.channels).length > 0;
1596
- return /* @__PURE__ */ jsxs15("header", { className: "cv-p-thread__header", children: [
1597
- onBack ? /* @__PURE__ */ jsx22(ParticipantBackButton, { label: labels.back, onBack }) : null,
1598
- icon !== void 0 ? /* @__PURE__ */ jsx22(ParticipantConversationAvatar, { icon }) : null,
1599
- /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__heading", children: [
1600
- icon === void 0 && group ? /* @__PURE__ */ jsx22("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1601
- /* @__PURE__ */ jsx22(Title, { conversation, labels, onOpenSubject }),
1602
- conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__meta", children: [
1603
- conversation.protocol ? /* @__PURE__ */ jsx22(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1604
- /* @__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 })
1605
1886
  ] }) : null
1606
- ] })
1887
+ ] }),
1888
+ actions ? /* @__PURE__ */ jsx25("div", { className: "cv-p-thread__actions", children: actions }) : null
1607
1889
  ] });
1608
1890
  }
1609
1891
 
1610
1892
  // src/participant/ParticipantThread.tsx
1611
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
1893
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
1612
1894
  function Footer({ props }) {
1613
1895
  const { conversation, labels, draft } = props;
1614
1896
  if (conversation.status === "closed") {
1615
- 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 });
1616
1898
  }
1617
- return /* @__PURE__ */ jsx23(
1899
+ return /* @__PURE__ */ jsx26(
1618
1900
  ParticipantComposer,
1619
1901
  {
1620
1902
  value: draft.value,
@@ -1631,29 +1913,36 @@ function Footer({ props }) {
1631
1913
  }
1632
1914
  );
1633
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
+ }
1634
1931
  function ThreadBody(props) {
1635
1932
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
1636
1933
  const subjectCard = renderSubjectCard?.(conversation);
1637
1934
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
1638
- return /* @__PURE__ */ jsxs16("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
1639
- /* @__PURE__ */ jsx23(
1640
- ParticipantThreadHeader,
1641
- {
1642
- conversation,
1643
- labels,
1644
- subjectGroups: props.subjectGroups,
1645
- renderSubjectIcon: props.renderSubjectIcon,
1646
- onBack: props.onBack,
1647
- onOpenSubject: props.onOpenSubject
1648
- }
1649
- ),
1650
- subjectCard ? /* @__PURE__ */ jsx23("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1651
- /* @__PURE__ */ jsxs16("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1652
- loadView.isLoading ? /* @__PURE__ */ jsx23(ParticipantLoading, { labels }) : null,
1653
- loadView.hasError ? /* @__PURE__ */ jsx23(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1654
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx23("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1655
- /* @__PURE__ */ jsx23(ParticipantTimeline, { ...props }),
1656
- /* @__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(
1657
1946
  ParticipantScrollToLatest,
1658
1947
  {
1659
1948
  label: labels.scrollToLatest,
@@ -1664,14 +1953,20 @@ function ThreadBody(props) {
1664
1953
  }
1665
1954
  )
1666
1955
  ] }),
1667
- /* @__PURE__ */ jsx23(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1668
- /* @__PURE__ */ jsx23(Footer, { props })
1956
+ /* @__PURE__ */ jsx26(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1957
+ /* @__PURE__ */ jsx26(Footer, { props })
1669
1958
  ] });
1670
1959
  }
1671
1960
  function ParticipantThread(props) {
1672
- 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
+ ] });
1673
1965
  }
1674
1966
 
1967
+ // src/participant/useParticipantThreadController.ts
1968
+ import { useMemo as useMemo4 } from "react";
1969
+
1675
1970
  // src/participant/bindAttachmentUrlResolver.ts
1676
1971
  function bindAttachmentUrlResolver(api) {
1677
1972
  return (attachment, disposition) => api.resolveAttachmentUrl(attachment, disposition);
@@ -1753,8 +2048,8 @@ function appendEntryToDraft(entry, current) {
1753
2048
  return { text, files: [...entry.files, ...current.files] };
1754
2049
  }
1755
2050
 
1756
- // src/participant/useParticipantDraft.ts
1757
- import { useState as useState3 } from "react";
2051
+ // src/participant/useParticipantConversationView.ts
2052
+ import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
1758
2053
 
1759
2054
  // src/participant/participantDrafts.ts
1760
2055
  var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
@@ -1782,30 +2077,6 @@ function takeDraftForSend(drafts, subject) {
1782
2077
  };
1783
2078
  }
1784
2079
 
1785
- // src/participant/useParticipantDraft.ts
1786
- function useParticipantDraft(draftsRef, subject) {
1787
- const [draft, setDraftState] = useState3(() => getDraft(draftsRef.current, subject));
1788
- function replaceDrafts(next) {
1789
- draftsRef.current = next;
1790
- setDraftState(getDraft(next, subject));
1791
- }
1792
- function update(change) {
1793
- replaceDrafts(setDraft(draftsRef.current, subject, { ...getDraft(draftsRef.current, subject), ...change }));
1794
- }
1795
- function takeForSend() {
1796
- const taken = takeDraftForSend(draftsRef.current, subject);
1797
- if (taken.content) replaceDrafts(taken.drafts);
1798
- return taken.content;
1799
- }
1800
- function restoreFromEntry(entry) {
1801
- update(appendEntryToDraft(entry, getDraft(draftsRef.current, subject)));
1802
- }
1803
- return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
1804
- }
1805
-
1806
- // src/participant/useParticipantConversationView.ts
1807
- import { useCallback as useCallback4, useMemo as useMemo3 } from "react";
1808
-
1809
2080
  // src/participant/participantSendStates.ts
1810
2081
  var NO_ENTRIES = [];
1811
2082
  function selectSendEntries(states, subject) {
@@ -1822,7 +2093,7 @@ function participantSendStatesReducer(states, { subject, action }) {
1822
2093
  }
1823
2094
 
1824
2095
  // src/participant/useParticipantThread.ts
1825
- import { useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo2, useReducer as useReducer4, useRef as useRef6 } from "react";
2096
+ import { useCallback as useCallback4, useEffect as useEffect11, useMemo as useMemo2, useReducer as useReducer5, useRef as useRef9 } from "react";
1826
2097
 
1827
2098
  // src/participant/participantThreadState.ts
1828
2099
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -1858,7 +2129,7 @@ function participantThreadReducer(state, action) {
1858
2129
  }
1859
2130
 
1860
2131
  // src/participant/useParticipantMarkRead.ts
1861
- import { useEffect as useEffect7 } from "react";
2132
+ import { useEffect as useEffect9 } from "react";
1862
2133
 
1863
2134
  // src/participant/shouldMarkRead.ts
1864
2135
  function shouldMarkParticipantRead(params) {
@@ -1870,12 +2141,12 @@ function shouldMarkParticipantRead(params) {
1870
2141
  }
1871
2142
 
1872
2143
  // src/participant/useDocumentVisibility.ts
1873
- import { useEffect as useEffect6, useState as useState4 } from "react";
2144
+ import { useEffect as useEffect8, useState as useState4 } from "react";
1874
2145
  function useDocumentVisibility() {
1875
2146
  const [visibilityState, setVisibilityState] = useState4(
1876
2147
  () => typeof document === "undefined" ? "visible" : document.visibilityState
1877
2148
  );
1878
- useEffect6(() => {
2149
+ useEffect8(() => {
1879
2150
  function handleVisibilityChange() {
1880
2151
  setVisibilityState(document.visibilityState);
1881
2152
  }
@@ -1890,18 +2161,18 @@ function useDocumentVisibility() {
1890
2161
  function useParticipantMarkRead(params) {
1891
2162
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
1892
2163
  const visibilityState = useDocumentVisibility();
1893
- useEffect7(() => {
2164
+ useEffect9(() => {
1894
2165
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
1895
2166
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
1896
2167
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
1897
2168
  }
1898
2169
 
1899
2170
  // src/participant/useParticipantRevalidation.ts
1900
- import { useEffect as useEffect8, useRef as useRef4 } from "react";
2171
+ import { useEffect as useEffect10, useRef as useRef6 } from "react";
1901
2172
  function useParticipantRevalidation(api, onRevalidate) {
1902
- const handlerRef = useRef4(onRevalidate);
2173
+ const handlerRef = useRef6(onRevalidate);
1903
2174
  handlerRef.current = onRevalidate;
1904
- useEffect8(() => {
2175
+ useEffect10(() => {
1905
2176
  function handleFocus() {
1906
2177
  handlerRef.current();
1907
2178
  }
@@ -1928,10 +2199,10 @@ function useParticipantRevalidation(api, onRevalidate) {
1928
2199
  }
1929
2200
 
1930
2201
  // src/participant/useParticipantSend.ts
1931
- import { useCallback as useCallback2, useRef as useRef5 } from "react";
2202
+ import { useCallback as useCallback2, useRef as useRef7 } from "react";
1932
2203
  function useParticipantSend(params) {
1933
2204
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
1934
- const entriesRef = useRef5(params.entries);
2205
+ const entriesRef = useRef7(params.entries);
1935
2206
  entriesRef.current = params.entries;
1936
2207
  const deliver = useCallback2(
1937
2208
  async (entry) => {
@@ -1982,6 +2253,24 @@ function useParticipantSend(params) {
1982
2253
  return { send, retry };
1983
2254
  }
1984
2255
 
2256
+ // src/participant/useParticipantUnread.ts
2257
+ import { useCallback as useCallback3, useRef as useRef8, useState as useState5 } from "react";
2258
+ function useParticipantUnread(params) {
2259
+ const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2260
+ const [acknowledgedCount, setAcknowledgedCount] = useState5(0);
2261
+ const messagesRef = useRef8(messages);
2262
+ messagesRef.current = messages;
2263
+ const handleMarkedRead = useCallback3(
2264
+ (subject) => {
2265
+ if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective));
2266
+ onMarkedRead?.(subject);
2267
+ },
2268
+ [perspective, onMarkedRead]
2269
+ );
2270
+ const unreadCount = perspective ? resolveViewerUnread({ messages, perspective, acknowledgedCount }) : reportedUnreadCount;
2271
+ return { unreadCount, handleMarkedRead };
2272
+ }
2273
+
1985
2274
  // src/participant/useParticipantThread.ts
1986
2275
  var THREAD_PAGE_SIZE = 30;
1987
2276
  function describeError(error) {
@@ -1992,15 +2281,15 @@ function isForSubject(event, subject) {
1992
2281
  return event.subject.subjectType === subject.subjectType && event.subject.subjectId === subject.subjectId;
1993
2282
  }
1994
2283
  function useParticipantThread(params) {
1995
- const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params;
2284
+ const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
1996
2285
  const { subjectType, subjectId } = subject;
1997
2286
  const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
1998
- const [state, dispatch] = useReducer4(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
1999
- const apiRef = useRef6(api);
2287
+ const [state, dispatch] = useReducer5(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2288
+ const apiRef = useRef9(api);
2000
2289
  apiRef.current = api;
2001
- const activeSubjectKey = useRef6("");
2290
+ const activeSubjectKey = useRef9("");
2002
2291
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
2003
- const refresh = useCallback3(async () => {
2292
+ const refresh = useCallback4(async () => {
2004
2293
  const requestKey = `${subjectType}:${subjectId}`;
2005
2294
  dispatch({ type: "started" });
2006
2295
  try {
@@ -2013,7 +2302,7 @@ function useParticipantThread(params) {
2013
2302
  }
2014
2303
  }, [stableSubject, subjectType, subjectId]);
2015
2304
  const oldestMessageId = state.messages[0]?.id;
2016
- const loadOlder = useCallback3(async () => {
2305
+ const loadOlder = useCallback4(async () => {
2017
2306
  if (!oldestMessageId) return;
2018
2307
  const requestKey = `${subjectType}:${subjectId}`;
2019
2308
  try {
@@ -2025,9 +2314,9 @@ function useParticipantThread(params) {
2025
2314
  if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
2026
2315
  }
2027
2316
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
2028
- const handleSentMessage = useCallback3((message) => dispatch({ type: "sendConfirmed", message }), []);
2317
+ const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
2029
2318
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
2030
- useEffect9(() => {
2319
+ useEffect11(() => {
2031
2320
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
2032
2321
  dispatch({ type: "reset" });
2033
2322
  void refresh();
@@ -2035,11 +2324,18 @@ function useParticipantThread(params) {
2035
2324
  useParticipantRevalidation(api, (event) => {
2036
2325
  if (isForSubject(event, stableSubject)) void refresh();
2037
2326
  });
2038
- useEffect9(() => {
2327
+ useEffect11(() => {
2039
2328
  if (sendEntries.length === 0) return;
2040
2329
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
2041
2330
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
2042
- useParticipantMarkRead({ apiRef, subject: stableSubject, unreadCount, messageCount: state.messages.length, onMarkedRead });
2331
+ const unread = useParticipantUnread({ messages: state.messages, perspective, reportedUnreadCount: unreadCount, onMarkedRead });
2332
+ useParticipantMarkRead({
2333
+ apiRef,
2334
+ subject: stableSubject,
2335
+ unreadCount: unread.unreadCount,
2336
+ messageCount: state.messages.length,
2337
+ onMarkedRead: unread.handleMarkedRead
2338
+ });
2043
2339
  const localPending = useMemo2(() => toLocalPending(sendEntries), [sendEntries]);
2044
2340
  return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
2045
2341
  }
@@ -2049,7 +2345,7 @@ function useParticipantConversationView(params) {
2049
2345
  const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
2050
2346
  const { subjectType, subjectId } = subject;
2051
2347
  const sendEntries = selectSendEntries(sendStates, subject);
2052
- const dispatchSend = useCallback4(
2348
+ const dispatchSend = useCallback5(
2053
2349
  (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
2054
2350
  [dispatchSendStates, subjectType, subjectId]
2055
2351
  );
@@ -2061,7 +2357,8 @@ function useParticipantConversationView(params) {
2061
2357
  onMarkedRead: params.onMarkedRead,
2062
2358
  sendEntries,
2063
2359
  dispatchSend,
2064
- hostPending: pendingMessages
2360
+ hostPending: pendingMessages,
2361
+ perspective: params.perspective
2065
2362
  });
2066
2363
  const items = useMemo3(
2067
2364
  () => mergeParticipantMessages({
@@ -2075,8 +2372,30 @@ function useParticipantConversationView(params) {
2075
2372
  return { thread, items, sendEntries, dispatchSend };
2076
2373
  }
2077
2374
 
2375
+ // src/participant/useParticipantDraft.ts
2376
+ import { useState as useState6 } from "react";
2377
+ function useParticipantDraft(draftsRef, subject) {
2378
+ const [draft, setDraftState] = useState6(() => getDraft(draftsRef.current, subject));
2379
+ function replaceDrafts(next) {
2380
+ draftsRef.current = next;
2381
+ setDraftState(getDraft(next, subject));
2382
+ }
2383
+ function update(change) {
2384
+ replaceDrafts(setDraft(draftsRef.current, subject, { ...getDraft(draftsRef.current, subject), ...change }));
2385
+ }
2386
+ function takeForSend() {
2387
+ const taken = takeDraftForSend(draftsRef.current, subject);
2388
+ if (taken.content) replaceDrafts(taken.drafts);
2389
+ return taken.content;
2390
+ }
2391
+ function restoreFromEntry(entry) {
2392
+ update(appendEntryToDraft(entry, getDraft(draftsRef.current, subject)));
2393
+ }
2394
+ return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
2395
+ }
2396
+
2078
2397
  // src/participant/useParticipantThreadScroll.ts
2079
- import { useEffect as useEffect10, useRef as useRef8 } from "react";
2398
+ import { useEffect as useEffect12, useRef as useRef11 } from "react";
2080
2399
 
2081
2400
  // src/participant/participantNewMessages.ts
2082
2401
  function collectServerMessageIds(items) {
@@ -2093,17 +2412,17 @@ function latestSeenTime(previousIds, items) {
2093
2412
  }
2094
2413
  return latest;
2095
2414
  }
2096
- function countNewIncomingMessages(previousIds, currentItems) {
2415
+ function countNewIncomingMessages(previousIds, currentItems, perspective) {
2097
2416
  const latestSeen = latestSeenTime(previousIds, currentItems);
2098
2417
  return currentItems.filter(
2099
- (item) => item.kind === "server" && item.message.direction === "outbound" && !previousIds.has(item.message.id) && Date.parse(item.message.createdAt) > latestSeen
2418
+ (item) => item.kind === "server" && !isMessageMine(item.message, perspective) && !previousIds.has(item.message.id) && Date.parse(item.message.createdAt) > latestSeen
2100
2419
  ).length;
2101
2420
  }
2102
2421
 
2103
2422
  // src/participant/useScrollAwayState.ts
2104
- import { useCallback as useCallback5 } from "react";
2423
+ import { useCallback as useCallback6 } from "react";
2105
2424
  function useScrollAwayState(ref, isNearBottom) {
2106
- const scrollToLatest = useCallback5(() => {
2425
+ const scrollToLatest = useCallback6(() => {
2107
2426
  const element = ref.current;
2108
2427
  if (!element) return;
2109
2428
  element.scrollTo(
@@ -2114,7 +2433,7 @@ function useScrollAwayState(ref, isNearBottom) {
2114
2433
  }
2115
2434
 
2116
2435
  // src/participant/useStickToBottom.ts
2117
- import { useCallback as useCallback6, useLayoutEffect as useLayoutEffect2, useRef as useRef7, useState as useState5 } from "react";
2436
+ import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
2118
2437
 
2119
2438
  // src/participant/participantScroll.ts
2120
2439
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -2136,17 +2455,17 @@ function resolveScrollAction(params) {
2136
2455
 
2137
2456
  // src/participant/useStickToBottom.ts
2138
2457
  function useStickToBottom({ firstKey, lastKey }) {
2139
- const ref = useRef7(null);
2140
- const snapshot = useRef7({
2458
+ const ref = useRef10(null);
2459
+ const snapshot = useRef10({
2141
2460
  hasPositioned: false,
2142
2461
  firstKey: void 0,
2143
2462
  lastKey: void 0,
2144
2463
  scrollTop: 0,
2145
2464
  scrollHeight: 0
2146
2465
  });
2147
- const [isNearBottom, setIsNearBottom] = useState5(true);
2148
- const isNearBottomRef = useRef7(true);
2149
- const onScroll = useCallback6((event) => {
2466
+ const [isNearBottom, setIsNearBottom] = useState7(true);
2467
+ const isNearBottomRef = useRef10(true);
2468
+ const onScroll = useCallback7((event) => {
2150
2469
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
2151
2470
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
2152
2471
  snapshot.current.scrollTop = scrollTop;
@@ -2154,7 +2473,7 @@ function useStickToBottom({ firstKey, lastKey }) {
2154
2473
  isNearBottomRef.current = isNear;
2155
2474
  setIsNearBottom(isNear);
2156
2475
  }, []);
2157
- useLayoutEffect2(() => {
2476
+ useLayoutEffect3(() => {
2158
2477
  const element = ref.current;
2159
2478
  const previous = snapshot.current;
2160
2479
  if (!element) return;
@@ -2194,27 +2513,73 @@ function keyOf2(item) {
2194
2513
  if (!item) return void 0;
2195
2514
  return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
2196
2515
  }
2197
- function useParticipantThreadScroll(items) {
2516
+ function useParticipantThreadScroll(items, perspective) {
2198
2517
  const { scroll: base, isNearBottom } = useStickToBottom({
2199
2518
  firstKey: keyOf2(items[0]),
2200
2519
  lastKey: keyOf2(items[items.length - 1])
2201
2520
  });
2202
2521
  const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2203
2522
  const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2204
- const seenIds = useRef8(/* @__PURE__ */ new Set());
2205
- useEffect10(() => {
2523
+ const seenIds = useRef11(/* @__PURE__ */ new Set());
2524
+ useEffect12(() => {
2206
2525
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
2207
2526
  }, [isNearBottom, items]);
2208
- return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) };
2527
+ return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
2528
+ }
2529
+
2530
+ // src/participant/useParticipantThreadController.ts
2531
+ function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending }) {
2532
+ return {
2533
+ retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
2534
+ discardLocal: (clientMessageId) => dispatchSend({ type: "discarded", clientMessageId }),
2535
+ editLocal: handleEdit,
2536
+ ...onRetryPending ? { retryHost: onRetryPending } : {}
2537
+ };
2538
+ }
2539
+ function useParticipantThreadController(params) {
2540
+ const { api, subject, conversation, draftsRef } = params;
2541
+ const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject);
2542
+ const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params);
2543
+ const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
2544
+ const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective);
2545
+ function handleSend() {
2546
+ const content = takeForSend();
2547
+ if (content) void thread.send(content);
2548
+ }
2549
+ function handleEdit(clientMessageId) {
2550
+ const entry = findFailedEntry(sendEntries, clientMessageId);
2551
+ if (!entry) return;
2552
+ restoreFromEntry(entry);
2553
+ dispatchSend({ type: "discarded", clientMessageId });
2554
+ }
2555
+ return {
2556
+ conversation,
2557
+ items,
2558
+ hasMore: thread.hasMore,
2559
+ labels: params.labels,
2560
+ resolveAttachmentUrl,
2561
+ draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
2562
+ onSend: handleSend,
2563
+ status: thread.status,
2564
+ refresh: () => void thread.refresh(),
2565
+ scroll,
2566
+ newMessagesCount,
2567
+ isSending: hasSendingEntry(sendEntries),
2568
+ channel: params.channel,
2569
+ locale: params.locale,
2570
+ onLoadOlder: () => void thread.loadOlder(),
2571
+ pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
2572
+ perspective: params.perspective
2573
+ };
2209
2574
  }
2210
2575
 
2211
2576
  // src/participant/ParticipantThreadScreen.tsx
2212
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2577
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
2213
2578
  function useOpenedConversation(api, subject, isNeeded) {
2214
- const [opened, setOpened] = useState6({ status: "idle" });
2579
+ const [opened, setOpened] = useState8({ status: "idle" });
2215
2580
  const { subjectType, subjectId } = subject;
2216
2581
  const canOpen = api.openConversation !== void 0;
2217
- useEffect11(() => {
2582
+ useEffect13(() => {
2218
2583
  if (!isNeeded || !canOpen) return;
2219
2584
  let isCurrent = true;
2220
2585
  setOpened({ status: "loading" });
@@ -2232,9 +2597,9 @@ function NotFound({
2232
2597
  labels,
2233
2598
  onBack
2234
2599
  }) {
2235
- return /* @__PURE__ */ jsxs17("div", { className: "cv-p cv-p-thread", children: [
2236
- /* @__PURE__ */ jsx24("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx24(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2237
- /* @__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 })
2238
2603
  ] });
2239
2604
  }
2240
2605
  function ParticipantThreadScreen(props) {
@@ -2244,59 +2609,25 @@ function ParticipantThreadScreen(props) {
2244
2609
  );
2245
2610
  const opened = useOpenedConversation(api, selected, listed === void 0);
2246
2611
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
2247
- if (conversation) return /* @__PURE__ */ jsx24(LoadedThread, { ...props, conversation });
2612
+ if (conversation) return /* @__PURE__ */ jsx27(LoadedThread, { ...props, conversation });
2248
2613
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
2249
2614
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
2250
2615
  if (isResolving || api.openConversation === void 0 && isListLoading)
2251
- return /* @__PURE__ */ jsx24("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2252
- 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 });
2253
2618
  }
2254
2619
  function LoadedThread(props) {
2255
- const { api, selected, conversation, draftsRef, inbox } = props;
2256
- const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, selected);
2257
- const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView({
2620
+ const core = useParticipantThreadController({
2258
2621
  ...props,
2259
- subject: selected,
2260
- onMarkedRead: inbox.markSubjectRead
2622
+ subject: props.selected,
2623
+ onMarkedRead: props.inbox.markSubjectRead
2261
2624
  });
2262
- const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
2263
- const { scroll, newMessagesCount } = useParticipantThreadScroll(items);
2264
- function handleSend() {
2265
- const content = takeForSend();
2266
- if (content) void thread.send(content);
2267
- }
2268
- function handleEdit(clientMessageId) {
2269
- const entry = findFailedEntry(sendEntries, clientMessageId);
2270
- if (!entry) return;
2271
- restoreFromEntry(entry);
2272
- dispatchSend({ type: "discarded", clientMessageId });
2273
- }
2274
- return /* @__PURE__ */ jsx24(
2625
+ return /* @__PURE__ */ jsx27(
2275
2626
  ParticipantThread,
2276
2627
  {
2277
- conversation,
2278
- items,
2279
- hasMore: thread.hasMore,
2280
- labels: props.labels,
2281
- resolveAttachmentUrl,
2282
- draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
2283
- onSend: handleSend,
2284
- status: thread.status,
2285
- refresh: () => void thread.refresh(),
2286
- scroll,
2287
- newMessagesCount,
2288
- isSending: hasSendingEntry(sendEntries),
2289
- channel: props.channel,
2290
- locale: props.locale,
2628
+ ...core,
2291
2629
  onBack: props.onBack,
2292
2630
  onOpenSubject: props.onOpenSubject,
2293
- onLoadOlder: () => void thread.loadOlder(),
2294
- pendingActions: {
2295
- retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
2296
- discardLocal: (clientMessageId) => dispatchSend({ type: "discarded", clientMessageId }),
2297
- editLocal: handleEdit,
2298
- ...props.onRetryPending ? { retryHost: props.onRetryPending } : {}
2299
- },
2300
2631
  quickReplies: props.quickReplies,
2301
2632
  renderSubjectCard: props.renderSubjectCard,
2302
2633
  subjectGroups: props.subjectGroups,
@@ -2429,14 +2760,14 @@ function buildParticipantThemeStyle(theme) {
2429
2760
  }
2430
2761
 
2431
2762
  // src/participant/ParticipantConversationsScreen.tsx
2432
- import { jsx as jsx25 } from "react/jsx-runtime";
2763
+ import { jsx as jsx28 } from "react/jsx-runtime";
2433
2764
  var ALL_FILTER3 = "all";
2434
2765
  function ParticipantConversationsScreen(props) {
2435
2766
  const { selected, inbox, subjectGroups, classNames } = props;
2436
- const [filter, setFilter] = useState7(ALL_FILTER3);
2437
- const [searchQuery, setSearchQuery] = useState7("");
2438
- const draftsRef = useRef9(/* @__PURE__ */ new Map());
2439
- 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());
2440
2771
  const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2441
2772
  const view = useMemo5(
2442
2773
  () => groupParticipantConversations({
@@ -2449,7 +2780,7 @@ function ParticipantConversationsScreen(props) {
2449
2780
  );
2450
2781
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
2451
2782
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
2452
- 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(
2453
2784
  ParticipantInbox,
2454
2785
  {
2455
2786
  view,
@@ -2466,7 +2797,7 @@ function ParticipantConversationsScreen(props) {
2466
2797
  renderSubjectIcon: props.renderSubjectIcon,
2467
2798
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
2468
2799
  }
2469
- ) }) : /* @__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(
2470
2801
  ParticipantThreadScreen,
2471
2802
  {
2472
2803
  ...props,
@@ -2481,7 +2812,7 @@ function ParticipantConversationsScreen(props) {
2481
2812
  }
2482
2813
 
2483
2814
  // src/participant/useParticipantInbox.ts
2484
- import { useCallback as useCallback7, useEffect as useEffect12, useReducer as useReducer6 } from "react";
2815
+ import { useCallback as useCallback8, useEffect as useEffect14, useReducer as useReducer7 } from "react";
2485
2816
 
2486
2817
  // src/participant/participantInboxState.ts
2487
2818
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -2531,8 +2862,8 @@ function describeError2(error) {
2531
2862
  return error instanceof Error ? error.message : "unknown error";
2532
2863
  }
2533
2864
  function useParticipantInbox(api) {
2534
- const [state, dispatch] = useReducer6(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2535
- const refresh = useCallback7(async () => {
2865
+ const [state, dispatch] = useReducer7(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2866
+ const refresh = useCallback8(async () => {
2536
2867
  dispatch({ type: "started" });
2537
2868
  try {
2538
2869
  const page = await api.listConversations();
@@ -2542,7 +2873,7 @@ function useParticipantInbox(api) {
2542
2873
  }
2543
2874
  }, [api]);
2544
2875
  const { nextCursor } = state;
2545
- const loadMore = useCallback7(async () => {
2876
+ const loadMore = useCallback8(async () => {
2546
2877
  if (!nextCursor) return;
2547
2878
  try {
2548
2879
  const page = await api.listConversations({ cursor: nextCursor });
@@ -2551,10 +2882,10 @@ function useParticipantInbox(api) {
2551
2882
  dispatch({ type: "failed", error: describeError2(error) });
2552
2883
  }
2553
2884
  }, [api, nextCursor]);
2554
- const markSubjectRead = useCallback7((subject) => {
2885
+ const markSubjectRead = useCallback8((subject) => {
2555
2886
  dispatch({ type: "markedRead", subject });
2556
2887
  }, []);
2557
- useEffect12(() => {
2888
+ useEffect14(() => {
2558
2889
  void refresh();
2559
2890
  }, [refresh]);
2560
2891
  useParticipantRevalidation(api, () => {
@@ -2564,11 +2895,11 @@ function useParticipantInbox(api) {
2564
2895
  }
2565
2896
 
2566
2897
  // src/participant/ParticipantConversations.tsx
2567
- import { jsx as jsx26 } from "react/jsx-runtime";
2898
+ import { jsx as jsx29 } from "react/jsx-runtime";
2568
2899
  function ParticipantConversations(props) {
2569
2900
  const inbox = useParticipantInbox(props.api);
2570
2901
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
2571
- return /* @__PURE__ */ jsx26(
2902
+ return /* @__PURE__ */ jsx29(
2572
2903
  ParticipantConversationsScreen,
2573
2904
  {
2574
2905
  ...props,
@@ -2576,7 +2907,111 @@ function ParticipantConversations(props) {
2576
2907
  }
2577
2908
  );
2578
2909
  }
2910
+
2911
+ // src/participant/ConversationThread.tsx
2912
+ import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer8, useRef as useRef13 } from "react";
2913
+
2914
+ // src/participant/conversationThreadApi.ts
2915
+ function toParticipantApi(api) {
2916
+ const { subscribe } = api;
2917
+ return {
2918
+ listConversations: async () => ({ data: [] }),
2919
+ fetchMessages: (subject, params) => api.fetchMessages(subject, params),
2920
+ sendMessage: (input) => api.sendMessage(input),
2921
+ markRead: async (subject) => {
2922
+ if (!api.markRead) throw new Error("The host api has no markRead");
2923
+ await api.markRead(subject);
2924
+ },
2925
+ resolveAttachmentUrl: (attachment, disposition) => api.resolveAttachmentUrl(attachment, disposition),
2926
+ ...subscribe ? { subscribe: (listener) => api.subscribe?.(listener) ?? (() => void 0) } : {}
2927
+ };
2928
+ }
2929
+
2930
+ // src/participant/conversationThreadSummary.ts
2931
+ function buildThreadSummary({
2932
+ subject,
2933
+ title,
2934
+ protocol,
2935
+ channels,
2936
+ status
2937
+ }) {
2938
+ return {
2939
+ subjectType: subject.subjectType,
2940
+ subjectId: subject.subjectId,
2941
+ subjectLabel: title,
2942
+ lastMessageAt: null,
2943
+ unreadCount: 0,
2944
+ awaitingParticipant: false,
2945
+ status: status ?? "open",
2946
+ ...protocol ? { protocol } : {},
2947
+ ...channels ? { channels } : {}
2948
+ };
2949
+ }
2950
+
2951
+ // src/participant/ConversationThread.tsx
2952
+ import { jsx as jsx30 } from "react/jsx-runtime";
2953
+ function useConversationThreadCore(props) {
2954
+ const { perspective = "participant" } = props;
2955
+ const { subjectType, subjectId } = props.subject;
2956
+ const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2957
+ const onMarkedReadRef = useRef13(props.onMarkedRead);
2958
+ onMarkedReadRef.current = props.onMarkedRead;
2959
+ const handleMarkedRead = useCallback9((marked) => onMarkedReadRef.current?.(marked), []);
2960
+ const api = useMemo6(() => toParticipantApi(props.api), [props.api]);
2961
+ const draftsRef = useRef13(/* @__PURE__ */ new Map());
2962
+ const [sendStates, dispatchSendStates] = useReducer8(participantSendStatesReducer, /* @__PURE__ */ new Map());
2963
+ const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2964
+ const { title, protocol, channels, status } = props;
2965
+ const conversation = useMemo6(
2966
+ () => buildThreadSummary({ subject, title, protocol, channels, status }),
2967
+ [subject, title, protocol, channels, status]
2968
+ );
2969
+ return useParticipantThreadController({
2970
+ api,
2971
+ subject,
2972
+ conversation,
2973
+ labels,
2974
+ draftsRef,
2975
+ sendStates,
2976
+ dispatchSendStates,
2977
+ onMarkedRead: handleMarkedRead,
2978
+ channel: props.channel,
2979
+ locale: props.locale,
2980
+ pendingMessages: props.pendingMessages,
2981
+ onRetryPending: props.onRetryPending,
2982
+ perspective
2983
+ });
2984
+ }
2985
+ function ConversationThreadBody(props) {
2986
+ const core = useConversationThreadCore(props);
2987
+ return /* @__PURE__ */ jsx30(
2988
+ "div",
2989
+ {
2990
+ className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2991
+ style: buildParticipantThemeStyle(props.theme),
2992
+ children: /* @__PURE__ */ jsx30("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx30(
2993
+ ParticipantThread,
2994
+ {
2995
+ ...core,
2996
+ onBack: props.onBack,
2997
+ quickReplies: props.quickReplies,
2998
+ subjectGroups: props.subjectGroups,
2999
+ renderSubjectIcon: props.renderSubjectIcon,
3000
+ avatars: props.avatars,
3001
+ renderAuthorAvatar: props.renderAuthorAvatar,
3002
+ tail: props.tail,
3003
+ counterpartLabel: props.counterpartLabel,
3004
+ headerActions: props.headerActions
3005
+ }
3006
+ ) })
3007
+ }
3008
+ );
3009
+ }
3010
+ function ConversationThread(props) {
3011
+ return /* @__PURE__ */ jsx30(ConversationThreadBody, { ...props }, draftKey(props.subject));
3012
+ }
2579
3013
  export {
3014
+ ConversationThread,
2580
3015
  DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
2581
3016
  ParticipantConversations,
2582
3017
  groupParticipantConversations