@adatechnology/conversations-ui 0.6.1 → 0.8.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 (119) hide show
  1. package/README.md +221 -19
  2. package/dist/{ConversationSimulatorPanel-CrfFjiWh.d.ts → ConversationSimulatorPanel-q-wVgBCN.d.ts} +1 -1
  3. package/dist/{chunk-HOKNKXIT.js → chunk-HMCOTZAX.js} +1 -1
  4. package/dist/{chunk-FAQLC3TX.js → chunk-L3OFAQPS.js} +2 -4
  5. package/dist/{chunk-CBYEHPX6.js → chunk-PJNR6UMN.js} +63 -1
  6. package/dist/flows/index.js +3 -5
  7. package/dist/index.d.ts +4 -4
  8. package/dist/index.js +834 -764
  9. package/dist/participant/index.d.ts +62 -3
  10. package/dist/participant/index.js +1444 -394
  11. package/dist/preview/index.d.ts +3 -3
  12. package/dist/preview/index.js +2 -3
  13. package/dist/{quickReply.types-B4gmZsSr.d.ts → quickReply.types-DzWH3NYV.d.ts} +1 -1
  14. package/dist/styles.css +629 -20
  15. package/package.json +3 -3
  16. package/src/participant/ConversationThread.test.tsx +165 -0
  17. package/src/participant/ConversationThread.tsx +117 -0
  18. package/src/participant/ParticipantAuthorAvatar.test.tsx +30 -0
  19. package/src/participant/ParticipantAuthorAvatar.tsx +24 -0
  20. package/src/participant/ParticipantBackButton.tsx +14 -0
  21. package/src/participant/ParticipantBubbleStatus.tsx +67 -0
  22. package/src/participant/ParticipantChannelBadges.tsx +13 -4
  23. package/src/participant/ParticipantComposer.tsx +8 -40
  24. package/src/participant/ParticipantComposerControls.tsx +83 -0
  25. package/src/participant/ParticipantConversationAvatar.tsx +13 -0
  26. package/src/participant/ParticipantConversations.tsx +7 -0
  27. package/src/participant/ParticipantConversationsOptions.test.tsx +87 -0
  28. package/src/participant/ParticipantCopyAnnouncer.tsx +48 -0
  29. package/src/participant/ParticipantCopyableToken.tsx +51 -0
  30. package/src/participant/ParticipantDayDivider.tsx +18 -0
  31. package/src/participant/ParticipantInbox.tsx +3 -1
  32. package/src/participant/ParticipantInboxRow.tsx +6 -4
  33. package/src/participant/ParticipantMessageBubble.test.tsx +52 -5
  34. package/src/participant/ParticipantMessageBubble.tsx +32 -84
  35. package/src/participant/ParticipantMessageNodes.tsx +54 -0
  36. package/src/participant/ParticipantMessageText.test.tsx +106 -0
  37. package/src/participant/ParticipantMessageText.tsx +52 -0
  38. package/src/participant/ParticipantProtocolBadge.tsx +15 -7
  39. package/src/participant/ParticipantProtocolView.test.tsx +4 -6
  40. package/src/participant/ParticipantScrollToLatest.test.tsx +153 -0
  41. package/src/participant/ParticipantScrollToLatest.tsx +40 -0
  42. package/src/participant/ParticipantThread.test.tsx +37 -0
  43. package/src/participant/ParticipantThread.tsx +54 -95
  44. package/src/participant/ParticipantThreadAvatarSequence.test.tsx +65 -0
  45. package/src/participant/ParticipantThreadHeader.test.tsx +181 -0
  46. package/src/participant/ParticipantThreadHeader.tsx +83 -0
  47. package/src/participant/ParticipantThreadScreen.tsx +30 -56
  48. package/src/participant/ParticipantTimeline.tsx +106 -0
  49. package/src/participant/conversationThreadApi.test.ts +27 -0
  50. package/src/participant/conversationThreadApi.ts +24 -0
  51. package/src/participant/conversationThreadCss.test.ts +116 -0
  52. package/src/participant/conversationThreadSummary.ts +34 -0
  53. package/src/participant/index.ts +4 -0
  54. package/src/participant/participantAutoGrow.test.ts +43 -0
  55. package/src/participant/participantAutoGrow.ts +12 -0
  56. package/src/participant/participantAvatar.test.ts +74 -0
  57. package/src/participant/participantAvatar.ts +48 -0
  58. package/src/participant/participantBubbleTail.test.tsx +107 -0
  59. package/src/participant/participantChatDesign.test.tsx +198 -0
  60. package/src/participant/participantContrast.test-helper.ts +116 -0
  61. package/src/participant/participantCopyAnnouncement.test.ts +52 -0
  62. package/src/participant/participantCopyAnnouncement.ts +37 -0
  63. package/src/participant/participantCss.test-helper.ts +34 -0
  64. package/src/participant/participantDay.test.ts +22 -0
  65. package/src/participant/participantDay.ts +8 -0
  66. package/src/participant/participantDocuments.ts +45 -0
  67. package/src/participant/participantGolden.test.tsx +46 -0
  68. package/src/participant/participantGoldenScenarios.test-helper.tsx +57 -0
  69. package/src/participant/participantHeaderA11y.test.tsx +85 -0
  70. package/src/participant/participantHeaderDesign.test.tsx +111 -0
  71. package/src/participant/participantLabels.ts +11 -0
  72. package/src/participant/participantMessageAtoms.ts +53 -0
  73. package/src/participant/participantMessageCopyables.test.ts +141 -0
  74. package/src/participant/participantMessageCopyables.ts +76 -0
  75. package/src/participant/participantMessageFormat.test-helper.ts +21 -0
  76. package/src/participant/participantMessageFormat.test.ts +177 -0
  77. package/src/participant/participantMessageFormat.ts +135 -0
  78. package/src/participant/participantMessageFormat.types.ts +22 -0
  79. package/src/participant/participantMessageInline.test.ts +82 -0
  80. package/src/participant/participantMessageInline.ts +99 -0
  81. package/src/participant/participantMessageLinks.test.ts +100 -0
  82. package/src/participant/participantMessageLinks.ts +54 -0
  83. package/src/participant/participantMessageTextStyle.test.ts +137 -0
  84. package/src/participant/participantMessages.ts +4 -3
  85. package/src/participant/participantNewMessages.test.ts +21 -0
  86. package/src/participant/participantNewMessages.ts +8 -3
  87. package/src/participant/participantPerspective.test.ts +67 -0
  88. package/src/participant/participantPerspective.ts +50 -0
  89. package/src/participant/participantPerspectiveRender.test.tsx +94 -0
  90. package/src/participant/participantScrollFocus.test.ts +22 -0
  91. package/src/participant/participantScrollFocus.ts +10 -0
  92. package/src/participant/participantScrollView.test.ts +74 -0
  93. package/src/participant/participantScrollView.ts +38 -0
  94. package/src/participant/participantScrollbars.test.ts +156 -0
  95. package/src/participant/participantSubjectIcon.test.tsx +27 -0
  96. package/src/participant/participantSubjectIcon.ts +26 -0
  97. package/src/participant/participantThemeContrast.test.ts +98 -0
  98. package/src/participant/test-fixtures/conversations-list.golden.html +1 -0
  99. package/src/participant/test-fixtures/conversations-selected.golden.html +1 -0
  100. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -0
  101. package/src/participant/test-fixtures/thread-closed.golden.html +1 -0
  102. package/src/participant/test-fixtures/thread-default.golden.html +1 -0
  103. package/src/participant/test-fixtures/thread-error.golden.html +1 -0
  104. package/src/participant/test-fixtures/thread-loading.golden.html +1 -0
  105. package/src/participant/test-fixtures/thread-plain.golden.html +1 -0
  106. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -0
  107. package/src/participant/useParticipantConversationView.ts +3 -0
  108. package/src/participant/useParticipantCopyFeedback.ts +33 -0
  109. package/src/participant/useParticipantThread.ts +13 -2
  110. package/src/participant/useParticipantThreadController.ts +112 -0
  111. package/src/participant/useParticipantThreadScroll.ts +13 -3
  112. package/src/participant/useParticipantUnread.test.tsx +40 -0
  113. package/src/participant/useParticipantUnread.ts +38 -0
  114. package/src/participant/useRevealActiveChip.ts +20 -0
  115. package/src/participant/useScrollAwayState.ts +24 -0
  116. package/src/participant/useStickToBottom.ts +16 -2
  117. package/src/styles.css +151 -23
  118. package/dist/chunk-MRVFFJEH.js +0 -64
  119. package/dist/chunk-OVFCLRQQ.js +0 -84
@@ -1,6 +1,3 @@
1
- import {
2
- MessageText
3
- } from "../chunk-OVFCLRQQ.js";
4
1
  import {
5
2
  StatusTicks,
6
3
  channelCapabilityFor,
@@ -8,10 +5,9 @@ import {
8
5
  formatTimestamp,
9
6
  isSameDay
10
7
  } from "../chunk-O6457XHK.js";
11
- import "../chunk-CBYEHPX6.js";
12
8
 
13
9
  // src/participant/ParticipantConversationsScreen.tsx
14
- import { useMemo as useMemo4, useReducer as useReducer3, useRef as useRef7, useState as useState7 } from "react";
10
+ import { useMemo as useMemo5, useReducer as useReducer5, useRef as useRef10, useState as useState8 } from "react";
15
11
 
16
12
  // src/participant/participantLoadView.ts
17
13
  function resolveLoadView({ status, hasItems }) {
@@ -37,6 +33,7 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
37
33
  me: "Me",
38
34
  newMessages: "New messages",
39
35
  loadOlder: "Load older messages",
36
+ scrollToLatest: "Scroll to latest message",
40
37
  closedNotice: "This conversation is closed",
41
38
  messageInputLabel: "Message",
42
39
  messageInputPlaceholder: "Write a message\u2026",
@@ -75,7 +72,10 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
75
72
  channelWhatsapp: "WhatsApp",
76
73
  channelEmail: "Email",
77
74
  channelPortal: "Portal",
78
- channelWebchat: "Web chat"
75
+ channelWebchat: "Web chat",
76
+ externalLinkHint: "opens in a new tab",
77
+ copyValue: "Copy",
78
+ valueCopied: "Copied"
79
79
  };
80
80
  function formatParticipantLabel(template, count) {
81
81
  return template.replace("{count}", String(count));
@@ -110,14 +110,19 @@ var ICONS = {
110
110
  Globe,
111
111
  MessagesSquare
112
112
  };
113
- function BadgeContent({ descriptor, labels, separator }) {
113
+ function BadgeContent({ descriptor, labels, separator, iconSize }) {
114
114
  const Icon = ICONS[descriptor.iconName];
115
115
  return /* @__PURE__ */ jsxs(Fragment, { children: [
116
- /* @__PURE__ */ jsx(Icon, { size: 14, "aria-hidden": true }),
116
+ /* @__PURE__ */ jsx(Icon, { size: iconSize, "aria-hidden": true }),
117
117
  /* @__PURE__ */ jsx("span", { className: "cv-p-sr-only", children: `${resolveParticipantChannelLabel(descriptor, labels)}${separator}` })
118
118
  ] });
119
119
  }
120
- function ParticipantChannelBadges({ channels, labels, variant = "list" }) {
120
+ function ParticipantChannelBadges({
121
+ channels,
122
+ labels,
123
+ variant = "list",
124
+ iconSize = 14
125
+ }) {
121
126
  const descriptors = resolveParticipantChannels(channels);
122
127
  if (descriptors.length === 0) return null;
123
128
  if (variant === "inline") {
@@ -128,12 +133,27 @@ function ParticipantChannelBadges({ channels, labels, variant = "list" }) {
128
133
  {
129
134
  descriptor,
130
135
  labels,
131
- separator: index < descriptors.length - 1 ? ", " : ""
136
+ separator: index < descriptors.length - 1 ? ", " : "",
137
+ iconSize
132
138
  }
133
139
  ) }, descriptor.channel))
134
140
  ] });
135
141
  }
136
- 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: "" }) }, descriptor.channel)) });
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)) });
143
+ }
144
+
145
+ // src/participant/participantSubjectIcon.ts
146
+ function isDrawable(node) {
147
+ return node !== null && node !== void 0 && node !== false && node !== "";
148
+ }
149
+ function resolveConversationIcon({
150
+ conversation,
151
+ group,
152
+ renderSubjectIcon
153
+ }) {
154
+ const custom = renderSubjectIcon?.(conversation);
155
+ if (isDrawable(custom)) return custom;
156
+ return isDrawable(group?.icon) ? group?.icon : void 0;
137
157
  }
138
158
 
139
159
  // src/participant/ParticipantInboxRow.tsx
@@ -144,11 +164,12 @@ function formatRowTime(iso, locale) {
144
164
  return date.toLocaleDateString(locale, { day: "2-digit", month: "2-digit" });
145
165
  }
146
166
  function RowIcon({ props, kind }) {
147
- const custom = props.renderSubjectIcon?.(props.conversation);
148
- if (custom !== null && custom !== void 0 && custom !== false && custom !== "") {
149
- return /* @__PURE__ */ jsx2("span", { className: "cv-p-row__icon cv-p-inbox__icon", "aria-hidden": "true", children: custom });
167
+ const { conversation, group, renderSubjectIcon } = props;
168
+ const hostIcon = renderSubjectIcon?.(conversation);
169
+ if (isDrawable(hostIcon)) {
170
+ return /* @__PURE__ */ jsx2("span", { className: "cv-p-row__icon cv-p-inbox__icon", "aria-hidden": "true", children: hostIcon });
150
171
  }
151
- return /* @__PURE__ */ jsx2("span", { className: "cv-p-row__icon", "aria-hidden": "true", children: props.group?.icon ?? kind.slice(0, 2) });
172
+ return /* @__PURE__ */ jsx2("span", { className: "cv-p-row__icon", "aria-hidden": "true", children: resolveConversationIcon({ conversation, group }) ?? kind.slice(0, 2) });
152
173
  }
153
174
  function ParticipantInboxRow(props) {
154
175
  const { conversation, group, labels, locale, onSelect } = props;
@@ -220,6 +241,41 @@ function ParticipantLoading({ labels }) {
220
241
  ] });
221
242
  }
222
243
 
244
+ // src/participant/useRevealActiveChip.ts
245
+ import { useEffect, useRef } from "react";
246
+
247
+ // src/participant/participantScrollView.ts
248
+ var BADGE_CAP = 99;
249
+ function resolveScrollToLatestView(params) {
250
+ const { isAwayFromBottom, newMessagesCount, hasMessages } = params;
251
+ if (!isAwayFromBottom || !hasMessages) return { isVisible: false, badge: void 0 };
252
+ if (newMessagesCount <= 0) return { isVisible: true, badge: void 0 };
253
+ return { isVisible: true, badge: newMessagesCount > BADGE_CAP ? `${BADGE_CAP}+` : String(newMessagesCount) };
254
+ }
255
+ function scrollToLatestOptions(params) {
256
+ return { top: params.scrollHeight, behavior: params.isReducedMotion ? "auto" : "smooth" };
257
+ }
258
+ function scrollIntoViewOptions(isReducedMotion) {
259
+ return { block: "nearest", inline: "nearest", behavior: isReducedMotion ? "auto" : "smooth" };
260
+ }
261
+ function readPrefersReducedMotion() {
262
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
263
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
264
+ }
265
+
266
+ // src/participant/useRevealActiveChip.ts
267
+ function useRevealActiveChip(active) {
268
+ const strip = useRef(null);
269
+ const previousActive = useRef(void 0);
270
+ useEffect(() => {
271
+ const wasActive = previousActive.current;
272
+ previousActive.current = active;
273
+ if (wasActive === void 0 || wasActive === active) return;
274
+ strip.current?.querySelector('[aria-pressed="true"]')?.scrollIntoView(scrollIntoViewOptions(readPrefersReducedMotion()));
275
+ }, [active]);
276
+ return strip;
277
+ }
278
+
223
279
  // src/participant/ParticipantInbox.tsx
224
280
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
225
281
  var ALL_FILTER = "all";
@@ -234,7 +290,8 @@ function sectionTitle(section, props) {
234
290
  return props.subjectGroups.find((group) => group.subjectType === section.key)?.label ?? section.key;
235
291
  }
236
292
  function Filters({ filters, active, labels, onChange }) {
237
- return /* @__PURE__ */ jsx5("div", { className: "cv-p-filters", role: "group", "aria-label": labels.filtersGroup, children: filters.map((filter) => /* @__PURE__ */ jsxs5(
293
+ const strip = useRevealActiveChip(active);
294
+ return /* @__PURE__ */ jsx5("div", { className: "cv-p-filters", role: "group", "aria-label": labels.filtersGroup, ref: strip, children: filters.map((filter) => /* @__PURE__ */ jsxs5(
238
295
  "button",
239
296
  {
240
297
  type: "button",
@@ -304,10 +361,90 @@ function ParticipantInbox(props) {
304
361
  }
305
362
 
306
363
  // src/participant/ParticipantThreadScreen.tsx
307
- import { useEffect as useEffect8, useMemo as useMemo3, useState as useState6 } from "react";
364
+ import { useEffect as useEffect11, useState as useState7 } from "react";
365
+
366
+ // src/participant/ParticipantBackButton.tsx
367
+ import { ArrowLeft } from "lucide-react";
368
+ import { jsx as jsx6 } from "react/jsx-runtime";
369
+ 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" }) });
371
+ }
308
372
 
309
373
  // src/participant/ParticipantComposer.tsx
310
- import { useRef, useState } from "react";
374
+ import { useState } from "react";
375
+
376
+ // src/participant/ParticipantComposerControls.tsx
377
+ import { useLayoutEffect, useRef as useRef2 } from "react";
378
+ import { Paperclip, Send } from "lucide-react";
379
+
380
+ // src/participant/participantAutoGrow.ts
381
+ function fitTextareaHeight(target) {
382
+ target.style.height = "auto";
383
+ target.style.height = `${target.scrollHeight + (target.offsetHeight - target.clientHeight)}px`;
384
+ }
385
+
386
+ // src/participant/ParticipantComposerControls.tsx
387
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
388
+ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
389
+ const fileInputRef = useRef2(null);
390
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
391
+ /* @__PURE__ */ jsx7(
392
+ "input",
393
+ {
394
+ ref: fileInputRef,
395
+ className: "cv-p-composer__file-input",
396
+ type: "file",
397
+ multiple: true,
398
+ hidden: true,
399
+ accept: acceptedTypes?.join(","),
400
+ onChange: onChosen
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsx7(
404
+ "button",
405
+ {
406
+ type: "button",
407
+ className: "cv-p-composer__attach",
408
+ "aria-label": labels.attach,
409
+ disabled,
410
+ onClick: () => fileInputRef.current?.click(),
411
+ children: /* @__PURE__ */ jsx7(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
412
+ }
413
+ )
414
+ ] });
415
+ }
416
+ function SendButton({ label, disabled }) {
417
+ return /* @__PURE__ */ jsx7(
418
+ "button",
419
+ {
420
+ type: "submit",
421
+ className: "cv-p-button cv-p-button--primary cv-p-composer__send",
422
+ "aria-label": label,
423
+ disabled,
424
+ children: /* @__PURE__ */ jsx7(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
425
+ }
426
+ );
427
+ }
428
+ function MessageField({ value, label, placeholder, disabled, maxLength, onChange }) {
429
+ const fieldRef = useRef2(null);
430
+ useLayoutEffect(() => {
431
+ if (fieldRef.current) fitTextareaHeight(fieldRef.current);
432
+ }, [value]);
433
+ return /* @__PURE__ */ jsx7(
434
+ "textarea",
435
+ {
436
+ ref: fieldRef,
437
+ className: "cv-p-composer__input",
438
+ "aria-label": label,
439
+ placeholder,
440
+ rows: 1,
441
+ value,
442
+ maxLength,
443
+ disabled,
444
+ onChange: (event) => onChange(event.target.value)
445
+ }
446
+ );
447
+ }
311
448
 
312
449
  // src/participant/participantFileValidation.ts
313
450
  function matchesAcceptedType(mimeType, acceptedTypes) {
@@ -342,45 +479,24 @@ function applyQuickReply(currentText, replyText) {
342
479
  }
343
480
 
344
481
  // src/participant/ParticipantComposer.tsx
345
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
482
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
346
483
  function QuickReplyStrip({ quickReplies, labels, onPick }) {
347
- return /* @__PURE__ */ jsx6("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx6("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
484
+ return /* @__PURE__ */ jsx8("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx8("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
348
485
  }
349
486
  function FileList({ files, labels, onRemove }) {
350
487
  if (files.length === 0) return null;
351
- return /* @__PURE__ */ jsx6("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs6("li", { className: "cv-p-files__item", children: [
352
- /* @__PURE__ */ jsx6("span", { className: "cv-p-files__name", children: file.name }),
353
- /* @__PURE__ */ jsx6("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
354
- /* @__PURE__ */ jsxs6("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, onClick: () => onRemove(file), children: [
355
- /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\xD7" }),
356
- /* @__PURE__ */ jsx6("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
488
+ 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
+ /* @__PURE__ */ jsx8("span", { className: "cv-p-files__name", children: file.name }),
490
+ /* @__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: [
492
+ /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\xD7" }),
493
+ /* @__PURE__ */ jsx8("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
357
494
  ] })
358
495
  ] }, `${file.name}-${file.size}-${index}`)) });
359
496
  }
360
497
  function rejectionText(rejection, labels) {
361
498
  return rejection === "tooLarge" ? labels.attachmentTooLarge : labels.attachmentTypeNotAccepted;
362
499
  }
363
- function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
364
- const fileInputRef = useRef(null);
365
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
366
- /* @__PURE__ */ jsx6(
367
- "input",
368
- {
369
- ref: fileInputRef,
370
- className: "cv-p-composer__file-input",
371
- type: "file",
372
- multiple: true,
373
- hidden: true,
374
- accept: acceptedTypes?.join(","),
375
- onChange: onChosen
376
- }
377
- ),
378
- /* @__PURE__ */ jsxs6("button", { type: "button", className: "cv-p-button", disabled, onClick: () => fileInputRef.current?.click(), children: [
379
- /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\uFF0B" }),
380
- /* @__PURE__ */ jsx6("span", { className: "cv-p-sr-only", children: labels.attach })
381
- ] })
382
- ] });
383
- }
384
500
  function useFileSelection(props) {
385
501
  const [rejection, setRejection] = useState(void 0);
386
502
  const { files, onFilesChange, channel } = props;
@@ -406,51 +522,737 @@ function ParticipantComposer(props) {
406
522
  event.preventDefault();
407
523
  if (canSend) onSend();
408
524
  }
409
- return /* @__PURE__ */ jsxs6("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
410
- props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx6(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
411
- /* @__PURE__ */ jsx6(FileList, { files, labels, onRemove: handleRemoveFile }),
412
- rejection ? /* @__PURE__ */ jsx6("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
413
- /* @__PURE__ */ jsxs6("div", { className: "cv-p-composer__row", children: [
414
- channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx6(AttachControl, { labels, disabled, acceptedTypes: props.acceptedTypes, onChosen: handleFilesChosen }) : null,
415
- /* @__PURE__ */ jsx6(
416
- "textarea",
525
+ return /* @__PURE__ */ jsxs7("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
526
+ props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx8(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
527
+ /* @__PURE__ */ jsx8(FileList, { files, labels, onRemove: handleRemoveFile }),
528
+ rejection ? /* @__PURE__ */ jsx8("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
529
+ /* @__PURE__ */ jsxs7("div", { className: "cv-p-composer__row", children: [
530
+ channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx8(AttachControl, { labels, disabled, acceptedTypes: props.acceptedTypes, onChosen: handleFilesChosen }) : null,
531
+ /* @__PURE__ */ jsx8(
532
+ MessageField,
417
533
  {
418
- className: "cv-p-composer__input",
419
- "aria-label": labels.messageInputLabel,
420
- placeholder: labels.messageInputPlaceholder,
421
- rows: 1,
422
534
  value,
423
- maxLength: props.maxLength,
535
+ label: labels.messageInputLabel,
536
+ placeholder: labels.messageInputPlaceholder,
424
537
  disabled,
425
- onChange: (event) => onChange(event.target.value)
538
+ maxLength: props.maxLength,
539
+ onChange
426
540
  }
427
541
  ),
428
- /* @__PURE__ */ jsxs6("button", { type: "submit", className: "cv-p-button cv-p-button--primary", disabled: !canSend, children: [
429
- /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\u27A4" }),
430
- /* @__PURE__ */ jsx6("span", { className: "cv-p-sr-only", children: labels.send })
431
- ] })
542
+ /* @__PURE__ */ jsx8(SendButton, { label: labels.send, disabled: !canSend })
432
543
  ] })
433
544
  ] });
434
545
  }
435
546
 
436
- // src/participant/participantBubbleActions.ts
437
- function bindPendingActions(actions, clientMessageId) {
438
- const { retryHost } = actions;
439
- return {
440
- retryLocal: () => actions.retryLocal(clientMessageId),
441
- discardLocal: () => actions.discardLocal(clientMessageId),
442
- editLocal: () => actions.editLocal(clientMessageId),
443
- ...retryHost ? { retryHost: () => retryHost(clientMessageId) } : {}
547
+ // src/participant/ParticipantCopyAnnouncer.tsx
548
+ import { createContext, useCallback, useContext, useEffect as useEffect2, useReducer, useRef as useRef3 } from "react";
549
+
550
+ // src/participant/participantCopyAnnouncement.ts
551
+ var INITIAL_COPY_ANNOUNCEMENT = { text: "", tick: 0 };
552
+ var REPEAT_MARKER = "\u200B";
553
+ function copyAnnouncementReducer(state, action) {
554
+ if (action.type === "announce") return { text: action.text, tick: state.tick + 1 };
555
+ return { ...state, text: "" };
556
+ }
557
+ function resolveLiveText(params) {
558
+ const { copy, notice, baselineNotice } = params;
559
+ if (copy.text !== "") {
560
+ const marker = copy.tick % 2 === 0 ? REPEAT_MARKER : "";
561
+ return { text: `${copy.text}${marker}`, baselineNotice: baselineNotice ?? notice };
562
+ }
563
+ if (baselineNotice !== void 0 && baselineNotice === notice) return { text: "", baselineNotice };
564
+ return { text: notice, baselineNotice: void 0 };
565
+ }
566
+
567
+ // src/participant/ParticipantCopyAnnouncer.tsx
568
+ import { jsx as jsx9 } from "react/jsx-runtime";
569
+ var ANNOUNCEMENT_MS = 3e3;
570
+ var AnnounceContext = createContext(() => void 0);
571
+ var AnnouncementContext = createContext(INITIAL_COPY_ANNOUNCEMENT);
572
+ function useAnnounceCopy() {
573
+ return useContext(AnnounceContext);
574
+ }
575
+ function ParticipantCopyAnnouncer({ children }) {
576
+ const [announcement, dispatch] = useReducer(copyAnnouncementReducer, INITIAL_COPY_ANNOUNCEMENT);
577
+ const announce = useCallback((text) => dispatch({ type: "announce", text }), []);
578
+ useEffect2(() => {
579
+ if (announcement.text === "") return void 0;
580
+ const timer = setTimeout(() => dispatch({ type: "expired" }), ANNOUNCEMENT_MS);
581
+ return () => clearTimeout(timer);
582
+ }, [announcement.tick, announcement.text]);
583
+ return /* @__PURE__ */ jsx9(AnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx9(AnnouncementContext.Provider, { value: announcement, children }) });
584
+ }
585
+ function ParticipantThreadLive({ notice }) {
586
+ const copy = useContext(AnnouncementContext);
587
+ const baselineNotice = useRef3(void 0);
588
+ const resolution = resolveLiveText({ copy, notice, baselineNotice: baselineNotice.current });
589
+ baselineNotice.current = resolution.baselineNotice;
590
+ return /* @__PURE__ */ jsx9("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: resolution.text });
591
+ }
592
+
593
+ // src/participant/ParticipantScrollToLatest.tsx
594
+ import { ChevronDown } from "lucide-react";
595
+
596
+ // src/participant/participantScrollFocus.ts
597
+ var SCROLLER_SELECTOR = ".cv-p-thread__scroll";
598
+ function focusScrollerOf(button) {
599
+ button.closest(SCROLLER_SELECTOR)?.focus({ preventScroll: true });
600
+ }
601
+
602
+ // src/participant/ParticipantScrollToLatest.tsx
603
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
604
+ function ParticipantScrollToLatest({
605
+ label,
606
+ isAwayFromBottom,
607
+ hasMessages,
608
+ newMessagesCount,
609
+ onClick
610
+ }) {
611
+ const { isVisible, badge } = resolveScrollToLatestView({ isAwayFromBottom, newMessagesCount, hasMessages });
612
+ if (!isVisible || onClick === void 0) return null;
613
+ return /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__latest", children: /* @__PURE__ */ jsxs8(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ className: "cv-p-thread__latest-button",
618
+ "aria-label": label,
619
+ onClick: (event) => {
620
+ focusScrollerOf(event.currentTarget);
621
+ onClick();
622
+ },
623
+ children: [
624
+ /* @__PURE__ */ jsx10(ChevronDown, { size: 24, "aria-hidden": "true", focusable: "false" }),
625
+ badge ? /* @__PURE__ */ jsx10("span", { className: "cv-p-thread__latest-badge", "aria-hidden": "true", children: badge }) : null
626
+ ]
627
+ }
628
+ ) });
629
+ }
630
+
631
+ // src/participant/ParticipantAuthorAvatar.tsx
632
+ import { User } from "lucide-react";
633
+
634
+ // src/participant/participantAvatar.ts
635
+ var NAME_PARTICLES = /* @__PURE__ */ new Set([
636
+ "a",
637
+ "e",
638
+ "o",
639
+ "da",
640
+ "das",
641
+ "de",
642
+ "del",
643
+ "di",
644
+ "do",
645
+ "dos",
646
+ "du",
647
+ "la",
648
+ "le",
649
+ "van",
650
+ "von"
651
+ ]);
652
+ function lettersOf(word) {
653
+ return word.replace(/[^\p{L}\p{M}]/gu, "");
654
+ }
655
+ function firstLetter(word) {
656
+ return (Array.from(word.normalize("NFC"))[0] ?? "").toUpperCase();
657
+ }
658
+ function authorInitials(name) {
659
+ const [first, ...rest] = (name ?? "").split(/\s+/).map(lettersOf).filter(Boolean);
660
+ if (first === void 0) return void 0;
661
+ const last = rest.filter((word) => !NAME_PARTICLES.has(word.toLowerCase())).pop();
662
+ return `${firstLetter(first)}${last === void 0 ? "" : firstLetter(last)}`;
663
+ }
664
+ function shouldShowAvatar({ previousAuthor, author, isMine }) {
665
+ if (isMine) return false;
666
+ if (previousAuthor === void 0) return true;
667
+ return previousAuthor !== author;
668
+ }
669
+
670
+ // src/participant/ParticipantAuthorAvatar.tsx
671
+ import { jsx as jsx11 } from "react/jsx-runtime";
672
+ function ParticipantAuthorAvatar({ name, render }) {
673
+ const fromHost = render?.({ name });
674
+ const hasHostContent = !(fromHost == null || fromHost === false || fromHost === "");
675
+ const initials = authorInitials(name);
676
+ const fallback = initials ?? /* @__PURE__ */ jsx11(User, { size: 16, "aria-hidden": true });
677
+ return /* @__PURE__ */ jsx11("span", { className: "cv-p-avatar", "aria-hidden": "true", children: hasHostContent ? fromHost : fallback });
678
+ }
679
+
680
+ // src/participant/participantDay.ts
681
+ function pad(value) {
682
+ return String(value).padStart(2, "0");
683
+ }
684
+ function toDateTimeAttribute(date) {
685
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
686
+ }
687
+
688
+ // src/participant/ParticipantDayDivider.tsx
689
+ import { jsx as jsx12 } from "react/jsx-runtime";
690
+ function ParticipantDayDivider({ day, locale }) {
691
+ const label = day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" });
692
+ 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
+ }
694
+
695
+ // src/participant/ParticipantBubbleStatus.tsx
696
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
697
+ function statusText(status, labels) {
698
+ const byStatus = {
699
+ sending: labels.statusSending,
700
+ queued: labels.statusQueued,
701
+ sent: labels.statusSent,
702
+ delivered: labels.statusDelivered,
703
+ read: labels.statusRead,
704
+ failed: labels.statusFailed
444
705
  };
706
+ return byStatus[status];
445
707
  }
446
- function resolveParticipantBubbleActions(item, handlers) {
447
- if (item.kind === "server" || item.displayState !== "failed") return {};
448
- if (item.origin === "host") return handlers.retryHost ? { onRetry: handlers.retryHost } : {};
449
- return { onRetry: handlers.retryLocal, onDiscard: handlers.discardLocal, onEdit: handlers.editLocal };
708
+ function OwnStatus({ status, labels, onRetry }) {
709
+ const ticksStatus = status === "sending" ? "queued" : status;
710
+ 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
+ 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 })
721
+ ] });
722
+ }
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
728
+ ] });
729
+ }
730
+
731
+ // src/participant/ParticipantMessageText.tsx
732
+ import { useMemo } from "react";
733
+
734
+ // src/participant/ParticipantCopyableToken.tsx
735
+ import { Check, Copy } from "lucide-react";
736
+
737
+ // src/participant/useParticipantCopyFeedback.ts
738
+ import { useEffect as useEffect3, useReducer as useReducer2 } from "react";
739
+
740
+ // src/participant/participantProtocolCopy.ts
741
+ async function copyProtocolToClipboard(protocol, clipboard) {
742
+ if (clipboard === void 0) return false;
743
+ try {
744
+ await clipboard.writeText(protocol);
745
+ return true;
746
+ } catch {
747
+ return false;
748
+ }
749
+ }
750
+ var INITIAL_PROTOCOL_COPY_STATE = { copyCount: 0, isCopied: false };
751
+ function protocolCopyReducer(state, action) {
752
+ if (action.type === "copied") return { copyCount: state.copyCount + 1, isCopied: true };
753
+ return { ...state, isCopied: false };
754
+ }
755
+
756
+ // src/participant/useParticipantCopyFeedback.ts
757
+ var COPIED_FEEDBACK_MS = 2e3;
758
+ function useParticipantCopyFeedback() {
759
+ const [state, dispatch] = useReducer2(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
760
+ const announce = useAnnounceCopy();
761
+ const { isCopied, copyCount } = state;
762
+ useEffect3(() => {
763
+ if (!isCopied) return void 0;
764
+ const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_FEEDBACK_MS);
765
+ return () => clearTimeout(timer);
766
+ }, [isCopied, copyCount]);
767
+ async function copyValue(value, announcement) {
768
+ const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
769
+ if (!await copyProtocolToClipboard(value, clipboard)) return;
770
+ dispatch({ type: "copied" });
771
+ announce(announcement);
772
+ }
773
+ return { isCopied, copy: (value, announcement) => void copyValue(value, announcement) };
774
+ }
775
+
776
+ // src/participant/ParticipantCopyableToken.tsx
777
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
778
+ function ParticipantCopyableToken({ copyKind, value, labels }) {
779
+ const { isCopied, copy } = useParticipantCopyFeedback();
780
+ return /* @__PURE__ */ jsxs10(
781
+ "button",
782
+ {
783
+ type: "button",
784
+ className: `cv-p-copyable cv-p-copyable--${copyKind}`,
785
+ "aria-label": `${labels.copyValue} ${value}`,
786
+ onClick: () => copy(value, labels.valueCopied),
787
+ children: [
788
+ value,
789
+ isCopied ? /* @__PURE__ */ jsxs10("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
790
+ /* @__PURE__ */ jsx14(Check, { size: 12, "aria-hidden": true }),
791
+ " ",
792
+ labels.valueCopied
793
+ ] }) : null
794
+ ]
795
+ }
796
+ );
797
+ }
798
+ function ParticipantLinkCopyButton({ value, labels }) {
799
+ const { isCopied, copy } = useParticipantCopyFeedback();
800
+ return /* @__PURE__ */ jsx14(
801
+ "button",
802
+ {
803
+ type: "button",
804
+ className: "cv-p-text__link-copy",
805
+ "aria-label": `${labels.copyValue} ${value}`,
806
+ onClick: () => copy(value, labels.valueCopied),
807
+ children: isCopied ? /* @__PURE__ */ jsx14(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx14(Copy, { size: 14, "aria-hidden": true })
808
+ }
809
+ );
810
+ }
811
+
812
+ // src/participant/ParticipantMessageNodes.tsx
813
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
814
+ var LINK_REL = "noopener noreferrer nofollow ugc";
815
+ function renderLink(node, labels, key) {
816
+ 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: [
819
+ node.label,
820
+ isExternal ? /* @__PURE__ */ jsxs11("span", { className: "cv-p-sr-only", children: [
821
+ " (",
822
+ labels.externalLinkHint,
823
+ ")"
824
+ ] }) : null
825
+ ] }),
826
+ /* @__PURE__ */ jsx15(ParticipantLinkCopyButton, { value: node.href, labels })
827
+ ] }, key);
828
+ }
829
+ function renderInlineNodes(nodes, labels) {
830
+ return nodes.map((node, key) => {
831
+ switch (node.kind) {
832
+ case "text":
833
+ return node.value;
834
+ case "strong":
835
+ return /* @__PURE__ */ jsx15("strong", { children: renderInlineNodes(node.children, labels) }, key);
836
+ case "em":
837
+ return /* @__PURE__ */ jsx15("em", { children: renderInlineNodes(node.children, labels) }, key);
838
+ case "del":
839
+ return /* @__PURE__ */ jsx15("del", { children: renderInlineNodes(node.children, labels) }, key);
840
+ case "code":
841
+ return /* @__PURE__ */ jsx15("code", { className: "cv-p-text__code", children: node.value }, key);
842
+ case "link":
843
+ return renderLink(node, labels, key);
844
+ case "copyable":
845
+ return /* @__PURE__ */ jsx15(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
846
+ }
847
+ });
848
+ }
849
+
850
+ // src/participant/participantDocuments.ts
851
+ var CNPJ_FIRST_WEIGHTS = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
852
+ var CNPJ_SECOND_WEIGHTS = [6, ...CNPJ_FIRST_WEIGHTS];
853
+ function charValue(character) {
854
+ return character.charCodeAt(0) - 48;
855
+ }
856
+ function isRepeatedSequence(value) {
857
+ return new Set(value).size === 1;
858
+ }
859
+ function weightedSum(characters, weights) {
860
+ let sum = 0;
861
+ for (let index = 0; index < weights.length; index += 1) {
862
+ sum += charValue(characters.charAt(index)) * (weights[index] ?? 0);
863
+ }
864
+ return sum;
865
+ }
866
+ function cnpjCheckDigit(characters, weights) {
867
+ const remainder = weightedSum(characters, weights) % 11;
868
+ return remainder < 2 ? 0 : 11 - remainder;
869
+ }
870
+ function cpfCheckDigit(digits, length) {
871
+ const weights = Array.from({ length }, (_, index) => length + 1 - index);
872
+ return weightedSum(digits, weights) * 10 % 11 % 10;
873
+ }
874
+ function isValidCpf(digits) {
875
+ if (!/^\d{11}$/.test(digits) || isRepeatedSequence(digits)) return false;
876
+ return cpfCheckDigit(digits, 9) === charValue(digits.charAt(9)) && cpfCheckDigit(digits, 10) === charValue(digits.charAt(10));
877
+ }
878
+ function isValidCnpj(characters) {
879
+ if (!/^[A-Z0-9]{12}\d{2}$/.test(characters) || isRepeatedSequence(characters)) return false;
880
+ const first = cnpjCheckDigit(characters, CNPJ_FIRST_WEIGHTS);
881
+ if (first !== charValue(characters.charAt(12))) return false;
882
+ return cnpjCheckDigit(characters, CNPJ_SECOND_WEIGHTS) === charValue(characters.charAt(13));
883
+ }
884
+
885
+ // src/participant/participantMessageCopyables.ts
886
+ var DOCUMENT_SEPARATORS = /[.\-/\s]/g;
887
+ var ACCESS_KEY_PATTERN = /\d{44}|\d{4}(?: \d{4}){10}/gu;
888
+ var CNPJ_PATTERN = /[A-Z0-9]{2}\.[A-Z0-9]{3}\.[A-Z0-9]{3}\/[A-Z0-9]{4}-\d{2}|[A-Z0-9]{12}\d{2}/gu;
889
+ var CPF_PATTERN = /\d{3}\.\d{3}\.\d{3}-\d{2}|\d{11}/gu;
890
+ var BRAZIL_PHONE_PATTERN = /(?:\+?55[\s.-]?)?(?:\([1-9][1-9]\)|[1-9][1-9])[\s.-]?(?:9\d{4}|[2-5]\d{3})[\s.-]?\d{4}/gu;
891
+ var INTERNATIONAL_PHONE_PATTERN = /\+\d{1,3}(?:[\s.-]?\d{2,5}){2,4}/gu;
892
+ var EMAIL_LOCAL = /^[\p{L}\p{N}._%+-]+$/u;
893
+ var EMAIL_DOMAIN = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)*\.\p{L}{2,}$/u;
894
+ function bare(raw) {
895
+ return raw.replace(DOCUMENT_SEPARATORS, "");
896
+ }
897
+ function isWordCharacter(character) {
898
+ return character !== void 0 && /[\p{L}\p{N}]/u.test(character);
899
+ }
900
+ function hasPhoneDigitCount(raw) {
901
+ const count = raw.replace(/\D/g, "").length;
902
+ return count >= 10 && count <= 15;
903
+ }
904
+ function isFormattedBrazilPhone(raw) {
905
+ return /^\+|\(|^\d{2}[\s.-]/u.test(raw) && hasPhoneDigitCount(raw);
906
+ }
907
+ var SCANNERS = [
908
+ { copyKind: "accessKey", pattern: ACCESS_KEY_PATTERN, isValid: (raw) => !/^(\d)\1+$/.test(bare(raw)) },
909
+ { copyKind: "cnpj", pattern: CNPJ_PATTERN, isValid: (raw) => isValidCnpj(bare(raw)) },
910
+ { copyKind: "cpf", pattern: CPF_PATTERN, isValid: (raw) => isValidCpf(bare(raw)) },
911
+ { copyKind: "phone", pattern: BRAZIL_PHONE_PATTERN, isValid: isFormattedBrazilPhone },
912
+ {
913
+ copyKind: "phone",
914
+ pattern: INTERNATIONAL_PHONE_PATTERN,
915
+ isValid: (raw) => hasPhoneDigitCount(raw) && !raw.startsWith("+55")
916
+ }
917
+ ];
918
+ function scan(text, scanner) {
919
+ const atoms = [];
920
+ for (const match of text.matchAll(scanner.pattern)) {
921
+ const [value] = match;
922
+ const start = match.index;
923
+ const end = start + value.length;
924
+ if (isWordCharacter(text[start - 1]) || isWordCharacter(text[end])) continue;
925
+ if (!scanner.isValid(value)) continue;
926
+ atoms.push({ start, end, node: { kind: "copyable", copyKind: scanner.copyKind, value } });
927
+ }
928
+ return atoms;
929
+ }
930
+ function findEmailAtoms(text) {
931
+ const atoms = [];
932
+ for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
933
+ let start = at;
934
+ while (start > 0 && /[\p{L}\p{N}._%+-]/u.test(text[start - 1] ?? "")) start -= 1;
935
+ let end = at + 1;
936
+ while (end < text.length && /[\p{L}\p{N}.-]/u.test(text[end] ?? "")) end += 1;
937
+ while (end > at + 1 && /[.-]/.test(text[end - 1] ?? "")) end -= 1;
938
+ const isValid = EMAIL_LOCAL.test(text.slice(start, at)) && EMAIL_DOMAIN.test(text.slice(at + 1, end));
939
+ if (isValid) {
940
+ atoms.push({ start, end, node: { kind: "copyable", copyKind: "email", value: text.slice(start, end) } });
941
+ }
942
+ }
943
+ return atoms;
944
+ }
945
+ function findCopyableAtoms(text) {
946
+ return [...SCANNERS.flatMap((scanner) => scan(text, scanner)), ...findEmailAtoms(text)];
947
+ }
948
+
949
+ // src/participant/participantMessageLinks.ts
950
+ var ALLOWED_LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
951
+ var URL_PATTERN = /https?:\/\/[^\s<>`\u0000-\u001f]+/giu;
952
+ var TRAILING_PUNCTUATION = `.,;:!?*_~'"]}>`;
953
+ function isAllowedLinkHref(href) {
954
+ try {
955
+ const url = new URL(href);
956
+ return ALLOWED_LINK_PROTOCOLS.has(url.protocol) && url.username === "" && url.password === "";
957
+ } catch {
958
+ return false;
959
+ }
960
+ }
961
+ function isAlphanumeric(character) {
962
+ return character !== void 0 && /[\p{L}\p{N}]/u.test(character);
963
+ }
964
+ function countOf(text, character) {
965
+ let count = 0;
966
+ for (const current of text) if (current === character) count += 1;
967
+ return count;
968
+ }
969
+ function trimUrl(raw) {
970
+ let openCount = countOf(raw, "(");
971
+ let closeCount = countOf(raw, ")");
972
+ let end = raw.length;
973
+ while (end > 0) {
974
+ const last = raw.charAt(end - 1);
975
+ if (last === ")" && closeCount > openCount) closeCount -= 1;
976
+ else if (!TRAILING_PUNCTUATION.includes(last)) break;
977
+ end -= 1;
978
+ }
979
+ return raw.slice(0, end);
980
+ }
981
+ function toLinkAtom(text, start, raw) {
982
+ const label = trimUrl(raw);
983
+ if (/^https?:\/\/$/i.test(label) || isAlphanumeric(text[start - 1])) return void 0;
984
+ const href = label.replace(/^https?/i, (scheme) => scheme.toLowerCase());
985
+ if (!isAllowedLinkHref(href)) return void 0;
986
+ return { start, end: start + label.length, node: { kind: "link", href, label } };
987
+ }
988
+ function findLinkAtoms(text) {
989
+ const atoms = [];
990
+ for (const match of text.matchAll(URL_PATTERN)) {
991
+ const atom = toLinkAtom(text, match.index, match[0]);
992
+ if (atom) atoms.push(atom);
993
+ }
994
+ return atoms;
995
+ }
996
+
997
+ // src/participant/participantMessageAtoms.ts
998
+ var TRIPLE_FENCE = "```";
999
+ function spanAt(text, open) {
1000
+ const isTriple = text.startsWith(TRIPLE_FENCE, open);
1001
+ const width = isTriple ? 3 : 1;
1002
+ const close = text.indexOf(isTriple ? TRIPLE_FENCE : "`", open + width);
1003
+ if (close === -1) return void 0;
1004
+ return { contentStart: open + width, close, width };
1005
+ }
1006
+ function findCodeAtoms(text) {
1007
+ const atoms = [];
1008
+ let cursor = text.indexOf("`");
1009
+ while (cursor !== -1) {
1010
+ const span = spanAt(text, cursor);
1011
+ if (span === void 0) {
1012
+ if (!text.startsWith(TRIPLE_FENCE, cursor)) break;
1013
+ cursor = text.indexOf("`", cursor + 3);
1014
+ continue;
1015
+ }
1016
+ if (span.close === span.contentStart) {
1017
+ cursor = text.indexOf("`", cursor + span.width);
1018
+ continue;
1019
+ }
1020
+ const end = span.close + span.width;
1021
+ atoms.push({ start: cursor, end, node: { kind: "code", value: text.slice(span.contentStart, span.close) } });
1022
+ cursor = text.indexOf("`", end);
1023
+ }
1024
+ return atoms;
1025
+ }
1026
+ function findInlineAtoms(text) {
1027
+ const candidates = [...findCodeAtoms(text), ...findLinkAtoms(text), ...findCopyableAtoms(text)];
1028
+ const ordered = candidates.map((atom, rank) => ({ atom, rank })).sort((a, b) => a.atom.start - b.atom.start || a.rank - b.rank);
1029
+ const accepted = [];
1030
+ let lastEnd = 0;
1031
+ for (const { atom } of ordered) {
1032
+ if (atom.start < lastEnd) continue;
1033
+ accepted.push(atom);
1034
+ lastEnd = atom.end;
1035
+ }
1036
+ return accepted;
1037
+ }
1038
+
1039
+ // src/participant/participantMessageInline.ts
1040
+ var MARKS = { "*": "strong", _: "em", "~": "del" };
1041
+ function isWord(character) {
1042
+ return character !== void 0 && /[\p{L}\p{N}]/u.test(character);
1043
+ }
1044
+ function isSpace(character) {
1045
+ return character === void 0 || /\s/u.test(character);
1046
+ }
1047
+ function characterBefore(text, index) {
1048
+ if (index <= 0) return void 0;
1049
+ const code = text.charCodeAt(index - 1);
1050
+ const isLowSurrogate = code >= 56320 && code <= 57343;
1051
+ if (isLowSurrogate && index >= 2) return String.fromCodePoint(text.codePointAt(index - 2) ?? code);
1052
+ return text.charAt(index - 1);
1053
+ }
1054
+ function buildContext(text) {
1055
+ const atoms = findInlineAtoms(text);
1056
+ const isMasked = new Array(text.length).fill(false);
1057
+ for (const atom of atoms) isMasked.fill(true, atom.start, atom.end);
1058
+ const closers = { "*": [], _: [], "~": [] };
1059
+ for (let index = 1; index < text.length; index += 1) {
1060
+ const character = text.charAt(index);
1061
+ const list = closers[character];
1062
+ const isCloser = list !== void 0 && !isMasked[index] && !isSpace(text[index - 1]) && !isWord(text[index + 1]);
1063
+ if (isCloser) list.push(index);
1064
+ }
1065
+ return { text, atomAt: new Map(atoms.map((atom) => [atom.start, atom])), isMasked, closers };
1066
+ }
1067
+ function isOpener(context, index) {
1068
+ const { text } = context;
1069
+ const next = text[index + 1];
1070
+ if (context.isMasked[index] || next === void 0 || isSpace(next) || next === text[index]) return false;
1071
+ return !isWord(characterBefore(text, index));
1072
+ }
1073
+ function findCloser(context, mark, from, limit) {
1074
+ const list = context.closers[mark] ?? [];
1075
+ let low = 0;
1076
+ let high = list.length;
1077
+ while (low < high) {
1078
+ const middle = low + high >> 1;
1079
+ if ((list[middle] ?? 0) < from) low = middle + 1;
1080
+ else high = middle;
1081
+ }
1082
+ const found = list[low];
1083
+ return found !== void 0 && found < limit ? found : void 0;
1084
+ }
1085
+ function parseRange(context, start, end) {
1086
+ const nodes = [];
1087
+ let textStart = start;
1088
+ let index = start;
1089
+ const flush = (until) => {
1090
+ if (until > textStart) nodes.push({ kind: "text", value: context.text.slice(textStart, until) });
1091
+ };
1092
+ while (index < end) {
1093
+ const atom = context.atomAt.get(index);
1094
+ const mark = MARKS[context.text.charAt(index)];
1095
+ const closer = mark !== void 0 && !atom && isOpener(context, index) ? findCloser(context, context.text.charAt(index), index + 2, end) : void 0;
1096
+ if (atom) {
1097
+ flush(index);
1098
+ nodes.push(atom.node);
1099
+ index = atom.end;
1100
+ } else if (mark !== void 0 && closer !== void 0) {
1101
+ flush(index);
1102
+ nodes.push({ kind: mark, children: parseRange(context, index + 1, closer) });
1103
+ index = closer + 1;
1104
+ } else {
1105
+ index += 1;
1106
+ continue;
1107
+ }
1108
+ textStart = index;
1109
+ }
1110
+ flush(end);
1111
+ return nodes;
1112
+ }
1113
+ function parseInline(line) {
1114
+ return parseRange(buildContext(line), 0, line.length);
1115
+ }
1116
+
1117
+ // src/participant/participantMessageFormat.ts
1118
+ var MAX_FORMATTED_LENGTH = 8e3;
1119
+ var FENCE = "```";
1120
+ var LANGUAGE_LABEL = /^[a-z][a-z0-9+#.-]{0,19}$/i;
1121
+ var QUOTE_PREFIX = /^> /;
1122
+ var BULLET_PREFIX = /^[-*•] +(?=\S)/;
1123
+ var ORDERED_PREFIX = /^(\d{1,3})[.)] +(?=\S)/;
1124
+ function joinLines(lines) {
1125
+ const nodes = [];
1126
+ lines.forEach((line, index) => {
1127
+ if (index > 0) nodes.push({ kind: "text", value: "\n" });
1128
+ nodes.push(...parseInline(line));
1129
+ });
1130
+ return mergeText(nodes);
1131
+ }
1132
+ function mergeText(nodes) {
1133
+ const merged = [];
1134
+ for (const node of nodes) {
1135
+ const previous = merged[merged.length - 1];
1136
+ if (node.kind === "text" && previous?.kind === "text") {
1137
+ merged[merged.length - 1] = { kind: "text", value: previous.value + node.value };
1138
+ } else {
1139
+ merged.push(node);
1140
+ }
1141
+ }
1142
+ return merged;
1143
+ }
1144
+ function plainParagraph(text) {
1145
+ return [{ kind: "paragraph", children: [{ kind: "text", value: text }] }];
1146
+ }
1147
+ function readFence(lines, index) {
1148
+ const first = (lines[index] ?? "").trimStart();
1149
+ if (!first.startsWith(FENCE)) return void 0;
1150
+ const inner = first.slice(FENCE.length);
1151
+ if (inner.trimEnd().endsWith(FENCE) && inner.trim() !== FENCE) {
1152
+ return { value: inner.trimEnd().slice(0, -FENCE.length), nextIndex: index + 1 };
1153
+ }
1154
+ const hasLabel = LANGUAGE_LABEL.test(inner.trim());
1155
+ const body = inner === "" || hasLabel ? [] : [inner];
1156
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
1157
+ const line = (lines[cursor] ?? "").trimEnd();
1158
+ if (!line.endsWith(FENCE)) {
1159
+ body.push(line);
1160
+ continue;
1161
+ }
1162
+ const last = line.slice(0, -FENCE.length);
1163
+ if (last !== "") body.push(last);
1164
+ if (last !== "" && hasLabel && body.length === 1) body.unshift(inner);
1165
+ return { value: body.join("\n"), nextIndex: cursor + 1 };
1166
+ }
1167
+ return void 0;
1168
+ }
1169
+ function startsSpecialBlock(line) {
1170
+ const trimmed = line.trimStart();
1171
+ return trimmed.startsWith(FENCE) || QUOTE_PREFIX.test(line) || BULLET_PREFIX.test(line) || ORDERED_PREFIX.test(line);
1172
+ }
1173
+ function readQuote(lines, index) {
1174
+ const quoted = [];
1175
+ let cursor = index;
1176
+ while (cursor < lines.length && QUOTE_PREFIX.test(lines[cursor] ?? "")) {
1177
+ quoted.push((lines[cursor] ?? "").slice(2));
1178
+ cursor += 1;
1179
+ }
1180
+ return { block: { kind: "quote", children: joinLines(quoted) }, nextIndex: cursor };
1181
+ }
1182
+ function readList(lines, index) {
1183
+ const ordered = ORDERED_PREFIX.test(lines[index] ?? "");
1184
+ const prefix = ordered ? ORDERED_PREFIX : BULLET_PREFIX;
1185
+ const start = ordered ? Number(ORDERED_PREFIX.exec(lines[index] ?? "")?.[1] ?? 1) : 1;
1186
+ const items = [];
1187
+ let cursor = index;
1188
+ while (cursor < lines.length && prefix.test(lines[cursor] ?? "")) {
1189
+ items.push(parseInline((lines[cursor] ?? "").replace(prefix, "")));
1190
+ cursor += 1;
1191
+ }
1192
+ return { block: { kind: "list", ordered, start, items }, nextIndex: cursor };
1193
+ }
1194
+ function readParagraph(lines, index) {
1195
+ const collected = [lines[index] ?? ""];
1196
+ let cursor = index + 1;
1197
+ while (cursor < lines.length && !startsSpecialBlock(lines[cursor] ?? "")) {
1198
+ collected.push(lines[cursor] ?? "");
1199
+ cursor += 1;
1200
+ }
1201
+ return { block: { kind: "paragraph", children: joinLines(collected) }, nextIndex: cursor };
1202
+ }
1203
+ function readBlock(lines, index, canFence) {
1204
+ const line = lines[index] ?? "";
1205
+ const fence = canFence ? readFence(lines, index) : void 0;
1206
+ if (fence) return { block: { kind: "pre", value: fence.value }, nextIndex: fence.nextIndex };
1207
+ if (QUOTE_PREFIX.test(line)) return readQuote(lines, index);
1208
+ if (BULLET_PREFIX.test(line) || ORDERED_PREFIX.test(line)) return readList(lines, index);
1209
+ return readParagraph(lines, index);
1210
+ }
1211
+ function parseParticipantMessage(text) {
1212
+ if (typeof text !== "string" || text.trim() === "") return [];
1213
+ if (text.length > MAX_FORMATTED_LENGTH) return plainParagraph(text);
1214
+ const lines = text.split(/\r\n?|\n/);
1215
+ const blocks = [];
1216
+ let canFence = true;
1217
+ let index = 0;
1218
+ while (index < lines.length) {
1219
+ const { block, nextIndex } = readBlock(lines, index, canFence);
1220
+ if (block.kind === "paragraph" && startsFenceLine(lines[index])) canFence = false;
1221
+ blocks.push(block);
1222
+ index = nextIndex;
1223
+ }
1224
+ return blocks;
1225
+ }
1226
+ function startsFenceLine(line) {
1227
+ return (line ?? "").trimStart().startsWith(FENCE);
1228
+ }
1229
+
1230
+ // src/participant/ParticipantMessageText.tsx
1231
+ import { jsx as jsx16 } from "react/jsx-runtime";
1232
+ function renderBlock(block, labels, key) {
1233
+ if (block.kind === "paragraph") return /* @__PURE__ */ jsx16("span", { children: renderInlineNodes(block.children, labels) }, key);
1234
+ if (block.kind === "pre") {
1235
+ return /* @__PURE__ */ jsx16("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx16("code", { children: block.value }) }, key);
1236
+ }
1237
+ if (block.kind === "quote") {
1238
+ return /* @__PURE__ */ jsx16("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
1239
+ }
1240
+ const items = block.items.map((item, index) => /* @__PURE__ */ jsx16("li", { children: renderInlineNodes(item, labels) }, index));
1241
+ if (block.ordered) {
1242
+ return /* @__PURE__ */ jsx16("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
1243
+ }
1244
+ return /* @__PURE__ */ jsx16("ul", { className: "cv-p-text__list", children: items }, key);
1245
+ }
1246
+ function ParticipantMessageText({ text, labels }) {
1247
+ const blocks = useMemo(() => parseParticipantMessage(text), [text]);
1248
+ if (blocks.length === 0) return null;
1249
+ const [only] = blocks;
1250
+ 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 });
450
1252
  }
451
1253
 
452
1254
  // src/participant/ParticipantAttachmentItem.tsx
453
- import { useEffect, useState as useState2 } from "react";
1255
+ import { useEffect as useEffect4, useState as useState2 } from "react";
454
1256
 
455
1257
  // src/participant/participantAttachmentUrl.ts
456
1258
  var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
@@ -463,11 +1265,11 @@ function isSafeAttachmentUrl(url) {
463
1265
  }
464
1266
 
465
1267
  // src/participant/ParticipantAttachmentItem.tsx
466
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1268
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
467
1269
  function useAttachmentUrl(attachment, resolveAttachmentUrl) {
468
1270
  const [url, setUrl] = useState2(void 0);
469
1271
  const disposition = attachment.kind === "document" ? "attachment" : "inline";
470
- useEffect(() => {
1272
+ useEffect4(() => {
471
1273
  let isCurrent = true;
472
1274
  resolveAttachmentUrl(attachment, disposition).then(
473
1275
  (resolved) => {
@@ -486,18 +1288,18 @@ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
486
1288
  function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
487
1289
  const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
488
1290
  if (attachment.kind === "image") {
489
- return /* @__PURE__ */ jsxs7("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
490
- url ? /* @__PURE__ */ jsx7("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
491
- /* @__PURE__ */ jsx7("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
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 })
492
1294
  ] });
493
1295
  }
494
1296
  if (attachment.kind === "audio") {
495
- return /* @__PURE__ */ jsxs7("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
496
- /* @__PURE__ */ jsx7("span", { className: "cv-p-attachment__name", children: attachment.filename }),
497
- /* @__PURE__ */ jsx7("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
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 })
498
1300
  ] });
499
1301
  }
500
- return /* @__PURE__ */ jsxs7(
1302
+ return /* @__PURE__ */ jsxs12(
501
1303
  "a",
502
1304
  {
503
1305
  className: "cv-p-attachment cv-p-attachment--document",
@@ -505,13 +1307,37 @@ function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels })
505
1307
  download: attachment.filename,
506
1308
  "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
507
1309
  children: [
508
- /* @__PURE__ */ jsx7("span", { className: "cv-p-attachment__name", children: attachment.filename }),
509
- /* @__PURE__ */ jsx7("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
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) })
510
1312
  ]
511
1313
  }
512
1314
  );
513
1315
  }
514
1316
 
1317
+ // src/participant/participantPerspective.ts
1318
+ var DEFAULT_PARTICIPANT_PERSPECTIVE = "participant";
1319
+ var OWN_DIRECTION = {
1320
+ participant: "inbound",
1321
+ operator: "outbound"
1322
+ };
1323
+ function isMessageMine(message, perspective = DEFAULT_PARTICIPANT_PERSPECTIVE) {
1324
+ return message.direction === OWN_DIRECTION[perspective];
1325
+ }
1326
+ function isUnreadByViewer(message, perspective) {
1327
+ if (isMessageMine(message, perspective)) return false;
1328
+ return !message.readAt && message.status !== "read";
1329
+ }
1330
+ function countOtherSideMessages(messages, perspective) {
1331
+ return messages.filter((message) => !isMessageMine(message, perspective)).length;
1332
+ }
1333
+ function countUnreadByViewer(messages, perspective) {
1334
+ return messages.filter((message) => isUnreadByViewer(message, perspective)).length;
1335
+ }
1336
+ function resolveViewerUnread({ messages, perspective, acknowledgedCount }) {
1337
+ if (countOtherSideMessages(messages, perspective) <= acknowledgedCount) return 0;
1338
+ return countUnreadByViewer(messages, perspective);
1339
+ }
1340
+
515
1341
  // src/participant/participantMessages.ts
516
1342
  function isSameSubject(left, right) {
517
1343
  return left.subjectType === right.subjectType && left.subjectId === right.subjectId;
@@ -537,8 +1363,8 @@ function mergeParticipantMessages(params) {
537
1363
  }
538
1364
  return items.sort((left, right) => createdAtOf(left) - createdAtOf(right));
539
1365
  }
540
- function isOwnMessage(message) {
541
- return message.direction === "inbound";
1366
+ function isOwnMessage(message, perspective) {
1367
+ return isMessageMine(message, perspective);
542
1368
  }
543
1369
  function resolveOwnMessageStatus(params) {
544
1370
  const { displayState, serverStatus, confirmsRead } = params;
@@ -550,12 +1376,12 @@ function resolveOwnMessageStatus(params) {
550
1376
  }
551
1377
 
552
1378
  // src/participant/ParticipantMessageBubble.tsx
553
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
554
- function describeItem(item) {
1379
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1380
+ function describeItem(item, perspective) {
555
1381
  if (item.kind === "server") {
556
1382
  const { message } = item;
557
1383
  return {
558
- isMine: isOwnMessage(message),
1384
+ isMine: isOwnMessage(message, perspective),
559
1385
  authorName: message.authorName ?? void 0,
560
1386
  text: message.text ?? void 0,
561
1387
  createdAt: message.createdAt,
@@ -573,48 +1399,11 @@ function describeItem(item) {
573
1399
  displayState: item.displayState
574
1400
  };
575
1401
  }
576
- function statusText(status, labels) {
577
- const byStatus = {
578
- sending: labels.statusSending,
579
- queued: labels.statusQueued,
580
- sent: labels.statusSent,
581
- delivered: labels.statusDelivered,
582
- read: labels.statusRead,
583
- failed: labels.statusFailed
584
- };
585
- return byStatus[status];
586
- }
587
- function OwnStatus({ status, labels, onRetry }) {
588
- const ticksStatus = status === "sending" ? "queued" : status;
589
- const isVisibleText = status === "sending" || status === "queued" || status === "failed";
590
- if (status === "failed" && onRetry) {
591
- return /* @__PURE__ */ jsxs8("button", { type: "button", className: "cv-p-bubble__retry", onClick: onRetry, children: [
592
- /* @__PURE__ */ jsx8(StatusTicks, { status: "failed", appearance: "stylesheet" }),
593
- /* @__PURE__ */ jsx8("span", { children: labels.statusFailed })
594
- ] });
595
- }
596
- const text = status === "failed" ? labels.statusFailedShort : statusText(status, labels);
597
- return /* @__PURE__ */ jsxs8("span", { className: "cv-p-bubble__status", children: [
598
- /* @__PURE__ */ jsx8(StatusTicks, { status: ticksStatus, appearance: "stylesheet" }),
599
- /* @__PURE__ */ jsx8("span", { className: isVisibleText ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
600
- ] });
601
- }
602
- function FailedActions({ labels, onDiscard, onEdit }) {
603
- if (!onDiscard && !onEdit) return null;
604
- return /* @__PURE__ */ jsxs8("span", { className: "cv-p-bubble__actions", children: [
605
- onEdit ? /* @__PURE__ */ jsx8("button", { type: "button", className: "cv-p-bubble__action", onClick: onEdit, children: labels.edit }) : null,
606
- onDiscard ? /* @__PURE__ */ jsx8("button", { type: "button", className: "cv-p-bubble__action", onClick: onDiscard, children: labels.discard }) : null
607
- ] });
608
- }
609
- function toTextPayload(text, createdAt, isMine) {
610
- return {
611
- id: createdAt,
612
- type: "text",
613
- content: text,
614
- direction: isMine ? "inbound" : "outbound",
615
- sender: isMine ? "customer" : "agent",
616
- timestamp: createdAt
617
- };
1402
+ function bubbleClassName(isMine, hasTail) {
1403
+ const classes = ["cv-p-bubble"];
1404
+ if (isMine) classes.push("cv-p-bubble--mine");
1405
+ if (!hasTail) classes.push("cv-p-bubble--no-tail");
1406
+ return classes.join(" ");
618
1407
  }
619
1408
  function ParticipantMessageBubble({
620
1409
  item,
@@ -623,15 +1412,18 @@ function ParticipantMessageBubble({
623
1412
  resolveAttachmentUrl,
624
1413
  onRetry,
625
1414
  onDiscard,
626
- onEdit
1415
+ onEdit,
1416
+ avatar,
1417
+ tail = true,
1418
+ perspective
627
1419
  }) {
628
- const content = describeItem(item);
1420
+ const content = describeItem(item, perspective);
629
1421
  const { isMine } = content;
630
1422
  const ownStatus = isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
631
- return /* @__PURE__ */ jsxs8("div", { className: isMine ? "cv-p-bubble cv-p-bubble--mine" : "cv-p-bubble", children: [
632
- isMine ? /* @__PURE__ */ jsx8("span", { className: "cv-p-sr-only", children: labels.me }) : content.authorName ? /* @__PURE__ */ jsx8("span", { className: "cv-p-bubble__author", children: content.authorName }) : null,
633
- content.text ? /* @__PURE__ */ jsx8(MessageText, { message: toTextPayload(content.text, content.createdAt, isMine), copyOnClick: false, appearance: "stylesheet" }) : null,
634
- content.attachments.map((attachment) => /* @__PURE__ */ jsx8(
1423
+ const bubble = /* @__PURE__ */ jsxs13("div", { className: bubbleClassName(isMine, tail), children: [
1424
+ isMine ? /* @__PURE__ */ jsx18("span", { className: "cv-p-sr-only", children: labels.me }) : content.authorName ? /* @__PURE__ */ jsx18("span", { className: "cv-p-bubble__author", children: content.authorName }) : null,
1425
+ content.text ? /* @__PURE__ */ jsx18(ParticipantMessageText, { text: content.text, labels }) : null,
1426
+ content.attachments.map((attachment) => /* @__PURE__ */ jsx18(
635
1427
  ParticipantAttachmentItem,
636
1428
  {
637
1429
  attachment,
@@ -640,36 +1432,110 @@ function ParticipantMessageBubble({
640
1432
  },
641
1433
  attachment.id
642
1434
  )),
643
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx8("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
644
- /* @__PURE__ */ jsxs8("span", { className: "cv-p-bubble__meta", children: [
645
- /* @__PURE__ */ jsx8("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
646
- ownStatus ? /* @__PURE__ */ jsx8(OwnStatus, { status: ownStatus, labels, onRetry }) : null
1435
+ content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx18("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1436
+ /* @__PURE__ */ jsxs13("span", { className: "cv-p-bubble__meta", children: [
1437
+ /* @__PURE__ */ jsx18("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1438
+ ownStatus ? /* @__PURE__ */ jsx18(OwnStatus, { status: ownStatus, labels, onRetry }) : null
647
1439
  ] }),
648
- ownStatus === "failed" ? /* @__PURE__ */ jsx8(FailedActions, { labels, onDiscard, onEdit }) : null
1440
+ ownStatus === "failed" ? /* @__PURE__ */ jsx18(FailedActions, { labels, onDiscard, onEdit }) : null
649
1441
  ] });
1442
+ if (avatar === void 0 || isMine) return bubble;
1443
+ return /* @__PURE__ */ jsxs13("div", { className: "cv-p-bubble-row", children: [
1444
+ /* @__PURE__ */ jsx18("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1445
+ bubble
1446
+ ] });
1447
+ }
1448
+
1449
+ // src/participant/participantBubbleActions.ts
1450
+ function bindPendingActions(actions, clientMessageId) {
1451
+ const { retryHost } = actions;
1452
+ return {
1453
+ retryLocal: () => actions.retryLocal(clientMessageId),
1454
+ discardLocal: () => actions.discardLocal(clientMessageId),
1455
+ editLocal: () => actions.editLocal(clientMessageId),
1456
+ ...retryHost ? { retryHost: () => retryHost(clientMessageId) } : {}
1457
+ };
1458
+ }
1459
+ function resolveParticipantBubbleActions(item, handlers) {
1460
+ if (item.kind === "server" || item.displayState !== "failed") return {};
1461
+ if (item.origin === "host") return handlers.retryHost ? { onRetry: handlers.retryHost } : {};
1462
+ return { onRetry: handlers.retryLocal, onDiscard: handlers.discardLocal, onEdit: handlers.editLocal };
650
1463
  }
651
1464
 
652
- // src/participant/ParticipantProtocolBadge.tsx
653
- import { useEffect as useEffect2, useReducer } from "react";
654
-
655
- // src/participant/participantProtocolCopy.ts
656
- async function copyProtocolToClipboard(protocol, clipboard) {
657
- if (clipboard === void 0) return false;
658
- try {
659
- await clipboard.writeText(protocol);
660
- return true;
661
- } catch {
662
- return false;
1465
+ // src/participant/ParticipantTimeline.tsx
1466
+ import { jsx as jsx19 } from "react/jsx-runtime";
1467
+ function createdAtOf2(item) {
1468
+ return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1469
+ }
1470
+ function keyOf(item) {
1471
+ return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
1472
+ }
1473
+ function authorOf(item, perspective) {
1474
+ if (item.kind === "pending") return { isMine: true, author: null };
1475
+ return { isMine: isOwnMessage(item.message, perspective), author: item.message.authorName ?? null };
1476
+ }
1477
+ function resolveActions(item, pendingActions) {
1478
+ if (!pendingActions || item.kind === "server") return {};
1479
+ return resolveParticipantBubbleActions(item, bindPendingActions(pendingActions, item.pending.clientMessageId));
1480
+ }
1481
+ function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1482
+ if (!isEnabled || isMine) return void 0;
1483
+ return showAvatar ? /* @__PURE__ */ jsx19(ParticipantAuthorAvatar, { name: author, render }) : null;
1484
+ }
1485
+ function ParticipantTimeline(props) {
1486
+ const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
1487
+ const confirmsRead = channelCapabilityFor(props.channel ?? "app").confirmsRead;
1488
+ const isAvatarEnabled = props.avatars === "initials" || props.renderAuthorAvatar !== void 0;
1489
+ const nodes = [];
1490
+ let previousDay;
1491
+ let previousAuthor;
1492
+ for (const item of items) {
1493
+ const day = new Date(createdAtOf2(item));
1494
+ if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1495
+ nodes.push(/* @__PURE__ */ jsx19(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1496
+ previousAuthor = void 0;
1497
+ }
1498
+ previousDay = day;
1499
+ const { isMine, author } = authorOf(item, props.perspective);
1500
+ const showAvatar = shouldShowAvatar({ previousAuthor, author, isMine });
1501
+ previousAuthor = isMine ? void 0 : author;
1502
+ const avatar = resolveAvatarSlot({
1503
+ isEnabled: isAvatarEnabled,
1504
+ isMine,
1505
+ showAvatar,
1506
+ author,
1507
+ render: props.renderAuthorAvatar
1508
+ });
1509
+ nodes.push(
1510
+ /* @__PURE__ */ jsx19(
1511
+ ParticipantMessageBubble,
1512
+ {
1513
+ item,
1514
+ labels,
1515
+ confirmsRead,
1516
+ resolveAttachmentUrl,
1517
+ tail: props.tail,
1518
+ perspective: props.perspective,
1519
+ avatar,
1520
+ ...resolveActions(item, pendingActions)
1521
+ },
1522
+ keyOf(item)
1523
+ )
1524
+ );
663
1525
  }
1526
+ return /* @__PURE__ */ jsx19("div", { className: "cv-p-thread__messages", children: nodes });
664
1527
  }
665
- var INITIAL_PROTOCOL_COPY_STATE = { copyCount: 0, isCopied: false };
666
- function protocolCopyReducer(state, action) {
667
- if (action.type === "copied") return { copyCount: state.copyCount + 1, isCopied: true };
668
- return { ...state, isCopied: false };
1528
+
1529
+ // src/participant/ParticipantConversationAvatar.tsx
1530
+ import { jsx as jsx20 } from "react/jsx-runtime";
1531
+ function ParticipantConversationAvatar({ icon }) {
1532
+ return /* @__PURE__ */ jsx20("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
669
1533
  }
670
1534
 
671
1535
  // src/participant/ParticipantProtocolBadge.tsx
672
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1536
+ import { Check as Check2, Copy as Copy2 } from "lucide-react";
1537
+ import { useEffect as useEffect5, useReducer as useReducer3 } from "react";
1538
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
673
1539
  var COPIED_ANNOUNCEMENT_MS = 3e3;
674
1540
  function ParticipantProtocolBadgeView({
675
1541
  protocol,
@@ -678,31 +1544,36 @@ function ParticipantProtocolBadgeView({
678
1544
  onCopy
679
1545
  }) {
680
1546
  const { isCopied, copyCount } = copyState;
681
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
682
- /* @__PURE__ */ jsxs9("div", { className: "cv-p-protocol cv-p-protocol--header", children: [
683
- /* @__PURE__ */ jsxs9("span", { className: "cv-p-protocol__code", children: [
684
- /* @__PURE__ */ jsxs9("span", { className: "cv-p-sr-only", children: [
1547
+ return /* @__PURE__ */ jsxs14(Fragment4, { children: [
1548
+ /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1549
+ /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol__code", children: [
1550
+ /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
685
1551
  labels.protocolPrefix,
686
1552
  " "
687
1553
  ] }),
688
1554
  protocol
689
1555
  ] }),
690
- /* @__PURE__ */ jsxs9("button", { type: "button", className: "cv-p-protocol__copy", onClick: onCopy, children: [
691
- /* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: isCopied ? labels.protocolCopied : labels.copyProtocol }),
692
- /* @__PURE__ */ jsxs9("span", { className: "cv-p-sr-only", children: [
693
- labels.copyProtocol,
694
- " ",
695
- protocol
696
- ] })
697
- ] })
1556
+ /* @__PURE__ */ jsxs14(
1557
+ "button",
1558
+ {
1559
+ type: "button",
1560
+ className: "cv-p-protocol__copy",
1561
+ "aria-label": `${labels.copyProtocol} ${protocol}`,
1562
+ onClick: onCopy,
1563
+ children: [
1564
+ isCopied ? /* @__PURE__ */ jsx21(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx21(Copy2, { size: 18, "aria-hidden": true }),
1565
+ isCopied ? /* @__PURE__ */ jsx21("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
1566
+ ]
1567
+ }
1568
+ )
698
1569
  ] }),
699
- /* @__PURE__ */ jsx9("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx9("span", { children: labels.protocolCopied }, copyCount) : null })
1570
+ /* @__PURE__ */ jsx21("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx21("span", { children: labels.protocolCopied }, copyCount) : null })
700
1571
  ] });
701
1572
  }
702
1573
  function ParticipantProtocolBadge({ protocol, labels }) {
703
- const [copyState, dispatch] = useReducer(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1574
+ const [copyState, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
704
1575
  const { isCopied, copyCount } = copyState;
705
- useEffect2(() => {
1576
+ useEffect5(() => {
706
1577
  if (!isCopied) return void 0;
707
1578
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
708
1579
  return () => clearTimeout(timer);
@@ -711,7 +1582,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
711
1582
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
712
1583
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
713
1584
  }
714
- return /* @__PURE__ */ jsx9(
1585
+ return /* @__PURE__ */ jsx21(
715
1586
  ParticipantProtocolBadgeView,
716
1587
  {
717
1588
  protocol,
@@ -722,77 +1593,58 @@ function ParticipantProtocolBadge({ protocol, labels }) {
722
1593
  );
723
1594
  }
724
1595
 
725
- // src/participant/ParticipantThread.tsx
726
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
727
- function createdAtOf2(item) {
728
- return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
729
- }
730
- function keyOf(item) {
731
- return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
732
- }
733
- function resolveActions(item, pendingActions) {
734
- if (!pendingActions || item.kind === "server") return {};
735
- return resolveParticipantBubbleActions(item, bindPendingActions(pendingActions, item.pending.clientMessageId));
1596
+ // src/participant/ParticipantThreadHeader.tsx
1597
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
1598
+ function Title({ conversation, labels, onOpenSubject }) {
1599
+ const { subjectType, subjectId, subjectLabel } = conversation;
1600
+ return /* @__PURE__ */ jsx22("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx22(
1601
+ "button",
1602
+ {
1603
+ type: "button",
1604
+ className: "cv-p-thread__title-button",
1605
+ "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1606
+ onClick: () => onOpenSubject({ subjectType, subjectId }),
1607
+ children: /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1608
+ }
1609
+ ) : /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
736
1610
  }
737
- function Header({ props }) {
738
- const { conversation, labels, onBack, onOpenSubject } = props;
739
- const { subjectType, subjectId } = conversation;
740
- return /* @__PURE__ */ jsxs10("header", { className: "cv-p-thread__header", children: [
741
- onBack ? /* @__PURE__ */ jsxs10("button", { type: "button", className: "cv-p-button cv-p-thread__back", onClick: onBack, children: [
742
- /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", children: "\u2039" }),
743
- /* @__PURE__ */ jsx10("span", { className: "cv-p-sr-only", children: labels.back })
744
- ] }) : null,
745
- /* @__PURE__ */ jsxs10("div", { className: "cv-p-thread__heading", children: [
746
- /* @__PURE__ */ jsx10("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx10(
747
- "button",
748
- {
749
- type: "button",
750
- className: "cv-p-thread__title-button",
751
- "aria-label": `${labels.openSubject}: ${conversation.subjectLabel}`,
752
- onClick: () => onOpenSubject({ subjectType, subjectId }),
753
- children: conversation.subjectLabel
754
- }
755
- ) : conversation.subjectLabel }),
756
- conversation.protocol ? /* @__PURE__ */ jsx10(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
757
- /* @__PURE__ */ jsx10(ParticipantChannelBadges, { channels: conversation.channels, labels })
758
- ] })
1611
+ function ParticipantThreadHeader({
1612
+ conversation,
1613
+ labels,
1614
+ subjectGroups,
1615
+ renderSubjectIcon,
1616
+ onBack,
1617
+ onOpenSubject,
1618
+ counterpartLabel,
1619
+ actions
1620
+ }) {
1621
+ const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1622
+ const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1623
+ const hasChannels = resolveParticipantChannels(conversation.channels).length > 0;
1624
+ return /* @__PURE__ */ jsxs15("header", { className: "cv-p-thread__header", children: [
1625
+ onBack ? /* @__PURE__ */ jsx22(ParticipantBackButton, { label: labels.back, onBack }) : null,
1626
+ icon !== void 0 ? /* @__PURE__ */ jsx22(ParticipantConversationAvatar, { icon }) : null,
1627
+ /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__heading", children: [
1628
+ icon === void 0 && group ? /* @__PURE__ */ jsx22("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1629
+ /* @__PURE__ */ jsx22(Title, { conversation, labels, onOpenSubject }),
1630
+ counterpartLabel ? /* @__PURE__ */ jsx22("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
1631
+ conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs15("div", { className: "cv-p-thread__meta", children: [
1632
+ conversation.protocol ? /* @__PURE__ */ jsx22(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1633
+ /* @__PURE__ */ jsx22(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
1634
+ ] }) : null
1635
+ ] }),
1636
+ actions ? /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__actions", children: actions }) : null
759
1637
  ] });
760
1638
  }
761
- function Timeline({ props }) {
762
- const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
763
- const confirmsRead = channelCapabilityFor(props.channel ?? "app").confirmsRead;
764
- const nodes = [];
765
- let previousDay;
766
- for (const item of items) {
767
- const day = new Date(createdAtOf2(item));
768
- if (previousDay === void 0 || !isSameDay(previousDay, day)) {
769
- nodes.push(
770
- /* @__PURE__ */ jsx10("div", { className: "cv-p-day", role: "separator", children: day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" }) }, `day:${keyOf(item)}`)
771
- );
772
- }
773
- previousDay = day;
774
- nodes.push(
775
- /* @__PURE__ */ jsx10(
776
- ParticipantMessageBubble,
777
- {
778
- item,
779
- labels,
780
- confirmsRead,
781
- resolveAttachmentUrl,
782
- ...resolveActions(item, pendingActions)
783
- },
784
- keyOf(item)
785
- )
786
- );
787
- }
788
- return /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__messages", children: nodes });
789
- }
1639
+
1640
+ // src/participant/ParticipantThread.tsx
1641
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
790
1642
  function Footer({ props }) {
791
1643
  const { conversation, labels, draft } = props;
792
1644
  if (conversation.status === "closed") {
793
- return /* @__PURE__ */ jsx10("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
1645
+ return /* @__PURE__ */ jsx23("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
794
1646
  }
795
- return /* @__PURE__ */ jsx10(
1647
+ return /* @__PURE__ */ jsx23(
796
1648
  ParticipantComposer,
797
1649
  {
798
1650
  value: draft.value,
@@ -809,23 +1661,51 @@ function Footer({ props }) {
809
1661
  }
810
1662
  );
811
1663
  }
812
- function ParticipantThread(props) {
1664
+ function ThreadBody(props) {
813
1665
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
814
1666
  const subjectCard = renderSubjectCard?.(conversation);
815
1667
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
816
- return /* @__PURE__ */ jsxs10("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
817
- /* @__PURE__ */ jsx10(Header, { props }),
818
- subjectCard ? /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
819
- /* @__PURE__ */ jsxs10("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, children: [
820
- loadView.isLoading ? /* @__PURE__ */ jsx10(ParticipantLoading, { labels }) : null,
821
- loadView.hasError ? /* @__PURE__ */ jsx10(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
822
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx10("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
823
- /* @__PURE__ */ jsx10(Timeline, { props })
1668
+ return /* @__PURE__ */ jsxs16("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
1669
+ /* @__PURE__ */ jsx23(
1670
+ ParticipantThreadHeader,
1671
+ {
1672
+ conversation,
1673
+ labels,
1674
+ subjectGroups: props.subjectGroups,
1675
+ renderSubjectIcon: props.renderSubjectIcon,
1676
+ onBack: props.onBack,
1677
+ onOpenSubject: props.onOpenSubject,
1678
+ counterpartLabel: props.counterpartLabel,
1679
+ actions: props.headerActions
1680
+ }
1681
+ ),
1682
+ subjectCard ? /* @__PURE__ */ jsx23("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1683
+ /* @__PURE__ */ jsxs16("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1684
+ loadView.isLoading ? /* @__PURE__ */ jsx23(ParticipantLoading, { labels }) : null,
1685
+ loadView.hasError ? /* @__PURE__ */ jsx23(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1686
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx23("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1687
+ /* @__PURE__ */ jsx23(ParticipantTimeline, { ...props }),
1688
+ /* @__PURE__ */ jsx23(
1689
+ ParticipantScrollToLatest,
1690
+ {
1691
+ label: labels.scrollToLatest,
1692
+ isAwayFromBottom: props.scroll.isAwayFromBottom === true,
1693
+ hasMessages: props.items.length > 0,
1694
+ newMessagesCount: props.newMessagesCount,
1695
+ onClick: props.scroll.scrollToLatest
1696
+ }
1697
+ )
824
1698
  ] }),
825
- /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: props.newMessagesCount > 0 ? labels.newMessages : "" }),
826
- /* @__PURE__ */ jsx10(Footer, { props })
1699
+ /* @__PURE__ */ jsx23(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1700
+ /* @__PURE__ */ jsx23(Footer, { props })
827
1701
  ] });
828
1702
  }
1703
+ function ParticipantThread(props) {
1704
+ return /* @__PURE__ */ jsx23(ParticipantCopyAnnouncer, { children: /* @__PURE__ */ jsx23(ThreadBody, { ...props }) });
1705
+ }
1706
+
1707
+ // src/participant/useParticipantThreadController.ts
1708
+ import { useMemo as useMemo4 } from "react";
829
1709
 
830
1710
  // src/participant/bindAttachmentUrlResolver.ts
831
1711
  function bindAttachmentUrlResolver(api) {
@@ -908,8 +1788,8 @@ function appendEntryToDraft(entry, current) {
908
1788
  return { text, files: [...entry.files, ...current.files] };
909
1789
  }
910
1790
 
911
- // src/participant/useParticipantDraft.ts
912
- import { useState as useState3 } from "react";
1791
+ // src/participant/useParticipantConversationView.ts
1792
+ import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
913
1793
 
914
1794
  // src/participant/participantDrafts.ts
915
1795
  var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
@@ -937,30 +1817,6 @@ function takeDraftForSend(drafts, subject) {
937
1817
  };
938
1818
  }
939
1819
 
940
- // src/participant/useParticipantDraft.ts
941
- function useParticipantDraft(draftsRef, subject) {
942
- const [draft, setDraftState] = useState3(() => getDraft(draftsRef.current, subject));
943
- function replaceDrafts(next) {
944
- draftsRef.current = next;
945
- setDraftState(getDraft(next, subject));
946
- }
947
- function update(change) {
948
- replaceDrafts(setDraft(draftsRef.current, subject, { ...getDraft(draftsRef.current, subject), ...change }));
949
- }
950
- function takeForSend() {
951
- const taken = takeDraftForSend(draftsRef.current, subject);
952
- if (taken.content) replaceDrafts(taken.drafts);
953
- return taken.content;
954
- }
955
- function restoreFromEntry(entry) {
956
- update(appendEntryToDraft(entry, getDraft(draftsRef.current, subject)));
957
- }
958
- return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
959
- }
960
-
961
- // src/participant/useParticipantConversationView.ts
962
- import { useCallback as useCallback3, useMemo as useMemo2 } from "react";
963
-
964
1820
  // src/participant/participantSendStates.ts
965
1821
  var NO_ENTRIES = [];
966
1822
  function selectSendEntries(states, subject) {
@@ -977,7 +1833,7 @@ function participantSendStatesReducer(states, { subject, action }) {
977
1833
  }
978
1834
 
979
1835
  // src/participant/useParticipantThread.ts
980
- import { useCallback as useCallback2, useEffect as useEffect6, useMemo, useReducer as useReducer2, useRef as useRef4 } from "react";
1836
+ import { useCallback as useCallback4, useEffect as useEffect9, useMemo as useMemo2, useReducer as useReducer4, useRef as useRef7 } from "react";
981
1837
 
982
1838
  // src/participant/participantThreadState.ts
983
1839
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -1013,7 +1869,7 @@ function participantThreadReducer(state, action) {
1013
1869
  }
1014
1870
 
1015
1871
  // src/participant/useParticipantMarkRead.ts
1016
- import { useEffect as useEffect4 } from "react";
1872
+ import { useEffect as useEffect7 } from "react";
1017
1873
 
1018
1874
  // src/participant/shouldMarkRead.ts
1019
1875
  function shouldMarkParticipantRead(params) {
@@ -1025,12 +1881,12 @@ function shouldMarkParticipantRead(params) {
1025
1881
  }
1026
1882
 
1027
1883
  // src/participant/useDocumentVisibility.ts
1028
- import { useEffect as useEffect3, useState as useState4 } from "react";
1884
+ import { useEffect as useEffect6, useState as useState3 } from "react";
1029
1885
  function useDocumentVisibility() {
1030
- const [visibilityState, setVisibilityState] = useState4(
1886
+ const [visibilityState, setVisibilityState] = useState3(
1031
1887
  () => typeof document === "undefined" ? "visible" : document.visibilityState
1032
1888
  );
1033
- useEffect3(() => {
1889
+ useEffect6(() => {
1034
1890
  function handleVisibilityChange() {
1035
1891
  setVisibilityState(document.visibilityState);
1036
1892
  }
@@ -1045,18 +1901,18 @@ function useDocumentVisibility() {
1045
1901
  function useParticipantMarkRead(params) {
1046
1902
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
1047
1903
  const visibilityState = useDocumentVisibility();
1048
- useEffect4(() => {
1904
+ useEffect7(() => {
1049
1905
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
1050
1906
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
1051
1907
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
1052
1908
  }
1053
1909
 
1054
1910
  // src/participant/useParticipantRevalidation.ts
1055
- import { useEffect as useEffect5, useRef as useRef2 } from "react";
1911
+ import { useEffect as useEffect8, useRef as useRef4 } from "react";
1056
1912
  function useParticipantRevalidation(api, onRevalidate) {
1057
- const handlerRef = useRef2(onRevalidate);
1913
+ const handlerRef = useRef4(onRevalidate);
1058
1914
  handlerRef.current = onRevalidate;
1059
- useEffect5(() => {
1915
+ useEffect8(() => {
1060
1916
  function handleFocus() {
1061
1917
  handlerRef.current();
1062
1918
  }
@@ -1083,12 +1939,12 @@ function useParticipantRevalidation(api, onRevalidate) {
1083
1939
  }
1084
1940
 
1085
1941
  // src/participant/useParticipantSend.ts
1086
- import { useCallback, useRef as useRef3 } from "react";
1942
+ import { useCallback as useCallback2, useRef as useRef5 } from "react";
1087
1943
  function useParticipantSend(params) {
1088
1944
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
1089
- const entriesRef = useRef3(params.entries);
1945
+ const entriesRef = useRef5(params.entries);
1090
1946
  entriesRef.current = params.entries;
1091
- const deliver = useCallback(
1947
+ const deliver = useCallback2(
1092
1948
  async (entry) => {
1093
1949
  const { clientMessageId } = entry;
1094
1950
  try {
@@ -1112,7 +1968,7 @@ function useParticipantSend(params) {
1112
1968
  },
1113
1969
  [apiRef, dispatchSend, onSentMessage]
1114
1970
  );
1115
- const send = useCallback(
1971
+ const send = useCallback2(
1116
1972
  async (content) => {
1117
1973
  const entry = createParticipantSendEntry({
1118
1974
  clientMessageId: crypto.randomUUID(),
@@ -1125,7 +1981,7 @@ function useParticipantSend(params) {
1125
1981
  },
1126
1982
  [deliver, dispatchSend, subject]
1127
1983
  );
1128
- const retry = useCallback(
1984
+ const retry = useCallback2(
1129
1985
  async (clientMessageId) => {
1130
1986
  const entry = findFailedEntry(entriesRef.current, clientMessageId);
1131
1987
  if (!entry) return;
@@ -1137,6 +1993,24 @@ function useParticipantSend(params) {
1137
1993
  return { send, retry };
1138
1994
  }
1139
1995
 
1996
+ // src/participant/useParticipantUnread.ts
1997
+ import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
1998
+ function useParticipantUnread(params) {
1999
+ const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2000
+ const [acknowledgedCount, setAcknowledgedCount] = useState4(0);
2001
+ const messagesRef = useRef6(messages);
2002
+ messagesRef.current = messages;
2003
+ const handleMarkedRead = useCallback3(
2004
+ (subject) => {
2005
+ if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective));
2006
+ onMarkedRead?.(subject);
2007
+ },
2008
+ [perspective, onMarkedRead]
2009
+ );
2010
+ const unreadCount = perspective ? resolveViewerUnread({ messages, perspective, acknowledgedCount }) : reportedUnreadCount;
2011
+ return { unreadCount, handleMarkedRead };
2012
+ }
2013
+
1140
2014
  // src/participant/useParticipantThread.ts
1141
2015
  var THREAD_PAGE_SIZE = 30;
1142
2016
  function describeError(error) {
@@ -1147,15 +2021,15 @@ function isForSubject(event, subject) {
1147
2021
  return event.subject.subjectType === subject.subjectType && event.subject.subjectId === subject.subjectId;
1148
2022
  }
1149
2023
  function useParticipantThread(params) {
1150
- const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params;
2024
+ const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
1151
2025
  const { subjectType, subjectId } = subject;
1152
- const stableSubject = useMemo(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
1153
- const [state, dispatch] = useReducer2(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
1154
- const apiRef = useRef4(api);
2026
+ const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2027
+ const [state, dispatch] = useReducer4(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2028
+ const apiRef = useRef7(api);
1155
2029
  apiRef.current = api;
1156
- const activeSubjectKey = useRef4("");
2030
+ const activeSubjectKey = useRef7("");
1157
2031
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
1158
- const refresh = useCallback2(async () => {
2032
+ const refresh = useCallback4(async () => {
1159
2033
  const requestKey = `${subjectType}:${subjectId}`;
1160
2034
  dispatch({ type: "started" });
1161
2035
  try {
@@ -1168,7 +2042,7 @@ function useParticipantThread(params) {
1168
2042
  }
1169
2043
  }, [stableSubject, subjectType, subjectId]);
1170
2044
  const oldestMessageId = state.messages[0]?.id;
1171
- const loadOlder = useCallback2(async () => {
2045
+ const loadOlder = useCallback4(async () => {
1172
2046
  if (!oldestMessageId) return;
1173
2047
  const requestKey = `${subjectType}:${subjectId}`;
1174
2048
  try {
@@ -1180,9 +2054,9 @@ function useParticipantThread(params) {
1180
2054
  if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
1181
2055
  }
1182
2056
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
1183
- const handleSentMessage = useCallback2((message) => dispatch({ type: "sendConfirmed", message }), []);
2057
+ const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
1184
2058
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
1185
- useEffect6(() => {
2059
+ useEffect9(() => {
1186
2060
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
1187
2061
  dispatch({ type: "reset" });
1188
2062
  void refresh();
@@ -1190,12 +2064,19 @@ function useParticipantThread(params) {
1190
2064
  useParticipantRevalidation(api, (event) => {
1191
2065
  if (isForSubject(event, stableSubject)) void refresh();
1192
2066
  });
1193
- useEffect6(() => {
2067
+ useEffect9(() => {
1194
2068
  if (sendEntries.length === 0) return;
1195
2069
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
1196
2070
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
1197
- useParticipantMarkRead({ apiRef, subject: stableSubject, unreadCount, messageCount: state.messages.length, onMarkedRead });
1198
- const localPending = useMemo(() => toLocalPending(sendEntries), [sendEntries]);
2071
+ const unread = useParticipantUnread({ messages: state.messages, perspective, reportedUnreadCount: unreadCount, onMarkedRead });
2072
+ useParticipantMarkRead({
2073
+ apiRef,
2074
+ subject: stableSubject,
2075
+ unreadCount: unread.unreadCount,
2076
+ messageCount: state.messages.length,
2077
+ onMarkedRead: unread.handleMarkedRead
2078
+ });
2079
+ const localPending = useMemo2(() => toLocalPending(sendEntries), [sendEntries]);
1199
2080
  return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
1200
2081
  }
1201
2082
 
@@ -1204,7 +2085,7 @@ function useParticipantConversationView(params) {
1204
2085
  const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
1205
2086
  const { subjectType, subjectId } = subject;
1206
2087
  const sendEntries = selectSendEntries(sendStates, subject);
1207
- const dispatchSend = useCallback3(
2088
+ const dispatchSend = useCallback5(
1208
2089
  (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
1209
2090
  [dispatchSendStates, subjectType, subjectId]
1210
2091
  );
@@ -1216,9 +2097,10 @@ function useParticipantConversationView(params) {
1216
2097
  onMarkedRead: params.onMarkedRead,
1217
2098
  sendEntries,
1218
2099
  dispatchSend,
1219
- hostPending: pendingMessages
2100
+ hostPending: pendingMessages,
2101
+ perspective: params.perspective
1220
2102
  });
1221
- const items = useMemo2(
2103
+ const items = useMemo3(
1222
2104
  () => mergeParticipantMessages({
1223
2105
  serverMessages: thread.messages,
1224
2106
  hostPending: pendingMessages ?? [],
@@ -1230,8 +2112,30 @@ function useParticipantConversationView(params) {
1230
2112
  return { thread, items, sendEntries, dispatchSend };
1231
2113
  }
1232
2114
 
2115
+ // src/participant/useParticipantDraft.ts
2116
+ import { useState as useState5 } from "react";
2117
+ function useParticipantDraft(draftsRef, subject) {
2118
+ const [draft, setDraftState] = useState5(() => getDraft(draftsRef.current, subject));
2119
+ function replaceDrafts(next) {
2120
+ draftsRef.current = next;
2121
+ setDraftState(getDraft(next, subject));
2122
+ }
2123
+ function update(change) {
2124
+ replaceDrafts(setDraft(draftsRef.current, subject, { ...getDraft(draftsRef.current, subject), ...change }));
2125
+ }
2126
+ function takeForSend() {
2127
+ const taken = takeDraftForSend(draftsRef.current, subject);
2128
+ if (taken.content) replaceDrafts(taken.drafts);
2129
+ return taken.content;
2130
+ }
2131
+ function restoreFromEntry(entry) {
2132
+ update(appendEntryToDraft(entry, getDraft(draftsRef.current, subject)));
2133
+ }
2134
+ return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
2135
+ }
2136
+
1233
2137
  // src/participant/useParticipantThreadScroll.ts
1234
- import { useEffect as useEffect7, useRef as useRef6 } from "react";
2138
+ import { useEffect as useEffect10, useRef as useRef9 } from "react";
1235
2139
 
1236
2140
  // src/participant/participantNewMessages.ts
1237
2141
  function collectServerMessageIds(items) {
@@ -1248,15 +2152,28 @@ function latestSeenTime(previousIds, items) {
1248
2152
  }
1249
2153
  return latest;
1250
2154
  }
1251
- function countNewIncomingMessages(previousIds, currentItems) {
2155
+ function countNewIncomingMessages(previousIds, currentItems, perspective) {
1252
2156
  const latestSeen = latestSeenTime(previousIds, currentItems);
1253
2157
  return currentItems.filter(
1254
- (item) => item.kind === "server" && item.message.direction === "outbound" && !previousIds.has(item.message.id) && Date.parse(item.message.createdAt) > latestSeen
2158
+ (item) => item.kind === "server" && !isMessageMine(item.message, perspective) && !previousIds.has(item.message.id) && Date.parse(item.message.createdAt) > latestSeen
1255
2159
  ).length;
1256
2160
  }
1257
2161
 
2162
+ // src/participant/useScrollAwayState.ts
2163
+ import { useCallback as useCallback6 } from "react";
2164
+ function useScrollAwayState(ref, isNearBottom) {
2165
+ const scrollToLatest = useCallback6(() => {
2166
+ const element = ref.current;
2167
+ if (!element) return;
2168
+ element.scrollTo(
2169
+ scrollToLatestOptions({ scrollHeight: element.scrollHeight, isReducedMotion: readPrefersReducedMotion() })
2170
+ );
2171
+ }, [ref]);
2172
+ return { isAwayFromBottom: !isNearBottom, scrollToLatest };
2173
+ }
2174
+
1258
2175
  // src/participant/useStickToBottom.ts
1259
- import { useCallback as useCallback4, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
2176
+ import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect2, useRef as useRef8, useState as useState6 } from "react";
1260
2177
 
1261
2178
  // src/participant/participantScroll.ts
1262
2179
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -1278,11 +2195,17 @@ function resolveScrollAction(params) {
1278
2195
 
1279
2196
  // src/participant/useStickToBottom.ts
1280
2197
  function useStickToBottom({ firstKey, lastKey }) {
1281
- const ref = useRef5(null);
1282
- const snapshot = useRef5({ hasPositioned: false, firstKey: void 0, lastKey: void 0, scrollTop: 0, scrollHeight: 0 });
1283
- const [isNearBottom, setIsNearBottom] = useState5(true);
1284
- const isNearBottomRef = useRef5(true);
1285
- const onScroll = useCallback4((event) => {
2198
+ const ref = useRef8(null);
2199
+ const snapshot = useRef8({
2200
+ hasPositioned: false,
2201
+ firstKey: void 0,
2202
+ lastKey: void 0,
2203
+ scrollTop: 0,
2204
+ scrollHeight: 0
2205
+ });
2206
+ const [isNearBottom, setIsNearBottom] = useState6(true);
2207
+ const isNearBottomRef = useRef8(true);
2208
+ const onScroll = useCallback7((event) => {
1286
2209
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
1287
2210
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
1288
2211
  snapshot.current.scrollTop = scrollTop;
@@ -1290,7 +2213,7 @@ function useStickToBottom({ firstKey, lastKey }) {
1290
2213
  isNearBottomRef.current = isNear;
1291
2214
  setIsNearBottom(isNear);
1292
2215
  }, []);
1293
- useLayoutEffect(() => {
2216
+ useLayoutEffect2(() => {
1294
2217
  const element = ref.current;
1295
2218
  const previous = snapshot.current;
1296
2219
  if (!element) return;
@@ -1330,22 +2253,73 @@ function keyOf2(item) {
1330
2253
  if (!item) return void 0;
1331
2254
  return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
1332
2255
  }
1333
- function useParticipantThreadScroll(items) {
1334
- const { scroll, isNearBottom } = useStickToBottom({ firstKey: keyOf2(items[0]), lastKey: keyOf2(items[items.length - 1]) });
1335
- const seenIds = useRef6(/* @__PURE__ */ new Set());
1336
- useEffect7(() => {
2256
+ function useParticipantThreadScroll(items, perspective) {
2257
+ const { scroll: base, isNearBottom } = useStickToBottom({
2258
+ firstKey: keyOf2(items[0]),
2259
+ lastKey: keyOf2(items[items.length - 1])
2260
+ });
2261
+ const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2262
+ const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2263
+ const seenIds = useRef9(/* @__PURE__ */ new Set());
2264
+ useEffect10(() => {
1337
2265
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
1338
2266
  }, [isNearBottom, items]);
1339
- return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) };
2267
+ return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
2268
+ }
2269
+
2270
+ // src/participant/useParticipantThreadController.ts
2271
+ function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending }) {
2272
+ return {
2273
+ retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
2274
+ discardLocal: (clientMessageId) => dispatchSend({ type: "discarded", clientMessageId }),
2275
+ editLocal: handleEdit,
2276
+ ...onRetryPending ? { retryHost: onRetryPending } : {}
2277
+ };
2278
+ }
2279
+ function useParticipantThreadController(params) {
2280
+ const { api, subject, conversation, draftsRef } = params;
2281
+ const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject);
2282
+ const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params);
2283
+ const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
2284
+ const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective);
2285
+ function handleSend() {
2286
+ const content = takeForSend();
2287
+ if (content) void thread.send(content);
2288
+ }
2289
+ function handleEdit(clientMessageId) {
2290
+ const entry = findFailedEntry(sendEntries, clientMessageId);
2291
+ if (!entry) return;
2292
+ restoreFromEntry(entry);
2293
+ dispatchSend({ type: "discarded", clientMessageId });
2294
+ }
2295
+ return {
2296
+ conversation,
2297
+ items,
2298
+ hasMore: thread.hasMore,
2299
+ labels: params.labels,
2300
+ resolveAttachmentUrl,
2301
+ draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
2302
+ onSend: handleSend,
2303
+ status: thread.status,
2304
+ refresh: () => void thread.refresh(),
2305
+ scroll,
2306
+ newMessagesCount,
2307
+ isSending: hasSendingEntry(sendEntries),
2308
+ channel: params.channel,
2309
+ locale: params.locale,
2310
+ onLoadOlder: () => void thread.loadOlder(),
2311
+ pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
2312
+ perspective: params.perspective
2313
+ };
1340
2314
  }
1341
2315
 
1342
2316
  // src/participant/ParticipantThreadScreen.tsx
1343
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2317
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1344
2318
  function useOpenedConversation(api, subject, isNeeded) {
1345
- const [opened, setOpened] = useState6({ status: "idle" });
2319
+ const [opened, setOpened] = useState7({ status: "idle" });
1346
2320
  const { subjectType, subjectId } = subject;
1347
2321
  const canOpen = api.openConversation !== void 0;
1348
- useEffect8(() => {
2322
+ useEffect11(() => {
1349
2323
  if (!isNeeded || !canOpen) return;
1350
2324
  let isCurrent = true;
1351
2325
  setOpened({ status: "loading" });
@@ -1359,13 +2333,13 @@ function useOpenedConversation(api, subject, isNeeded) {
1359
2333
  }, [api, canOpen, isNeeded, subjectType, subjectId]);
1360
2334
  return opened;
1361
2335
  }
1362
- function NotFound({ labels, onBack }) {
1363
- return /* @__PURE__ */ jsxs11("div", { className: "cv-p cv-p-thread", children: [
1364
- /* @__PURE__ */ jsx11("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsxs11("button", { type: "button", className: "cv-p-button cv-p-thread__back", onClick: onBack, children: [
1365
- /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u2039" }),
1366
- /* @__PURE__ */ jsx11("span", { className: "cv-p-sr-only", children: labels.back })
1367
- ] }) : null }),
1368
- /* @__PURE__ */ jsx11("p", { className: "cv-p-empty", children: labels.notFound })
2336
+ function NotFound({
2337
+ labels,
2338
+ onBack
2339
+ }) {
2340
+ return /* @__PURE__ */ jsxs17("div", { className: "cv-p cv-p-thread", children: [
2341
+ /* @__PURE__ */ jsx24("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx24(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2342
+ /* @__PURE__ */ jsx24("p", { className: "cv-p-empty", children: labels.notFound })
1369
2343
  ] });
1370
2344
  }
1371
2345
  function ParticipantThreadScreen(props) {
@@ -1375,60 +2349,32 @@ function ParticipantThreadScreen(props) {
1375
2349
  );
1376
2350
  const opened = useOpenedConversation(api, selected, listed === void 0);
1377
2351
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
1378
- if (conversation) return /* @__PURE__ */ jsx11(LoadedThread, { ...props, conversation });
2352
+ if (conversation) return /* @__PURE__ */ jsx24(LoadedThread, { ...props, conversation });
1379
2353
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
1380
2354
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
1381
- if (isResolving || api.openConversation === void 0 && isListLoading) return /* @__PURE__ */ jsx11("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
1382
- return /* @__PURE__ */ jsx11(NotFound, { labels, onBack: props.onBack });
2355
+ if (isResolving || api.openConversation === void 0 && isListLoading)
2356
+ return /* @__PURE__ */ jsx24("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2357
+ return /* @__PURE__ */ jsx24(NotFound, { labels, onBack: props.onBack });
1383
2358
  }
1384
2359
  function LoadedThread(props) {
1385
- const { api, selected, conversation, draftsRef, inbox } = props;
1386
- const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, selected);
1387
- const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView({
2360
+ const core = useParticipantThreadController({
1388
2361
  ...props,
1389
- subject: selected,
1390
- onMarkedRead: inbox.markSubjectRead
2362
+ subject: props.selected,
2363
+ onMarkedRead: props.inbox.markSubjectRead
1391
2364
  });
1392
- const resolveAttachmentUrl = useMemo3(() => bindAttachmentUrlResolver(api), [api]);
1393
- const { scroll, newMessagesCount } = useParticipantThreadScroll(items);
1394
- function handleSend() {
1395
- const content = takeForSend();
1396
- if (content) void thread.send(content);
1397
- }
1398
- function handleEdit(clientMessageId) {
1399
- const entry = findFailedEntry(sendEntries, clientMessageId);
1400
- if (!entry) return;
1401
- restoreFromEntry(entry);
1402
- dispatchSend({ type: "discarded", clientMessageId });
1403
- }
1404
- return /* @__PURE__ */ jsx11(
2365
+ return /* @__PURE__ */ jsx24(
1405
2366
  ParticipantThread,
1406
2367
  {
1407
- conversation,
1408
- items,
1409
- hasMore: thread.hasMore,
1410
- labels: props.labels,
1411
- resolveAttachmentUrl,
1412
- draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
1413
- onSend: handleSend,
1414
- status: thread.status,
1415
- refresh: () => void thread.refresh(),
1416
- scroll,
1417
- newMessagesCount,
1418
- isSending: hasSendingEntry(sendEntries),
1419
- channel: props.channel,
1420
- locale: props.locale,
2368
+ ...core,
1421
2369
  onBack: props.onBack,
1422
2370
  onOpenSubject: props.onOpenSubject,
1423
- onLoadOlder: () => void thread.loadOlder(),
1424
- pendingActions: {
1425
- retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
1426
- discardLocal: (clientMessageId) => dispatchSend({ type: "discarded", clientMessageId }),
1427
- editLocal: handleEdit,
1428
- ...props.onRetryPending ? { retryHost: props.onRetryPending } : {}
1429
- },
1430
2371
  quickReplies: props.quickReplies,
1431
- renderSubjectCard: props.renderSubjectCard
2372
+ renderSubjectCard: props.renderSubjectCard,
2373
+ subjectGroups: props.subjectGroups,
2374
+ renderSubjectIcon: props.renderSubjectIcon,
2375
+ avatars: props.avatars,
2376
+ renderAuthorAvatar: props.renderAuthorAvatar,
2377
+ tail: props.tail
1432
2378
  }
1433
2379
  );
1434
2380
  }
@@ -1554,16 +2500,16 @@ function buildParticipantThemeStyle(theme) {
1554
2500
  }
1555
2501
 
1556
2502
  // src/participant/ParticipantConversationsScreen.tsx
1557
- import { jsx as jsx12 } from "react/jsx-runtime";
2503
+ import { jsx as jsx25 } from "react/jsx-runtime";
1558
2504
  var ALL_FILTER3 = "all";
1559
2505
  function ParticipantConversationsScreen(props) {
1560
2506
  const { selected, inbox, subjectGroups, classNames } = props;
1561
- const [filter, setFilter] = useState7(ALL_FILTER3);
1562
- const [searchQuery, setSearchQuery] = useState7("");
1563
- const draftsRef = useRef7(/* @__PURE__ */ new Map());
1564
- const [sendStates, dispatchSendStates] = useReducer3(participantSendStatesReducer, /* @__PURE__ */ new Map());
1565
- const labels = useMemo4(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
1566
- const view = useMemo4(
2507
+ const [filter, setFilter] = useState8(ALL_FILTER3);
2508
+ const [searchQuery, setSearchQuery] = useState8("");
2509
+ const draftsRef = useRef10(/* @__PURE__ */ new Map());
2510
+ const [sendStates, dispatchSendStates] = useReducer5(participantSendStatesReducer, /* @__PURE__ */ new Map());
2511
+ const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2512
+ const view = useMemo5(
1567
2513
  () => groupParticipantConversations({
1568
2514
  conversations: inbox.conversations,
1569
2515
  subjectGroups,
@@ -1574,7 +2520,7 @@ function ParticipantConversationsScreen(props) {
1574
2520
  );
1575
2521
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
1576
2522
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
1577
- return /* @__PURE__ */ jsx12("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx12("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx12(
2523
+ return /* @__PURE__ */ jsx25("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
1578
2524
  ParticipantInbox,
1579
2525
  {
1580
2526
  view,
@@ -1591,7 +2537,7 @@ function ParticipantConversationsScreen(props) {
1591
2537
  renderSubjectIcon: props.renderSubjectIcon,
1592
2538
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
1593
2539
  }
1594
- ) }) : /* @__PURE__ */ jsx12("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx12(
2540
+ ) }) : /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
1595
2541
  ParticipantThreadScreen,
1596
2542
  {
1597
2543
  ...props,
@@ -1606,7 +2552,7 @@ function ParticipantConversationsScreen(props) {
1606
2552
  }
1607
2553
 
1608
2554
  // src/participant/useParticipantInbox.ts
1609
- import { useCallback as useCallback5, useEffect as useEffect9, useReducer as useReducer4 } from "react";
2555
+ import { useCallback as useCallback8, useEffect as useEffect12, useReducer as useReducer6 } from "react";
1610
2556
 
1611
2557
  // src/participant/participantInboxState.ts
1612
2558
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -1656,8 +2602,8 @@ function describeError2(error) {
1656
2602
  return error instanceof Error ? error.message : "unknown error";
1657
2603
  }
1658
2604
  function useParticipantInbox(api) {
1659
- const [state, dispatch] = useReducer4(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
1660
- const refresh = useCallback5(async () => {
2605
+ const [state, dispatch] = useReducer6(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2606
+ const refresh = useCallback8(async () => {
1661
2607
  dispatch({ type: "started" });
1662
2608
  try {
1663
2609
  const page = await api.listConversations();
@@ -1667,7 +2613,7 @@ function useParticipantInbox(api) {
1667
2613
  }
1668
2614
  }, [api]);
1669
2615
  const { nextCursor } = state;
1670
- const loadMore = useCallback5(async () => {
2616
+ const loadMore = useCallback8(async () => {
1671
2617
  if (!nextCursor) return;
1672
2618
  try {
1673
2619
  const page = await api.listConversations({ cursor: nextCursor });
@@ -1676,10 +2622,10 @@ function useParticipantInbox(api) {
1676
2622
  dispatch({ type: "failed", error: describeError2(error) });
1677
2623
  }
1678
2624
  }, [api, nextCursor]);
1679
- const markSubjectRead = useCallback5((subject) => {
2625
+ const markSubjectRead = useCallback8((subject) => {
1680
2626
  dispatch({ type: "markedRead", subject });
1681
2627
  }, []);
1682
- useEffect9(() => {
2628
+ useEffect12(() => {
1683
2629
  void refresh();
1684
2630
  }, [refresh]);
1685
2631
  useParticipantRevalidation(api, () => {
@@ -1689,11 +2635,11 @@ function useParticipantInbox(api) {
1689
2635
  }
1690
2636
 
1691
2637
  // src/participant/ParticipantConversations.tsx
1692
- import { jsx as jsx13 } from "react/jsx-runtime";
2638
+ import { jsx as jsx26 } from "react/jsx-runtime";
1693
2639
  function ParticipantConversations(props) {
1694
2640
  const inbox = useParticipantInbox(props.api);
1695
2641
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
1696
- return /* @__PURE__ */ jsx13(
2642
+ return /* @__PURE__ */ jsx26(
1697
2643
  ParticipantConversationsScreen,
1698
2644
  {
1699
2645
  ...props,
@@ -1701,7 +2647,111 @@ function ParticipantConversations(props) {
1701
2647
  }
1702
2648
  );
1703
2649
  }
2650
+
2651
+ // src/participant/ConversationThread.tsx
2652
+ import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer7, useRef as useRef11 } from "react";
2653
+
2654
+ // src/participant/conversationThreadApi.ts
2655
+ function toParticipantApi(api) {
2656
+ const { subscribe } = api;
2657
+ return {
2658
+ listConversations: async () => ({ data: [] }),
2659
+ fetchMessages: (subject, params) => api.fetchMessages(subject, params),
2660
+ sendMessage: (input) => api.sendMessage(input),
2661
+ markRead: async (subject) => {
2662
+ if (!api.markRead) throw new Error("The host api has no markRead");
2663
+ await api.markRead(subject);
2664
+ },
2665
+ resolveAttachmentUrl: (attachment, disposition) => api.resolveAttachmentUrl(attachment, disposition),
2666
+ ...subscribe ? { subscribe: (listener) => api.subscribe?.(listener) ?? (() => void 0) } : {}
2667
+ };
2668
+ }
2669
+
2670
+ // src/participant/conversationThreadSummary.ts
2671
+ function buildThreadSummary({
2672
+ subject,
2673
+ title,
2674
+ protocol,
2675
+ channels,
2676
+ status
2677
+ }) {
2678
+ return {
2679
+ subjectType: subject.subjectType,
2680
+ subjectId: subject.subjectId,
2681
+ subjectLabel: title,
2682
+ lastMessageAt: null,
2683
+ unreadCount: 0,
2684
+ awaitingParticipant: false,
2685
+ status: status ?? "open",
2686
+ ...protocol ? { protocol } : {},
2687
+ ...channels ? { channels } : {}
2688
+ };
2689
+ }
2690
+
2691
+ // src/participant/ConversationThread.tsx
2692
+ import { jsx as jsx27 } from "react/jsx-runtime";
2693
+ function useConversationThreadCore(props) {
2694
+ const { perspective = "participant" } = props;
2695
+ const { subjectType, subjectId } = props.subject;
2696
+ const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2697
+ const onMarkedReadRef = useRef11(props.onMarkedRead);
2698
+ onMarkedReadRef.current = props.onMarkedRead;
2699
+ const handleMarkedRead = useCallback9((marked) => onMarkedReadRef.current?.(marked), []);
2700
+ const api = useMemo6(() => toParticipantApi(props.api), [props.api]);
2701
+ const draftsRef = useRef11(/* @__PURE__ */ new Map());
2702
+ const [sendStates, dispatchSendStates] = useReducer7(participantSendStatesReducer, /* @__PURE__ */ new Map());
2703
+ const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2704
+ const { title, protocol, channels, status } = props;
2705
+ const conversation = useMemo6(
2706
+ () => buildThreadSummary({ subject, title, protocol, channels, status }),
2707
+ [subject, title, protocol, channels, status]
2708
+ );
2709
+ return useParticipantThreadController({
2710
+ api,
2711
+ subject,
2712
+ conversation,
2713
+ labels,
2714
+ draftsRef,
2715
+ sendStates,
2716
+ dispatchSendStates,
2717
+ onMarkedRead: handleMarkedRead,
2718
+ channel: props.channel,
2719
+ locale: props.locale,
2720
+ pendingMessages: props.pendingMessages,
2721
+ onRetryPending: props.onRetryPending,
2722
+ perspective
2723
+ });
2724
+ }
2725
+ function ConversationThreadBody(props) {
2726
+ const core = useConversationThreadCore(props);
2727
+ return /* @__PURE__ */ jsx27(
2728
+ "div",
2729
+ {
2730
+ className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2731
+ style: buildParticipantThemeStyle(props.theme),
2732
+ children: /* @__PURE__ */ jsx27("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx27(
2733
+ ParticipantThread,
2734
+ {
2735
+ ...core,
2736
+ onBack: props.onBack,
2737
+ quickReplies: props.quickReplies,
2738
+ subjectGroups: props.subjectGroups,
2739
+ renderSubjectIcon: props.renderSubjectIcon,
2740
+ avatars: props.avatars,
2741
+ renderAuthorAvatar: props.renderAuthorAvatar,
2742
+ tail: props.tail,
2743
+ counterpartLabel: props.counterpartLabel,
2744
+ headerActions: props.headerActions
2745
+ }
2746
+ ) })
2747
+ }
2748
+ );
2749
+ }
2750
+ function ConversationThread(props) {
2751
+ return /* @__PURE__ */ jsx27(ConversationThreadBody, { ...props }, draftKey(props.subject));
2752
+ }
1704
2753
  export {
2754
+ ConversationThread,
1705
2755
  DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
1706
2756
  ParticipantConversations,
1707
2757
  groupParticipantConversations