@adatechnology/conversations-ui 0.8.0 → 0.10.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 (129) hide show
  1. package/README.md +106 -0
  2. package/dist/{ConversationSimulatorPanel-q-wVgBCN.d.ts → ConversationSimulatorPanel-P6eqTSAO.d.ts} +1 -16
  3. package/dist/{chunk-HMCOTZAX.js → chunk-NSOAUYNE.js} +33 -28
  4. package/dist/{chunk-O6457XHK.js → chunk-WCZNIJ3M.js} +16 -1
  5. package/dist/{chunk-L3OFAQPS.js → chunk-Y326LLEP.js} +4 -14
  6. package/dist/chunk-ZSOD5AQT.js +19 -0
  7. package/dist/flows/index.js +4 -2
  8. package/dist/index.d.ts +10 -3
  9. package/dist/index.js +8 -6
  10. package/dist/participant/index.d.ts +54 -3
  11. package/dist/participant/index.js +1722 -418
  12. package/dist/preview/index.d.ts +2 -2
  13. package/dist/preview/index.js +2 -2
  14. package/dist/styles.css +281 -23
  15. package/package.json +3 -2
  16. package/src/AudioRecorderButton.tsx +4 -28
  17. package/src/Tooltip.tsx +5 -12
  18. package/src/TooltipDom.test.tsx +78 -0
  19. package/src/audioRecorderFormat.ts +27 -0
  20. package/src/domHarness.test-helper.ts +53 -0
  21. package/src/participant/ConversationThread.tsx +4 -0
  22. package/src/participant/ParticipantAttachmentItem.tsx +7 -10
  23. package/src/participant/ParticipantAudioAttachment.tsx +34 -0
  24. package/src/participant/ParticipantAudioPlayer.tsx +68 -0
  25. package/src/participant/ParticipantAudioPlayerControls.tsx +68 -0
  26. package/src/participant/ParticipantAudioRecorderView.tsx +55 -0
  27. package/src/participant/ParticipantAudioTranscription.tsx +90 -0
  28. package/src/participant/ParticipantBackButton.tsx +1 -1
  29. package/src/participant/ParticipantBubbleBody.tsx +54 -0
  30. package/src/participant/ParticipantBubbleStatus.tsx +22 -28
  31. package/src/participant/ParticipantChannelBadges.tsx +10 -2
  32. package/src/participant/ParticipantComposer.tsx +51 -16
  33. package/src/participant/ParticipantComposerAudio.tsx +95 -0
  34. package/src/participant/ParticipantComposerControls.tsx +2 -0
  35. package/src/participant/ParticipantConversations.tsx +3 -0
  36. package/src/participant/ParticipantCopyableToken.tsx +1 -0
  37. package/src/participant/ParticipantFailedMenu.tsx +40 -0
  38. package/src/participant/ParticipantFailedMenuList.tsx +41 -0
  39. package/src/participant/ParticipantMessageBubble.test.tsx +6 -6
  40. package/src/participant/ParticipantMessageBubble.tsx +20 -52
  41. package/src/participant/ParticipantMessageText.test.tsx +1 -1
  42. package/src/participant/ParticipantMessageText.tsx +4 -2
  43. package/src/participant/ParticipantProtocolBadge.tsx +1 -0
  44. package/src/participant/ParticipantRecorderParts.tsx +118 -0
  45. package/src/participant/ParticipantScrollToLatest.tsx +1 -1
  46. package/src/participant/ParticipantThread.tsx +30 -12
  47. package/src/participant/ParticipantThreadHeader.test.tsx +1 -1
  48. package/src/participant/ParticipantThreadScreen.tsx +1 -0
  49. package/src/participant/failedMenuKeyboard.ts +29 -0
  50. package/src/participant/index.ts +3 -0
  51. package/src/participant/participantAudio.types.ts +10 -0
  52. package/src/participant/participantAudioCapture.test.ts +15 -0
  53. package/src/participant/participantAudioCapture.ts +85 -0
  54. package/src/participant/participantAudioCoordinator.ts +13 -0
  55. package/src/participant/participantAudioFormat.test.ts +93 -0
  56. package/src/participant/participantAudioFormat.ts +72 -0
  57. package/src/participant/participantAudioGolden.test.tsx +168 -0
  58. package/src/participant/participantAudioLabels.test.ts +22 -0
  59. package/src/participant/participantAudioLabels.ts +76 -0
  60. package/src/participant/participantAudioPlayerControlsDom.test.tsx +123 -0
  61. package/src/participant/participantAudioPlayerCss.test.ts +63 -0
  62. package/src/participant/participantAudioPlayerDom.test.tsx +154 -0
  63. package/src/participant/participantAudioPlayerDomKit.test-helper.tsx +84 -0
  64. package/src/participant/participantAudioPlayerState.ts +35 -0
  65. package/src/participant/participantAudioRecorderDom.test.tsx +127 -0
  66. package/src/participant/participantAudioRecorderInterruptionDom.test.tsx +135 -0
  67. package/src/participant/participantAudioRecorderSpecDom.test.tsx +110 -0
  68. package/src/participant/participantAudioRecorderState.test.ts +144 -0
  69. package/src/participant/participantAudioRecorderState.ts +93 -0
  70. package/src/participant/participantAudioRecorderView.test.tsx +98 -0
  71. package/src/participant/participantAudioSupport.ts +36 -0
  72. package/src/participant/participantAudioTranscriptionDom.test.tsx +198 -0
  73. package/src/participant/participantComposerAudioDom.test.tsx +185 -0
  74. package/src/participant/participantComposerAudioGolden.test.tsx +68 -0
  75. package/src/participant/participantComposerAudioSsr.test.tsx +74 -0
  76. package/src/participant/participantFailedMenuDom.test.tsx +154 -0
  77. package/src/participant/participantFailedMenuState.test.ts +61 -0
  78. package/src/participant/participantFailedMenuState.ts +57 -0
  79. package/src/participant/participantFailedMessage.test.tsx +83 -0
  80. package/src/participant/participantFileValidation.ts +1 -1
  81. package/src/participant/participantGolden.test.tsx +2 -0
  82. package/src/participant/participantGoldenScenarios.test-helper.tsx +14 -1
  83. package/src/participant/participantLabels.ts +10 -1
  84. package/src/participant/participantMediaFakes.test-helper.ts +82 -0
  85. package/src/participant/participantNoMicrophoneDom.test.tsx +44 -0
  86. package/src/participant/participantPublicTypes.test.ts +11 -0
  87. package/src/participant/participantRecorderDomKit.test-helper.tsx +131 -0
  88. package/src/participant/participantRecorderFakes.test-helper.ts +47 -0
  89. package/src/participant/participantSpecRecorderFake.test-helper.ts +52 -0
  90. package/src/participant/participantThemeContrast.test.ts +17 -0
  91. package/src/participant/participantTooltips.test.tsx +74 -0
  92. package/src/participant/test-fixtures/composer-audio-denied.golden.html +1 -0
  93. package/src/participant/test-fixtures/composer-audio-idle.golden.html +1 -0
  94. package/src/participant/test-fixtures/composer-audio-recording.golden.html +1 -0
  95. package/src/participant/test-fixtures/composer-audio-review.golden.html +1 -0
  96. package/src/participant/test-fixtures/thread-audio-native.golden.html +1 -0
  97. package/src/participant/test-fixtures/thread-audio-player.golden.html +1 -0
  98. package/src/participant/test-fixtures/thread-audio-transcription-empty.golden.html +1 -0
  99. package/src/participant/test-fixtures/thread-audio-transcription-pending.golden.html +1 -0
  100. package/src/participant/test-fixtures/thread-audio-transcription-ready.golden.html +1 -0
  101. package/src/participant/test-fixtures/thread-audio-transcription-too_long.golden.html +1 -0
  102. package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -1
  103. package/src/participant/test-fixtures/thread-closed.golden.html +1 -1
  104. package/src/participant/test-fixtures/thread-composer-microphone-idle.golden.html +1 -0
  105. package/src/participant/test-fixtures/thread-default.golden.html +1 -1
  106. package/src/participant/test-fixtures/thread-empty-draft.golden.html +1 -0
  107. package/src/participant/test-fixtures/thread-error.golden.html +1 -1
  108. package/src/participant/test-fixtures/thread-loading.golden.html +1 -1
  109. package/src/participant/test-fixtures/thread-plain.golden.html +1 -1
  110. package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -1
  111. package/src/participant/useFailedMenuEffects.ts +59 -0
  112. package/src/participant/useParticipantAudioMediaEvents.ts +46 -0
  113. package/src/participant/useParticipantAudioPlayer.ts +52 -0
  114. package/src/participant/useParticipantAudioPlayerActions.ts +47 -0
  115. package/src/participant/useParticipantAudioRecorder.ts +153 -0
  116. package/src/participant/useParticipantAudioRecorderCore.ts +55 -0
  117. package/src/participant/useParticipantAudioRecorderEffects.ts +70 -0
  118. package/src/participant/useParticipantAudioSourceRetry.ts +42 -0
  119. package/src/participant/useParticipantFailedMenu.ts +60 -0
  120. package/src/participant/useParticipantSendAudio.test.tsx +85 -0
  121. package/src/participant/useParticipantSendAudio.ts +28 -0
  122. package/src/participant/useParticipantThreadController.ts +20 -7
  123. package/src/participant/useStableAttachmentUrl.ts +71 -0
  124. package/src/participant/useSuppressTouchTooltips.ts +38 -0
  125. package/src/participant/useTranscriptionOverflow.ts +25 -0
  126. package/src/participant/useTranscriptionReadyAnnouncement.ts +24 -0
  127. package/src/styles.css +48 -5
  128. package/src/tooltipLayerRegistry.test.ts +56 -0
  129. package/src/tooltipLayerRegistry.ts +34 -0
@@ -1,13 +1,18 @@
1
1
  import {
2
+ TooltipLayer
3
+ } from "../chunk-NSOAUYNE.js";
4
+ import {
5
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
2
6
  StatusTicks,
3
7
  channelCapabilityFor,
4
8
  formatFileSize,
5
9
  formatTimestamp,
6
- isSameDay
7
- } from "../chunk-O6457XHK.js";
10
+ isSameDay,
11
+ resolveRecordingFormat
12
+ } from "../chunk-WCZNIJ3M.js";
8
13
 
9
14
  // src/participant/ParticipantConversationsScreen.tsx
10
- import { useMemo as useMemo5, useReducer as useReducer5, useRef as useRef10, useState as useState8 } from "react";
15
+ import { useMemo as useMemo6, useReducer as useReducer6, useRef as useRef21, useState as useState14 } from "react";
11
16
 
12
17
  // src/participant/participantLoadView.ts
13
18
  function resolveLoadView({ status, hasItems }) {
@@ -18,7 +23,47 @@ function resolveLoadView({ status, hasItems }) {
18
23
  };
19
24
  }
20
25
 
26
+ // src/participant/participantAudioLabels.ts
27
+ var DEFAULT_PARTICIPANT_AUDIO_LABELS = {
28
+ recordAudio: "Record audio",
29
+ recordingInProgress: "Recording",
30
+ stopRecording: "Stop recording",
31
+ recordingElapsed: "{elapsed} of {limit}",
32
+ reviewAudio: "Listen before sending",
33
+ sendAudio: "Send audio",
34
+ discardAudio: "Discard audio",
35
+ microphoneDenied: "No permission to use the microphone.",
36
+ recordingUnsupported: "This device cannot record audio.",
37
+ audioEmpty: "Nothing was captured by the microphone.",
38
+ audioTooLarge: "Audio too large to send.",
39
+ audioPlayer: "Audio",
40
+ playAudio: "Play audio",
41
+ pauseAudio: "Pause audio",
42
+ audioPosition: "Audio position",
43
+ audioPositionValue: "{position} of {duration}",
44
+ playbackRate: "Playback speed {rate}",
45
+ audioUnavailable: "Audio unavailable",
46
+ transcribing: "Transcribing\u2026",
47
+ transcriptionReady: "Transcription ready",
48
+ machineTranscription: "Transcription generated by machine",
49
+ emptyTranscription: "Nothing was said in the audio",
50
+ transcriptionTooLong: "Transcription only for audios shorter than {minutes} minutes",
51
+ showFullTranscription: "Show all",
52
+ collapseTranscription: "Show less"
53
+ };
54
+ function resolveParticipantAudioLabels(labels) {
55
+ const resolved = { ...DEFAULT_PARTICIPANT_AUDIO_LABELS };
56
+ for (const key of Object.keys(resolved)) {
57
+ resolved[key] = labels?.[key] ?? DEFAULT_PARTICIPANT_AUDIO_LABELS[key];
58
+ }
59
+ return resolved;
60
+ }
61
+ function formatRecordingElapsedLabel(template, elapsed, limit) {
62
+ return template.replace("{elapsed}", elapsed).replace("{limit}", limit);
63
+ }
64
+
21
65
  // src/participant/participantLabels.ts
66
+ var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
22
67
  var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
23
68
  inboxTitle: "Conversations",
24
69
  sectionAwaiting: "Waiting for your reply",
@@ -55,6 +100,7 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
55
100
  retry: "Retry",
56
101
  discard: "Discard",
57
102
  edit: "Edit",
103
+ messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
58
104
  notFound: "Conversation not found",
59
105
  loading: "Loading\u2026",
60
106
  loadError: "Could not load. Check your connection and try again.",
@@ -75,7 +121,8 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
75
121
  channelWebchat: "Web chat",
76
122
  externalLinkHint: "opens in a new tab",
77
123
  copyValue: "Copy",
78
- valueCopied: "Copied"
124
+ valueCopied: "Copied",
125
+ ...DEFAULT_PARTICIPANT_AUDIO_LABELS
79
126
  };
80
127
  function formatParticipantLabel(template, count) {
81
128
  return template.replace("{count}", String(count));
@@ -128,18 +175,34 @@ function ParticipantChannelBadges({
128
175
  if (variant === "inline") {
129
176
  return /* @__PURE__ */ jsxs("span", { className: "cv-p-channels", children: [
130
177
  /* @__PURE__ */ jsx("span", { className: "cv-p-sr-only", children: `${labels.channelsGroup}: ` }),
131
- descriptors.map((descriptor, index) => /* @__PURE__ */ jsx("span", { className: `cv-p-channel cv-p-channel--${descriptor.channel}`, children: /* @__PURE__ */ jsx(
132
- BadgeContent,
178
+ descriptors.map((descriptor, index) => /* @__PURE__ */ jsx(
179
+ "span",
133
180
  {
134
- descriptor,
135
- labels,
136
- separator: index < descriptors.length - 1 ? ", " : "",
137
- iconSize
138
- }
139
- ) }, descriptor.channel))
181
+ className: `cv-p-channel cv-p-channel--${descriptor.channel}`,
182
+ "data-cv-tooltip": resolveParticipantChannelLabel(descriptor, labels),
183
+ children: /* @__PURE__ */ jsx(
184
+ BadgeContent,
185
+ {
186
+ descriptor,
187
+ labels,
188
+ separator: index < descriptors.length - 1 ? ", " : "",
189
+ iconSize
190
+ }
191
+ )
192
+ },
193
+ descriptor.channel
194
+ ))
140
195
  ] });
141
196
  }
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)) });
197
+ return /* @__PURE__ */ jsx("ul", { className: "cv-p-channels", "aria-label": labels.channelsGroup, children: descriptors.map((descriptor) => /* @__PURE__ */ jsx(
198
+ "li",
199
+ {
200
+ className: `cv-p-channel cv-p-channel--${descriptor.channel}`,
201
+ "data-cv-tooltip": resolveParticipantChannelLabel(descriptor, labels),
202
+ children: /* @__PURE__ */ jsx(BadgeContent, { descriptor, labels, separator: "", iconSize })
203
+ },
204
+ descriptor.channel
205
+ )) });
143
206
  }
144
207
 
145
208
  // src/participant/participantSubjectIcon.ts
@@ -361,21 +424,828 @@ function ParticipantInbox(props) {
361
424
  }
362
425
 
363
426
  // src/participant/ParticipantThreadScreen.tsx
364
- import { useEffect as useEffect11, useState as useState7 } from "react";
427
+ import { useEffect as useEffect20, useState as useState13 } from "react";
365
428
 
366
429
  // src/participant/ParticipantBackButton.tsx
367
430
  import { ArrowLeft } from "lucide-react";
368
431
  import { jsx as jsx6 } from "react/jsx-runtime";
369
432
  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" }) });
433
+ return /* @__PURE__ */ jsx6("button", { type: "button", className: "cv-p-thread__back", "aria-label": label, "data-cv-tooltip": label, onClick: onBack, children: /* @__PURE__ */ jsx6(ArrowLeft, { size: 24, "aria-hidden": "true", focusable: "false" }) });
371
434
  }
372
435
 
436
+ // src/participant/ParticipantThread.tsx
437
+ import { useRef as useRef13 } from "react";
438
+
373
439
  // src/participant/ParticipantComposer.tsx
374
- import { useState } from "react";
440
+ import { useState as useState3 } from "react";
441
+
442
+ // src/participant/ParticipantComposerAudio.tsx
443
+ import { useEffect as useEffect5, useRef as useRef6 } from "react";
444
+
445
+ // src/participant/participantFileValidation.ts
446
+ function matchesAcceptedType(mimeType, acceptedTypes) {
447
+ return acceptedTypes.some(
448
+ (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
449
+ );
450
+ }
451
+ function rejectionOf(file, params) {
452
+ if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
453
+ if (file.size > params.maxBytes) return "tooLarge";
454
+ return void 0;
455
+ }
456
+ function reduceFilesSelection(params) {
457
+ const files = [...params.current];
458
+ const rejected = [];
459
+ for (const file of params.incoming) {
460
+ const reason = rejectionOf(file, params);
461
+ if (reason === void 0) files.push(file);
462
+ else rejected.push({ file, reason });
463
+ }
464
+ return { files, rejected };
465
+ }
466
+ function maxBytesForChannel(channel = "app") {
467
+ return channelCapabilityFor(channel).attachments.maxBytes;
468
+ }
469
+
470
+ // src/participant/participantAudio.types.ts
471
+ var DEFAULT_MAX_RECORDING_BYTES = 16 * 1024 * 1024;
472
+
473
+ // src/participant/participantAudioRecorderState.ts
474
+ var MINIMUM_RECORDING_MILLISECONDS = 500;
475
+ var MUTE_INTERRUPTION_DELAY_MS = 3e3;
476
+ var RETRYABLE = ["idle", "denied", "unsupported", "empty", "tooLarge"];
477
+ var TRANSITIONS = {
478
+ request: [[RETRYABLE, "requesting"]],
479
+ granted: [[["requesting"], "recording"]],
480
+ microphoneDenied: [[["requesting"], "denied"]],
481
+ microphoneUnavailable: [[["requesting"], "unsupported"]],
482
+ capturedReview: [[["recording"], "review"]],
483
+ capturedEmpty: [[["recording"], "empty"]],
484
+ capturedTooLarge: [[["recording"], "tooLarge"]],
485
+ cancel: [[["requesting", "recording", "review"], "idle"]]
486
+ };
487
+ function canStartRecording(status) {
488
+ return RETRYABLE.includes(status);
489
+ }
490
+ function reduceRecorderStatus(status, event) {
491
+ const match = TRANSITIONS[event].find(([from]) => from.includes(status));
492
+ return match ? match[1] : status;
493
+ }
494
+ function resolveInterruptionAction(status) {
495
+ if (status === "recording") return "stop";
496
+ if (status === "review") return "pause";
497
+ if (status === "requesting") return "cancel";
498
+ return "ignore";
499
+ }
500
+ function lowerOnly(requested, ceiling) {
501
+ if (requested === void 0 || !Number.isFinite(requested) || requested <= 0) return ceiling;
502
+ return Math.min(requested, ceiling);
503
+ }
504
+ function resolveRecordingLimits({ options, channelMaxBytes }) {
505
+ const sizeCeiling = lowerOnly(channelMaxBytes, DEFAULT_MAX_RECORDING_BYTES);
506
+ return {
507
+ maxDurationMs: lowerOnly(options?.maxDurationMs, DEFAULT_MAX_RECORDING_MILLISECONDS),
508
+ maxSizeBytes: lowerOnly(options?.maxSizeBytes, sizeCeiling)
509
+ };
510
+ }
511
+ function classifyCapture({ sizeBytes, durationMs }, limits) {
512
+ if (sizeBytes === 0 || durationMs < MINIMUM_RECORDING_MILLISECONDS) return "capturedEmpty";
513
+ if (sizeBytes > limits.maxSizeBytes) return "capturedTooLarge";
514
+ return "capturedReview";
515
+ }
516
+ function classifyMicrophoneError(error) {
517
+ const name = typeof error === "object" && error !== null && "name" in error ? error.name : void 0;
518
+ return name === "NotAllowedError" || name === "SecurityError" ? "microphoneDenied" : "microphoneUnavailable";
519
+ }
520
+ function formatRecordingClock(milliseconds) {
521
+ const totalSeconds = Math.floor(Math.max(0, milliseconds) / 1e3);
522
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
523
+ return `${Math.floor(totalSeconds / 60)}:${seconds}`;
524
+ }
525
+
526
+ // src/participant/ParticipantRecorderParts.tsx
527
+ import { Mic, Send, Square, Trash2 } from "lucide-react";
528
+
529
+ // src/participant/participantAudioFormat.ts
530
+ var PLAYBACK_RATES = [1, 1.5, 2];
531
+ var SEEK_STEP_SECONDS = 5;
532
+ var UNKNOWN_CLOCK = "--:--";
533
+ function formatAudioClock(seconds) {
534
+ if (seconds === void 0 || !Number.isFinite(seconds) || seconds < 0) return UNKNOWN_CLOCK;
535
+ const wholeSeconds = Math.floor(seconds);
536
+ const remainder = String(wholeSeconds % 60).padStart(2, "0");
537
+ return `${Math.floor(wholeSeconds / 60)}:${remainder}`;
538
+ }
539
+ function nextRateIndex(index) {
540
+ return (index + 1) % PLAYBACK_RATES.length;
541
+ }
542
+ function formatPlaybackRate(rate) {
543
+ return `${rate}\xD7`;
544
+ }
545
+ function resolveServerDurationSeconds(durationMs) {
546
+ if (durationMs === void 0 || !Number.isFinite(durationMs) || durationMs <= 0) return void 0;
547
+ return durationMs / 1e3;
548
+ }
549
+ function resolveMediaDurationSeconds(duration) {
550
+ return Number.isFinite(duration) && duration > 0 ? duration : void 0;
551
+ }
552
+ function clampPosition(position, duration) {
553
+ const floored = Math.max(0, position);
554
+ return duration === void 0 ? floored : Math.min(duration, floored);
555
+ }
556
+ function resolveSeekKeyTarget({ key, position, duration }) {
557
+ if (key === "ArrowRight" || key === "ArrowUp") return clampPosition(position + SEEK_STEP_SECONDS, duration);
558
+ if (key === "ArrowLeft" || key === "ArrowDown") return clampPosition(position - SEEK_STEP_SECONDS, duration);
559
+ if (key === "Home") return 0;
560
+ if (key === "End") return duration;
561
+ return void 0;
562
+ }
563
+ function resolveSeekMax({ duration, maxSeen, position }) {
564
+ if (duration !== void 0) return Math.ceil(duration);
565
+ return Math.ceil(Math.max(maxSeen, position));
566
+ }
567
+ function formatPositionValueText({ template, position, duration }) {
568
+ const positionText = formatAudioClock(position);
569
+ if (duration === void 0) return positionText;
570
+ return template.replace("{position}", positionText).replace("{duration}", formatAudioClock(duration));
571
+ }
572
+
573
+ // src/participant/ParticipantAudioPlayerControls.tsx
574
+ import { Pause, Play } from "lucide-react";
575
+ import { jsx as jsx7 } from "react/jsx-runtime";
576
+ function AudioToggleButton({ label, isPlaying, isPending, isDisabled, onToggle }) {
577
+ return /* @__PURE__ */ jsx7(
578
+ "button",
579
+ {
580
+ type: "button",
581
+ className: "cv-p-audio__toggle",
582
+ "aria-label": label,
583
+ "data-cv-tooltip": label,
584
+ "aria-busy": isPending ? true : void 0,
585
+ disabled: isDisabled,
586
+ onClick: onToggle,
587
+ children: isPlaying ? /* @__PURE__ */ jsx7(Pause, { size: 20, "aria-hidden": "true", focusable: "false" }) : /* @__PURE__ */ jsx7(Play, { size: 20, "aria-hidden": "true", focusable: "false" })
588
+ }
589
+ );
590
+ }
591
+ function AudioSeekRange({ label, max, position, valueText, isDisabled, onSeek, onKeyDown }) {
592
+ return /* @__PURE__ */ jsx7(
593
+ "input",
594
+ {
595
+ type: "range",
596
+ className: "cv-p-audio__seek",
597
+ "aria-label": label,
598
+ min: 0,
599
+ max,
600
+ step: 1,
601
+ value: Math.min(position, max),
602
+ "aria-valuetext": valueText,
603
+ disabled: isDisabled,
604
+ onChange: (event) => onSeek(Number(event.currentTarget.value)),
605
+ onKeyDown
606
+ }
607
+ );
608
+ }
609
+ function AudioRateButton({ label, text, onClick }) {
610
+ return /* @__PURE__ */ jsx7("button", { type: "button", className: "cv-p-audio__rate", "aria-label": label, onClick, children: text });
611
+ }
612
+
613
+ // src/participant/useParticipantAudioPlayer.ts
614
+ import { useCallback, useEffect as useEffect3, useRef as useRef3, useState } from "react";
615
+
616
+ // src/participant/participantAudioCoordinator.ts
617
+ var activeHandle;
618
+ function claimAudioPlayback(handle) {
619
+ if (activeHandle && activeHandle !== handle) activeHandle.pause();
620
+ activeHandle = handle;
621
+ }
622
+ function releaseAudioPlayback(handle) {
623
+ if (activeHandle === handle) activeHandle = void 0;
624
+ }
625
+
626
+ // src/participant/participantAudioPlayerState.ts
627
+ var INITIAL_PLAYER_STATE = {
628
+ isPlaying: false,
629
+ position: 0,
630
+ mediaDuration: void 0,
631
+ maxSeen: 0,
632
+ rateIndex: 0,
633
+ hasFailed: false
634
+ };
635
+ function applyPlaybackRate(audio, rateIndex) {
636
+ const rate = PLAYBACK_RATES[rateIndex] ?? 1;
637
+ audio.defaultPlaybackRate = rate;
638
+ audio.playbackRate = rate;
639
+ }
640
+
641
+ // src/participant/useParticipantAudioSourceRetry.ts
642
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
643
+ function useParticipantAudioSourceRetry({ context, url, onSourceError }) {
644
+ const { audioRef, update } = context;
645
+ const canRetryRef = useRef2(true);
646
+ const resumeAtRef = useRef2(void 0);
647
+ useEffect2(() => {
648
+ const audio = audioRef.current;
649
+ const resumeAt = resumeAtRef.current;
650
+ if (!audio || resumeAt === void 0 || !url) return;
651
+ resumeAtRef.current = void 0;
652
+ audio.currentTime = resumeAt;
653
+ Promise.resolve(audio.play()).catch(() => update({ isPlaying: false }));
654
+ }, [url, audioRef, update]);
655
+ function allowRetry() {
656
+ canRetryRef.current = true;
657
+ }
658
+ function handleError(event) {
659
+ if (!onSourceError || !canRetryRef.current) return update({ hasFailed: true, isPlaying: false });
660
+ canRetryRef.current = false;
661
+ resumeAtRef.current = event.currentTarget.currentTime;
662
+ onSourceError().then((fresh) => {
663
+ if (fresh && fresh !== url) return;
664
+ resumeAtRef.current = void 0;
665
+ update({ hasFailed: true, isPlaying: false });
666
+ });
667
+ }
668
+ return { handleError, allowRetry };
669
+ }
670
+
671
+ // src/participant/useParticipantAudioMediaEvents.ts
672
+ function useParticipantAudioMediaEvents({ context, url, onSourceError }) {
673
+ const { handleRef, state, update } = context;
674
+ const { handleError, allowRetry } = useParticipantAudioSourceRetry({ context, url, onSourceError });
675
+ function handleStop() {
676
+ releaseAudioPlayback(handleRef.current);
677
+ update({ isPlaying: false });
678
+ }
679
+ function handleDuration(event) {
680
+ update({ mediaDuration: resolveMediaDurationSeconds(event.currentTarget.duration) });
681
+ }
682
+ return {
683
+ onPlay: () => {
684
+ claimAudioPlayback(handleRef.current);
685
+ update({ isPlaying: true });
686
+ },
687
+ onPause: handleStop,
688
+ onEnded: handleStop,
689
+ onTimeUpdate: (event) => {
690
+ const position = event.currentTarget.currentTime;
691
+ update({ position, maxSeen: Math.max(state.maxSeen, position) });
692
+ },
693
+ onLoadedMetadata: (event) => {
694
+ allowRetry();
695
+ handleDuration(event);
696
+ },
697
+ onDurationChange: handleDuration,
698
+ onError: handleError
699
+ };
700
+ }
701
+
702
+ // src/participant/useParticipantAudioPlayerActions.ts
703
+ function useParticipantAudioPlayerActions({ context, url, duration }) {
704
+ const { audioRef, handleRef, state, update } = context;
705
+ function handleToggle() {
706
+ const audio = audioRef.current;
707
+ if (!audio || !url) return;
708
+ if (!audio.paused) return audio.pause();
709
+ claimAudioPlayback(handleRef.current);
710
+ applyPlaybackRate(audio, state.rateIndex);
711
+ Promise.resolve(audio.play()).catch(() => update({ isPlaying: false }));
712
+ }
713
+ function seekTo(seconds) {
714
+ const audio = audioRef.current;
715
+ if (!audio || !url) return;
716
+ audio.currentTime = seconds;
717
+ update({ position: seconds, maxSeen: Math.max(state.maxSeen, seconds) });
718
+ }
719
+ function handleKeyDown(event) {
720
+ const position = audioRef.current?.currentTime ?? state.position;
721
+ const target = resolveSeekKeyTarget({ key: event.key, position, duration });
722
+ if (target === void 0) return;
723
+ event.preventDefault();
724
+ seekTo(target);
725
+ }
726
+ function handleRateClick() {
727
+ const rateIndex = nextRateIndex(state.rateIndex);
728
+ if (audioRef.current) applyPlaybackRate(audioRef.current, rateIndex);
729
+ update({ rateIndex });
730
+ }
731
+ return { handleToggle, seekTo, handleKeyDown, handleRateClick };
732
+ }
733
+
734
+ // src/participant/useParticipantAudioPlayer.ts
735
+ function assignRef(target, element) {
736
+ if (typeof target === "function") target(element);
737
+ else if (target) target.current = element;
738
+ }
739
+ function useParticipantAudioPlayer({ url, durationMs, onSourceError, mediaRef }) {
740
+ const audioRef = useRef3(null);
741
+ const handleRef = useRef3({ pause: () => audioRef.current?.pause() });
742
+ const [state, setState2] = useState(INITIAL_PLAYER_STATE);
743
+ const update = useCallback((patch) => setState2((previous) => ({ ...previous, ...patch })), []);
744
+ const mediaEvents = useParticipantAudioMediaEvents({ context: { audioRef, handleRef, state, update }, url, onSourceError });
745
+ const duration = resolveServerDurationSeconds(durationMs) ?? state.mediaDuration;
746
+ const setMedia = useCallback(
747
+ (element) => {
748
+ audioRef.current = element;
749
+ assignRef(mediaRef, element);
750
+ },
751
+ [mediaRef]
752
+ );
753
+ useEffect3(() => {
754
+ const handle = handleRef.current;
755
+ return () => releaseAudioPlayback(handle);
756
+ }, []);
757
+ const actions = useParticipantAudioPlayerActions({ context: { audioRef, handleRef, state, update }, url, duration });
758
+ return {
759
+ state,
760
+ duration,
761
+ rate: PLAYBACK_RATES[state.rateIndex] ?? 1,
762
+ mediaProps: { ...mediaEvents, ref: setMedia },
763
+ ...actions
764
+ };
765
+ }
766
+
767
+ // src/participant/ParticipantAudioPlayer.tsx
768
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
769
+ function fillRate(template, rate) {
770
+ return template.replace("{rate}", formatPlaybackRate(rate));
771
+ }
772
+ function ParticipantAudioPlayer({
773
+ labels,
774
+ url,
775
+ durationMs,
776
+ filename,
777
+ isPending = false,
778
+ isUnavailable = false,
779
+ onSourceError,
780
+ mediaRef
781
+ }) {
782
+ const player = useParticipantAudioPlayer({ url, durationMs, onSourceError, mediaRef });
783
+ const { state, duration, rate } = player;
784
+ const position = Math.floor(state.position);
785
+ const hasFailed = isUnavailable || state.hasFailed;
786
+ return /* @__PURE__ */ jsxs6("div", { className: "cv-p-audio", role: "group", "aria-label": filename ? `${labels.audioPlayer}: ${filename}` : labels.audioPlayer, children: [
787
+ /* @__PURE__ */ jsx8(
788
+ AudioToggleButton,
789
+ {
790
+ label: state.isPlaying ? labels.pauseAudio : labels.playAudio,
791
+ isPlaying: state.isPlaying,
792
+ isPending,
793
+ isDisabled: hasFailed,
794
+ onToggle: player.handleToggle
795
+ }
796
+ ),
797
+ /* @__PURE__ */ jsx8(
798
+ AudioSeekRange,
799
+ {
800
+ label: labels.audioPosition,
801
+ max: resolveSeekMax({ duration, maxSeen: state.maxSeen, position }),
802
+ position,
803
+ valueText: formatPositionValueText({ template: labels.audioPositionValue, position, duration }),
804
+ isDisabled: hasFailed,
805
+ onSeek: player.seekTo,
806
+ onKeyDown: player.handleKeyDown
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsx8("span", { className: "cv-p-audio__time", children: `${formatAudioClock(position)} / ${formatAudioClock(duration)}` }),
810
+ /* @__PURE__ */ jsx8(AudioRateButton, { label: fillRate(labels.playbackRate, rate), text: formatPlaybackRate(rate), onClick: player.handleRateClick }),
811
+ /* @__PURE__ */ jsx8("audio", { className: "cv-p-audio__media", preload: "none", src: url, ...player.mediaProps }),
812
+ hasFailed ? /* @__PURE__ */ jsx8("span", { className: "cv-p-audio__error", children: labels.audioUnavailable }) : null
813
+ ] });
814
+ }
815
+
816
+ // src/participant/ParticipantRecorderParts.tsx
817
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
818
+ function RecordAudioButton({ label, isBusy, disabled, onClick, buttonRef }) {
819
+ return /* @__PURE__ */ jsx9(
820
+ "button",
821
+ {
822
+ ref: buttonRef,
823
+ type: "button",
824
+ className: "cv-p-button cv-p-button--primary cv-p-composer__record",
825
+ "aria-label": label,
826
+ "data-cv-tooltip": label,
827
+ "aria-busy": isBusy ? true : void 0,
828
+ disabled: disabled || isBusy,
829
+ onClick,
830
+ children: /* @__PURE__ */ jsx9(Mic, { size: 20, "aria-hidden": "true", focusable: "false" })
831
+ }
832
+ );
833
+ }
834
+ function RecorderError({ message }) {
835
+ return /* @__PURE__ */ jsx9("p", { className: "cv-p-composer__error", role: "alert", children: message });
836
+ }
837
+ function focusOnMount(element) {
838
+ element?.focus();
839
+ }
840
+ function DiscardButton({ labels, onDiscard }) {
841
+ return /* @__PURE__ */ jsx9(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ className: "cv-p-button cv-p-recorder__discard",
846
+ "aria-label": labels.discardAudio,
847
+ "data-cv-tooltip": labels.discardAudio,
848
+ onClick: onDiscard,
849
+ children: /* @__PURE__ */ jsx9(Trash2, { size: 20, "aria-hidden": "true", focusable: "false" })
850
+ }
851
+ );
852
+ }
853
+ function RecordingGroup({ labels, clockText, onStop, onDiscard }) {
854
+ return /* @__PURE__ */ jsxs7("div", { className: "cv-p-recorder", role: "group", "aria-label": labels.recordingInProgress, children: [
855
+ /* @__PURE__ */ jsx9(DiscardButton, { labels, onDiscard }),
856
+ /* @__PURE__ */ jsx9("span", { className: "cv-p-recorder__dot", "aria-hidden": "true" }),
857
+ /* @__PURE__ */ jsx9("span", { className: "cv-p-recorder__elapsed", role: "timer", children: clockText }),
858
+ /* @__PURE__ */ jsx9(
859
+ "button",
860
+ {
861
+ type: "button",
862
+ ref: focusOnMount,
863
+ className: "cv-p-button cv-p-button--primary cv-p-recorder__stop",
864
+ "aria-label": labels.stopRecording,
865
+ "data-cv-tooltip": labels.stopRecording,
866
+ onClick: onStop,
867
+ children: /* @__PURE__ */ jsx9(Square, { size: 20, "aria-hidden": "true", focusable: "false" })
868
+ }
869
+ )
870
+ ] });
871
+ }
872
+ function ReviewGroup({ labels, url, audioRef, durationMs, onSend, onDiscard, disabled = false }) {
873
+ return /* @__PURE__ */ jsxs7("div", { className: "cv-p-recorder cv-p-recorder--review", role: "group", "aria-label": labels.reviewAudio, children: [
874
+ /* @__PURE__ */ jsx9("p", { className: "cv-p-recorder__hint", "aria-hidden": "true", children: labels.reviewAudio }),
875
+ /* @__PURE__ */ jsx9(DiscardButton, { labels, onDiscard }),
876
+ /* @__PURE__ */ jsx9(ParticipantAudioPlayer, { labels, url, durationMs, mediaRef: audioRef }),
877
+ /* @__PURE__ */ jsx9(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ ref: focusOnMount,
882
+ className: "cv-p-button cv-p-button--primary cv-p-recorder__send",
883
+ "aria-label": labels.sendAudio,
884
+ "data-cv-tooltip": labels.sendAudio,
885
+ disabled,
886
+ onClick: onSend,
887
+ children: /* @__PURE__ */ jsx9(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
888
+ }
889
+ )
890
+ ] });
891
+ }
892
+
893
+ // src/participant/ParticipantAudioRecorderView.tsx
894
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
895
+ var ERROR_LABEL_BY_STATE = {
896
+ denied: "microphoneDenied",
897
+ unsupported: "recordingUnsupported",
898
+ empty: "audioEmpty",
899
+ tooLarge: "audioTooLarge"
900
+ };
901
+ function resolveRecorderErrorMessage(state, labels) {
902
+ return state in ERROR_LABEL_BY_STATE ? labels[ERROR_LABEL_BY_STATE[state]] : void 0;
903
+ }
904
+ function ParticipantAudioRecorderView(props) {
905
+ const { state, elapsedMs, maxDurationMs = DEFAULT_MAX_RECORDING_MILLISECONDS, review, actions, disabled = false } = props;
906
+ const labels = resolveParticipantAudioLabels(props.labels);
907
+ if (state === "recording") {
908
+ const clockText = formatRecordingElapsedLabel(labels.recordingElapsed, formatRecordingClock(elapsedMs), formatRecordingClock(maxDurationMs));
909
+ return /* @__PURE__ */ jsx10(RecordingGroup, { labels, clockText, onStop: actions?.onStop, onDiscard: actions?.onDiscard });
910
+ }
911
+ if (state === "review") {
912
+ return /* @__PURE__ */ jsx10(ReviewGroup, { labels, url: review?.url, audioRef: review?.audioRef, durationMs: review?.durationMs, onSend: actions?.onSend, onDiscard: actions?.onDiscard, disabled });
913
+ }
914
+ const errorMessage = resolveRecorderErrorMessage(state, labels);
915
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
916
+ errorMessage ? /* @__PURE__ */ jsx10(RecorderError, { message: errorMessage }) : null,
917
+ /* @__PURE__ */ jsx10(RecordAudioButton, { label: labels.recordAudio, isBusy: state === "requesting", disabled, onClick: actions?.onStart })
918
+ ] });
919
+ }
920
+
921
+ // src/participant/useParticipantAudioRecorder.ts
922
+ import { useCallback as useCallback3, useMemo } from "react";
923
+
924
+ // src/participant/participantAudioCapture.ts
925
+ function stopStream(stream) {
926
+ for (const track of stream.getTracks()) track.stop();
927
+ }
928
+ function formatRecordedFileName(moment, extension) {
929
+ const iso = moment.toISOString();
930
+ const day = iso.slice(0, 10).replace(/-/g, "");
931
+ const time = iso.slice(11, 19).replace(/:/g, "");
932
+ return `audio-${day}-${time}.${extension}`;
933
+ }
934
+ function buildRecordedFile(blob, format, moment = /* @__PURE__ */ new Date()) {
935
+ return new File([blob], formatRecordedFileName(moment, format.extension), { type: format.uploadMimeType });
936
+ }
937
+ function createMuteWatch(onPersistentMute) {
938
+ let timer;
939
+ const clear = () => {
940
+ if (timer !== void 0) clearTimeout(timer);
941
+ timer = void 0;
942
+ };
943
+ const arm = () => {
944
+ clear();
945
+ timer = setTimeout(onPersistentMute, MUTE_INTERRUPTION_DELAY_MS);
946
+ };
947
+ return { arm, clear };
948
+ }
949
+ function openCaptureSession({ stream, format, onInterrupt, onStopped }) {
950
+ const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
951
+ const chunks = [];
952
+ const startedAt = Date.now();
953
+ const muteWatch = createMuteWatch(onInterrupt);
954
+ for (const track of stream.getTracks()) {
955
+ track.onended = onInterrupt;
956
+ track.onmute = muteWatch.arm;
957
+ track.onunmute = muteWatch.clear;
958
+ }
959
+ recorder.ondataavailable = (event) => {
960
+ if (event.data.size > 0) chunks.push(event.data);
961
+ };
962
+ recorder.onerror = onInterrupt;
963
+ recorder.onstop = () => onStopped(new Blob(chunks, { type: format.uploadMimeType }), Date.now() - startedAt);
964
+ recorder.start();
965
+ let isStopRequested = false;
966
+ function requestStop() {
967
+ if (isStopRequested || recorder.state === "inactive") return;
968
+ isStopRequested = true;
969
+ recorder.stop();
970
+ }
971
+ function release() {
972
+ muteWatch.clear();
973
+ recorder.ondataavailable = null;
974
+ recorder.onerror = null;
975
+ recorder.onstop = null;
976
+ for (const track of stream.getTracks()) {
977
+ track.onended = null;
978
+ track.onmute = null;
979
+ track.onunmute = null;
980
+ }
981
+ stopStream(stream);
982
+ }
983
+ return { format, startedAt, requestStop, release };
984
+ }
985
+
986
+ // src/participant/useParticipantAudioRecorderCore.ts
987
+ import { useCallback as useCallback2, useRef as useRef4, useState as useState2 } from "react";
988
+ function useRecorderCore(limits) {
989
+ const [status, setStatus] = useState2("idle");
990
+ const [elapsedMs, setElapsedMs] = useState2(0);
991
+ const [recorded, setRecorded] = useState2();
992
+ const statusRef = useRef4("idle");
993
+ const limitsRef = useRef4(limits);
994
+ const tokenRef = useRef4(0);
995
+ const sessionRef = useRef4(void 0);
996
+ const recordedRef = useRef4(void 0);
997
+ const audioRef = useRef4(null);
998
+ limitsRef.current = limits;
999
+ const moveTo = useCallback2((event) => {
1000
+ statusRef.current = reduceRecorderStatus(statusRef.current, event);
1001
+ setStatus(statusRef.current);
1002
+ }, []);
1003
+ const releaseSession = useCallback2(() => {
1004
+ sessionRef.current?.release();
1005
+ sessionRef.current = void 0;
1006
+ }, []);
1007
+ const releaseRecorded = useCallback2(() => {
1008
+ if (recordedRef.current) URL.revokeObjectURL(recordedRef.current.objectUrl);
1009
+ recordedRef.current = void 0;
1010
+ setRecorded(void 0);
1011
+ }, []);
1012
+ return {
1013
+ status,
1014
+ elapsedMs,
1015
+ setElapsedMs,
1016
+ recorded,
1017
+ setRecorded,
1018
+ limitsRef,
1019
+ statusRef,
1020
+ tokenRef,
1021
+ sessionRef,
1022
+ recordedRef,
1023
+ audioRef,
1024
+ moveTo,
1025
+ releaseSession,
1026
+ releaseRecorded
1027
+ };
1028
+ }
1029
+
1030
+ // src/participant/useParticipantAudioRecorderEffects.ts
1031
+ import { useEffect as useEffect4, useRef as useRef5 } from "react";
1032
+ var ELAPSED_TICK_MS = 250;
1033
+ function useRecorderInterruptionListeners(onInterruption) {
1034
+ useEffect4(() => {
1035
+ const handleVisibilityChange = () => {
1036
+ if (document.visibilityState === "hidden") onInterruption();
1037
+ };
1038
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1039
+ window.addEventListener("pagehide", onInterruption);
1040
+ return () => {
1041
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1042
+ window.removeEventListener("pagehide", onInterruption);
1043
+ };
1044
+ }, [onInterruption]);
1045
+ }
1046
+ function useRecordingClock({ status, maxDurationMs, sessionRef, setElapsedMs, stop }) {
1047
+ useEffect4(() => {
1048
+ if (status !== "recording") return void 0;
1049
+ const startedAt = sessionRef.current?.startedAt ?? Date.now();
1050
+ const tick = setInterval(() => setElapsedMs(Date.now() - startedAt), ELAPSED_TICK_MS);
1051
+ const ceiling = setTimeout(stop, maxDurationMs);
1052
+ return () => {
1053
+ clearInterval(tick);
1054
+ clearTimeout(ceiling);
1055
+ };
1056
+ }, [status, maxDurationMs, sessionRef, setElapsedMs, stop]);
1057
+ }
1058
+ function useRecorderLifecycle({ status, onCaptureChange, teardown }) {
1059
+ const isCapturing = status === "requesting" || status === "recording" || status === "review";
1060
+ const reportedRef = useRef5(false);
1061
+ const onCaptureChangeRef = useRef5(onCaptureChange);
1062
+ onCaptureChangeRef.current = onCaptureChange;
1063
+ useEffect4(() => {
1064
+ if (reportedRef.current === isCapturing) return;
1065
+ reportedRef.current = isCapturing;
1066
+ onCaptureChangeRef.current?.(isCapturing);
1067
+ }, [isCapturing]);
1068
+ useEffect4(
1069
+ () => () => {
1070
+ teardown();
1071
+ if (!reportedRef.current) return;
1072
+ reportedRef.current = false;
1073
+ onCaptureChangeRef.current?.(false);
1074
+ },
1075
+ [teardown]
1076
+ );
1077
+ }
1078
+
1079
+ // src/participant/useParticipantAudioRecorder.ts
1080
+ function useRecorderFinalize(core) {
1081
+ const { statusRef, sessionRef, recordedRef, limitsRef, setRecorded, moveTo, releaseSession } = core;
1082
+ return useCallback3(
1083
+ (blob, durationMs) => {
1084
+ const format = sessionRef.current?.format;
1085
+ if (statusRef.current !== "recording" || !format) return;
1086
+ releaseSession();
1087
+ const event = classifyCapture({ sizeBytes: blob.size, durationMs }, limitsRef.current);
1088
+ if (event === "capturedReview") {
1089
+ const file = buildRecordedFile(blob, format);
1090
+ recordedRef.current = { file, objectUrl: URL.createObjectURL(file), durationMs };
1091
+ setRecorded(recordedRef.current);
1092
+ }
1093
+ moveTo(event);
1094
+ },
1095
+ [statusRef, sessionRef, recordedRef, limitsRef, setRecorded, moveTo, releaseSession]
1096
+ );
1097
+ }
1098
+ function useRecorderInterruption(core) {
1099
+ const { statusRef, sessionRef, tokenRef, audioRef, moveTo } = core;
1100
+ return useCallback3(() => {
1101
+ const action = resolveInterruptionAction(statusRef.current);
1102
+ if (action === "stop") sessionRef.current?.requestStop();
1103
+ if (action === "pause") audioRef.current?.pause();
1104
+ if (action === "cancel") {
1105
+ tokenRef.current += 1;
1106
+ moveTo("cancel");
1107
+ }
1108
+ }, [statusRef, sessionRef, tokenRef, audioRef, moveTo]);
1109
+ }
1110
+ function useRecorderStart(core, { onInterrupt, onStopped }) {
1111
+ const { statusRef, tokenRef, sessionRef, setElapsedMs, moveTo } = core;
1112
+ const beginRecording = useCallback3(
1113
+ (stream, format) => {
1114
+ try {
1115
+ sessionRef.current = openCaptureSession({ stream, format, onInterrupt, onStopped });
1116
+ } catch {
1117
+ stopStream(stream);
1118
+ moveTo("microphoneUnavailable");
1119
+ return;
1120
+ }
1121
+ setElapsedMs(0);
1122
+ moveTo("granted");
1123
+ },
1124
+ [sessionRef, setElapsedMs, onInterrupt, onStopped, moveTo]
1125
+ );
1126
+ return useCallback3(async () => {
1127
+ if (!canStartRecording(statusRef.current)) return;
1128
+ const token = tokenRef.current += 1;
1129
+ moveTo("request");
1130
+ const format = resolveRecordingFormat();
1131
+ if (!format || !navigator.mediaDevices?.getUserMedia) return moveTo("microphoneUnavailable");
1132
+ let stream;
1133
+ try {
1134
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1135
+ } catch (error) {
1136
+ if (token === tokenRef.current) moveTo(classifyMicrophoneError(error));
1137
+ return;
1138
+ }
1139
+ if (token !== tokenRef.current) return stopStream(stream);
1140
+ beginRecording(stream, format);
1141
+ }, [statusRef, tokenRef, moveTo, beginRecording]);
1142
+ }
1143
+ function useRecorderActions(core) {
1144
+ const { statusRef, tokenRef, sessionRef, recordedRef, moveTo, releaseSession, releaseRecorded } = core;
1145
+ const stop = useCallback3(() => sessionRef.current?.requestStop(), [sessionRef]);
1146
+ const discard = useCallback3(() => {
1147
+ tokenRef.current += 1;
1148
+ releaseSession();
1149
+ releaseRecorded();
1150
+ moveTo("cancel");
1151
+ }, [tokenRef, moveTo, releaseRecorded, releaseSession]);
1152
+ const confirm = useCallback3(() => {
1153
+ if (statusRef.current !== "review") return void 0;
1154
+ const file = recordedRef.current?.file;
1155
+ releaseRecorded();
1156
+ moveTo("cancel");
1157
+ return file;
1158
+ }, [statusRef, recordedRef, moveTo, releaseRecorded]);
1159
+ const teardown = useCallback3(() => {
1160
+ tokenRef.current += 1;
1161
+ releaseSession();
1162
+ if (recordedRef.current) URL.revokeObjectURL(recordedRef.current.objectUrl);
1163
+ recordedRef.current = void 0;
1164
+ }, [tokenRef, recordedRef, releaseSession]);
1165
+ return { stop, discard, confirm, teardown };
1166
+ }
1167
+ function useParticipantAudioRecorder({ options, channelMaxBytes } = {}) {
1168
+ const limits = useMemo(
1169
+ () => resolveRecordingLimits({ options, channelMaxBytes }),
1170
+ [options?.maxDurationMs, options?.maxSizeBytes, channelMaxBytes]
1171
+ );
1172
+ const core = useRecorderCore(limits);
1173
+ const onInterrupt = useRecorderInterruption(core);
1174
+ const onStopped = useRecorderFinalize(core);
1175
+ const start = useRecorderStart(core, { onInterrupt, onStopped });
1176
+ const { stop, discard, confirm, teardown } = useRecorderActions(core);
1177
+ useRecorderInterruptionListeners(onInterrupt);
1178
+ useRecordingClock({ status: core.status, maxDurationMs: limits.maxDurationMs, sessionRef: core.sessionRef, setElapsedMs: core.setElapsedMs, stop });
1179
+ useRecorderLifecycle({ status: core.status, onCaptureChange: options?.onCaptureChange, teardown });
1180
+ const { status, elapsedMs, recorded, audioRef } = core;
1181
+ return { status, elapsedMs, limits, recorded, audioRef, start, stop, discard, confirm };
1182
+ }
1183
+
1184
+ // src/participant/ParticipantComposerAudio.tsx
1185
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1186
+ function isCapturingStatus(status) {
1187
+ return status === "recording" || status === "review";
1188
+ }
1189
+ function useRestoreMicrophoneFocus(status) {
1190
+ const microphoneRef = useRef6(null);
1191
+ const wasCapturingRef = useRef6(false);
1192
+ useEffect5(() => {
1193
+ const isCapturing = isCapturingStatus(status);
1194
+ if (wasCapturingRef.current && !isCapturing) microphoneRef.current?.focus();
1195
+ wasCapturingRef.current = isCapturing;
1196
+ }, [status]);
1197
+ return microphoneRef;
1198
+ }
1199
+ function RecorderLine({ recorder, labels, onSendAudio, disabled }) {
1200
+ function handleSendAudio() {
1201
+ const file = recorder.confirm();
1202
+ if (file) onSendAudio(file);
1203
+ }
1204
+ return /* @__PURE__ */ jsx11(
1205
+ ParticipantAudioRecorderView,
1206
+ {
1207
+ state: recorder.status,
1208
+ elapsedMs: recorder.elapsedMs,
1209
+ labels,
1210
+ disabled,
1211
+ maxDurationMs: recorder.limits.maxDurationMs,
1212
+ review: { url: recorder.recorded?.objectUrl, audioRef: recorder.audioRef, durationMs: recorder.recorded?.durationMs },
1213
+ actions: { onStop: recorder.stop, onSend: handleSendAudio, onDiscard: recorder.discard }
1214
+ }
1215
+ );
1216
+ }
1217
+ function ComposerAudioRow({ settings, labels, disabled, isDraftEmpty, sendButton, children }) {
1218
+ const recorder = useParticipantAudioRecorder({
1219
+ options: settings.audioRecording,
1220
+ channelMaxBytes: maxBytesForChannel(settings.channel)
1221
+ });
1222
+ const microphoneRef = useRestoreMicrophoneFocus(recorder.status);
1223
+ const audioLabels = resolveParticipantAudioLabels(labels);
1224
+ const errorMessage = resolveRecorderErrorMessage(recorder.status, audioLabels);
1225
+ if (isCapturingStatus(recorder.status)) {
1226
+ return /* @__PURE__ */ jsx11(RecorderLine, { recorder, labels, onSendAudio: settings.onSendAudio, disabled });
1227
+ }
1228
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1229
+ errorMessage ? /* @__PURE__ */ jsx11(RecorderError, { message: errorMessage }) : null,
1230
+ /* @__PURE__ */ jsxs9("div", { className: "cv-p-composer__row", children: [
1231
+ children,
1232
+ isDraftEmpty ? /* @__PURE__ */ jsx11(
1233
+ RecordAudioButton,
1234
+ {
1235
+ label: audioLabels.recordAudio,
1236
+ isBusy: recorder.status === "requesting",
1237
+ disabled,
1238
+ onClick: () => void recorder.start(),
1239
+ buttonRef: microphoneRef
1240
+ }
1241
+ ) : sendButton
1242
+ ] })
1243
+ ] });
1244
+ }
375
1245
 
376
1246
  // src/participant/ParticipantComposerControls.tsx
377
- import { useLayoutEffect, useRef as useRef2 } from "react";
378
- import { Paperclip, Send } from "lucide-react";
1247
+ import { useLayoutEffect, useRef as useRef7 } from "react";
1248
+ import { Paperclip, Send as Send2 } from "lucide-react";
379
1249
 
380
1250
  // src/participant/participantAutoGrow.ts
381
1251
  function fitTextareaHeight(target) {
@@ -384,11 +1254,11 @@ function fitTextareaHeight(target) {
384
1254
  }
385
1255
 
386
1256
  // src/participant/ParticipantComposerControls.tsx
387
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1257
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
388
1258
  function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
389
- const fileInputRef = useRef2(null);
390
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
391
- /* @__PURE__ */ jsx7(
1259
+ const fileInputRef = useRef7(null);
1260
+ return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1261
+ /* @__PURE__ */ jsx12(
392
1262
  "input",
393
1263
  {
394
1264
  ref: fileInputRef,
@@ -400,37 +1270,39 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
400
1270
  onChange: onChosen
401
1271
  }
402
1272
  ),
403
- /* @__PURE__ */ jsx7(
1273
+ /* @__PURE__ */ jsx12(
404
1274
  "button",
405
1275
  {
406
1276
  type: "button",
407
1277
  className: "cv-p-composer__attach",
408
1278
  "aria-label": labels.attach,
1279
+ "data-cv-tooltip": labels.attach,
409
1280
  disabled,
410
1281
  onClick: () => fileInputRef.current?.click(),
411
- children: /* @__PURE__ */ jsx7(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
1282
+ children: /* @__PURE__ */ jsx12(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
412
1283
  }
413
1284
  )
414
1285
  ] });
415
1286
  }
416
1287
  function SendButton({ label, disabled }) {
417
- return /* @__PURE__ */ jsx7(
1288
+ return /* @__PURE__ */ jsx12(
418
1289
  "button",
419
1290
  {
420
1291
  type: "submit",
421
1292
  className: "cv-p-button cv-p-button--primary cv-p-composer__send",
422
1293
  "aria-label": label,
1294
+ "data-cv-tooltip": label,
423
1295
  disabled,
424
- children: /* @__PURE__ */ jsx7(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
1296
+ children: /* @__PURE__ */ jsx12(Send2, { size: 20, "aria-hidden": "true", focusable: "false" })
425
1297
  }
426
1298
  );
427
1299
  }
428
1300
  function MessageField({ value, label, placeholder, disabled, maxLength, onChange }) {
429
- const fieldRef = useRef2(null);
1301
+ const fieldRef = useRef7(null);
430
1302
  useLayoutEffect(() => {
431
1303
  if (fieldRef.current) fitTextareaHeight(fieldRef.current);
432
1304
  }, [value]);
433
- return /* @__PURE__ */ jsx7(
1305
+ return /* @__PURE__ */ jsx12(
434
1306
  "textarea",
435
1307
  {
436
1308
  ref: fieldRef,
@@ -446,29 +1318,20 @@ function MessageField({ value, label, placeholder, disabled, maxLength, onChange
446
1318
  );
447
1319
  }
448
1320
 
449
- // src/participant/participantFileValidation.ts
450
- function matchesAcceptedType(mimeType, acceptedTypes) {
451
- return acceptedTypes.some(
452
- (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
453
- );
1321
+ // src/participant/participantAudioSupport.ts
1322
+ function resolveBrowserRecordingFormat() {
1323
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) return void 0;
1324
+ return resolveRecordingFormat();
454
1325
  }
455
- function rejectionOf(file, params) {
456
- if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
457
- if (file.size > params.maxBytes) return "tooLarge";
458
- return void 0;
459
- }
460
- function reduceFilesSelection(params) {
461
- const files = [...params.current];
462
- const rejected = [];
463
- for (const file of params.incoming) {
464
- const reason = rejectionOf(file, params);
465
- if (reason === void 0) files.push(file);
466
- else rejected.push({ file, reason });
467
- }
468
- return { files, rejected };
469
- }
470
- function maxBytesForChannel(channel = "app") {
471
- return channelCapabilityFor(channel).attachments.maxBytes;
1326
+ function resolveComposerAudio(params) {
1327
+ const { audioRecording, onSendAudio, channel = "app", acceptedTypes } = params;
1328
+ if (!audioRecording || !onSendAudio) return void 0;
1329
+ const capability = channelCapabilityFor(channel);
1330
+ if (!capability.audio.records || !capability.attachments.accepted) return void 0;
1331
+ const format = resolveBrowserRecordingFormat();
1332
+ if (!format) return void 0;
1333
+ if (acceptedTypes && !matchesAcceptedType(format.uploadMimeType, acceptedTypes)) return void 0;
1334
+ return { audioRecording, channel, onSendAudio };
472
1335
  }
473
1336
 
474
1337
  // src/participant/participantQuickReply.ts
@@ -479,18 +1342,18 @@ function applyQuickReply(currentText, replyText) {
479
1342
  }
480
1343
 
481
1344
  // src/participant/ParticipantComposer.tsx
482
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1345
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
483
1346
  function QuickReplyStrip({ quickReplies, labels, onPick }) {
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)) });
1347
+ return /* @__PURE__ */ jsx13("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx13("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
485
1348
  }
486
1349
  function FileList({ files, labels, onRemove }) {
487
1350
  if (files.length === 0) return null;
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 })
1351
+ return /* @__PURE__ */ jsx13("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs11("li", { className: "cv-p-files__item", children: [
1352
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-files__name", children: file.name }),
1353
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
1354
+ /* @__PURE__ */ jsxs11("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, "data-cv-tooltip": labels.removeAttachment, onClick: () => onRemove(file), children: [
1355
+ /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", children: "\xD7" }),
1356
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
494
1357
  ] })
495
1358
  ] }, `${file.name}-${file.size}-${index}`)) });
496
1359
  }
@@ -498,7 +1361,7 @@ function rejectionText(rejection, labels) {
498
1361
  return rejection === "tooLarge" ? labels.attachmentTooLarge : labels.attachmentTypeNotAccepted;
499
1362
  }
500
1363
  function useFileSelection(props) {
501
- const [rejection, setRejection] = useState(void 0);
1364
+ const [rejection, setRejection] = useState3(void 0);
502
1365
  const { files, onFilesChange, channel } = props;
503
1366
  const maxBytes = maxBytesForChannel(channel);
504
1367
  function handleFilesChosen(event) {
@@ -514,38 +1377,85 @@ function useFileSelection(props) {
514
1377
  }
515
1378
  return { rejection, handleFilesChosen, handleRemoveFile };
516
1379
  }
1380
+ function ComposerRow({ composer, canSend, onFilesChosen }) {
1381
+ const { value, onChange, files, labels, channel = "app", disabled = false } = composer;
1382
+ const attach = channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx13(AttachControl, { labels, disabled, acceptedTypes: composer.acceptedTypes, onChosen: onFilesChosen }) : null;
1383
+ const field = /* @__PURE__ */ jsx13(
1384
+ MessageField,
1385
+ {
1386
+ value,
1387
+ label: labels.messageInputLabel,
1388
+ placeholder: labels.messageInputPlaceholder,
1389
+ disabled,
1390
+ maxLength: composer.maxLength,
1391
+ onChange
1392
+ }
1393
+ );
1394
+ const sendButton = /* @__PURE__ */ jsx13(SendButton, { label: labels.send, disabled: !canSend });
1395
+ const audio = resolveComposerAudio(composer);
1396
+ if (!audio) {
1397
+ return /* @__PURE__ */ jsxs11("div", { className: "cv-p-composer__row", children: [
1398
+ attach,
1399
+ field,
1400
+ sendButton
1401
+ ] });
1402
+ }
1403
+ const isDraftEmpty = value.trim().length === 0 && files.length === 0;
1404
+ return /* @__PURE__ */ jsxs11(ComposerAudioRow, { settings: audio, labels, disabled, isDraftEmpty, sendButton, children: [
1405
+ attach,
1406
+ field
1407
+ ] });
1408
+ }
517
1409
  function ParticipantComposer(props) {
518
- const { value, onChange, files, onSend, labels, channel = "app", disabled = false, isSending = false } = props;
1410
+ const { value, onChange, files, onSend, labels, disabled = false, isSending = false } = props;
519
1411
  const { rejection, handleFilesChosen, handleRemoveFile } = useFileSelection(props);
520
1412
  const canSend = !disabled && !isSending && (value.trim().length > 0 || files.length > 0);
521
1413
  function handleSubmit(event) {
522
1414
  event.preventDefault();
523
1415
  if (canSend) onSend();
524
1416
  }
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,
533
- {
534
- value,
535
- label: labels.messageInputLabel,
536
- placeholder: labels.messageInputPlaceholder,
537
- disabled,
538
- maxLength: props.maxLength,
539
- onChange
540
- }
541
- ),
542
- /* @__PURE__ */ jsx8(SendButton, { label: labels.send, disabled: !canSend })
543
- ] })
1417
+ return /* @__PURE__ */ jsxs11("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
1418
+ props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx13(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
1419
+ /* @__PURE__ */ jsx13(FileList, { files, labels, onRemove: handleRemoveFile }),
1420
+ rejection ? /* @__PURE__ */ jsx13("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
1421
+ /* @__PURE__ */ jsx13(ComposerRow, { composer: props, canSend, onFilesChosen: handleFilesChosen })
544
1422
  ] });
545
1423
  }
546
1424
 
1425
+ // src/participant/useSuppressTouchTooltips.ts
1426
+ import { useEffect as useEffect6 } from "react";
1427
+ function useSuppressTouchTooltips(rootRef) {
1428
+ useEffect6(() => {
1429
+ const root = rootRef.current;
1430
+ if (!root) return void 0;
1431
+ let isTouching = false;
1432
+ function handlePointerDown(event) {
1433
+ isTouching = event.pointerType === "touch";
1434
+ }
1435
+ function handlePointerOver(event) {
1436
+ if (event.pointerType === "touch") event.stopPropagation();
1437
+ }
1438
+ function handleFocusIn(event) {
1439
+ if (isTouching) event.stopPropagation();
1440
+ }
1441
+ function handleKeyDown() {
1442
+ isTouching = false;
1443
+ }
1444
+ root.addEventListener("pointerdown", handlePointerDown, true);
1445
+ root.addEventListener("pointerover", handlePointerOver);
1446
+ root.addEventListener("focusin", handleFocusIn);
1447
+ root.addEventListener("keydown", handleKeyDown, true);
1448
+ return () => {
1449
+ root.removeEventListener("pointerdown", handlePointerDown, true);
1450
+ root.removeEventListener("pointerover", handlePointerOver);
1451
+ root.removeEventListener("focusin", handleFocusIn);
1452
+ root.removeEventListener("keydown", handleKeyDown, true);
1453
+ };
1454
+ }, [rootRef]);
1455
+ }
1456
+
547
1457
  // src/participant/ParticipantCopyAnnouncer.tsx
548
- import { createContext, useCallback, useContext, useEffect as useEffect2, useReducer, useRef as useRef3 } from "react";
1458
+ import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect7, useReducer, useRef as useRef8 } from "react";
549
1459
 
550
1460
  // src/participant/participantCopyAnnouncement.ts
551
1461
  var INITIAL_COPY_ANNOUNCEMENT = { text: "", tick: 0 };
@@ -565,7 +1475,7 @@ function resolveLiveText(params) {
565
1475
  }
566
1476
 
567
1477
  // src/participant/ParticipantCopyAnnouncer.tsx
568
- import { jsx as jsx9 } from "react/jsx-runtime";
1478
+ import { jsx as jsx14 } from "react/jsx-runtime";
569
1479
  var ANNOUNCEMENT_MS = 3e3;
570
1480
  var AnnounceContext = createContext(() => void 0);
571
1481
  var AnnouncementContext = createContext(INITIAL_COPY_ANNOUNCEMENT);
@@ -574,20 +1484,20 @@ function useAnnounceCopy() {
574
1484
  }
575
1485
  function ParticipantCopyAnnouncer({ children }) {
576
1486
  const [announcement, dispatch] = useReducer(copyAnnouncementReducer, INITIAL_COPY_ANNOUNCEMENT);
577
- const announce = useCallback((text) => dispatch({ type: "announce", text }), []);
578
- useEffect2(() => {
1487
+ const announce = useCallback4((text) => dispatch({ type: "announce", text }), []);
1488
+ useEffect7(() => {
579
1489
  if (announcement.text === "") return void 0;
580
1490
  const timer = setTimeout(() => dispatch({ type: "expired" }), ANNOUNCEMENT_MS);
581
1491
  return () => clearTimeout(timer);
582
1492
  }, [announcement.tick, announcement.text]);
583
- return /* @__PURE__ */ jsx9(AnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx9(AnnouncementContext.Provider, { value: announcement, children }) });
1493
+ return /* @__PURE__ */ jsx14(AnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx14(AnnouncementContext.Provider, { value: announcement, children }) });
584
1494
  }
585
1495
  function ParticipantThreadLive({ notice }) {
586
1496
  const copy = useContext(AnnouncementContext);
587
- const baselineNotice = useRef3(void 0);
1497
+ const baselineNotice = useRef8(void 0);
588
1498
  const resolution = resolveLiveText({ copy, notice, baselineNotice: baselineNotice.current });
589
1499
  baselineNotice.current = resolution.baselineNotice;
590
- return /* @__PURE__ */ jsx9("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: resolution.text });
1500
+ return /* @__PURE__ */ jsx14("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: resolution.text });
591
1501
  }
592
1502
 
593
1503
  // src/participant/ParticipantScrollToLatest.tsx
@@ -600,7 +1510,7 @@ function focusScrollerOf(button) {
600
1510
  }
601
1511
 
602
1512
  // src/participant/ParticipantScrollToLatest.tsx
603
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1513
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
604
1514
  function ParticipantScrollToLatest({
605
1515
  label,
606
1516
  isAwayFromBottom,
@@ -610,19 +1520,20 @@ function ParticipantScrollToLatest({
610
1520
  }) {
611
1521
  const { isVisible, badge } = resolveScrollToLatestView({ isAwayFromBottom, newMessagesCount, hasMessages });
612
1522
  if (!isVisible || onClick === void 0) return null;
613
- return /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__latest", children: /* @__PURE__ */ jsxs8(
1523
+ return /* @__PURE__ */ jsx15("div", { className: "cv-p-thread__latest", children: /* @__PURE__ */ jsxs12(
614
1524
  "button",
615
1525
  {
616
1526
  type: "button",
617
1527
  className: "cv-p-thread__latest-button",
618
1528
  "aria-label": label,
1529
+ "data-cv-tooltip": label,
619
1530
  onClick: (event) => {
620
1531
  focusScrollerOf(event.currentTarget);
621
1532
  onClick();
622
1533
  },
623
1534
  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
1535
+ /* @__PURE__ */ jsx15(ChevronDown, { size: 24, "aria-hidden": "true", focusable: "false" }),
1536
+ badge ? /* @__PURE__ */ jsx15("span", { className: "cv-p-thread__latest-badge", "aria-hidden": "true", children: badge }) : null
626
1537
  ]
627
1538
  }
628
1539
  ) });
@@ -655,45 +1566,271 @@ function lettersOf(word) {
655
1566
  function firstLetter(word) {
656
1567
  return (Array.from(word.normalize("NFC"))[0] ?? "").toUpperCase();
657
1568
  }
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)}`;
1569
+ function authorInitials(name) {
1570
+ const [first, ...rest] = (name ?? "").split(/\s+/).map(lettersOf).filter(Boolean);
1571
+ if (first === void 0) return void 0;
1572
+ const last = rest.filter((word) => !NAME_PARTICLES.has(word.toLowerCase())).pop();
1573
+ return `${firstLetter(first)}${last === void 0 ? "" : firstLetter(last)}`;
1574
+ }
1575
+ function shouldShowAvatar({ previousAuthor, author, isMine }) {
1576
+ if (isMine) return false;
1577
+ if (previousAuthor === void 0) return true;
1578
+ return previousAuthor !== author;
1579
+ }
1580
+
1581
+ // src/participant/ParticipantAuthorAvatar.tsx
1582
+ import { jsx as jsx16 } from "react/jsx-runtime";
1583
+ function ParticipantAuthorAvatar({ name, render }) {
1584
+ const fromHost = render?.({ name });
1585
+ const hasHostContent = !(fromHost == null || fromHost === false || fromHost === "");
1586
+ const initials = authorInitials(name);
1587
+ const fallback = initials ?? /* @__PURE__ */ jsx16(User, { size: 16, "aria-hidden": true });
1588
+ return /* @__PURE__ */ jsx16("span", { className: "cv-p-avatar", "aria-hidden": "true", children: hasHostContent ? fromHost : fallback });
1589
+ }
1590
+
1591
+ // src/participant/participantDay.ts
1592
+ function pad(value) {
1593
+ return String(value).padStart(2, "0");
1594
+ }
1595
+ function toDateTimeAttribute(date) {
1596
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1597
+ }
1598
+
1599
+ // src/participant/ParticipantDayDivider.tsx
1600
+ import { jsx as jsx17 } from "react/jsx-runtime";
1601
+ function ParticipantDayDivider({ day, locale }) {
1602
+ const label = day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" });
1603
+ return /* @__PURE__ */ jsx17("div", { className: "cv-p-day", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx17("time", { className: "cv-p-day__label", dateTime: toDateTimeAttribute(day), children: label }) });
1604
+ }
1605
+
1606
+ // src/participant/ParticipantMessageBubble.tsx
1607
+ import { useId as useId3 } from "react";
1608
+
1609
+ // src/participant/ParticipantAttachmentItem.tsx
1610
+ import { useEffect as useEffect11, useState as useState7 } from "react";
1611
+
1612
+ // src/participant/ParticipantAudioTranscription.tsx
1613
+ import { useState as useState5 } from "react";
1614
+
1615
+ // src/participant/useTranscriptionOverflow.ts
1616
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState4 } from "react";
1617
+ function useTranscriptionOverflow(text, isExpanded) {
1618
+ const textRef = useRef9(null);
1619
+ const [isOverflowing, setIsOverflowing] = useState4(false);
1620
+ useEffect8(() => {
1621
+ const element = textRef.current;
1622
+ if (!element || isExpanded) return void 0;
1623
+ const measure = () => setIsOverflowing(element.scrollHeight > element.clientHeight);
1624
+ measure();
1625
+ if (typeof ResizeObserver === "undefined") return void 0;
1626
+ const observer = new ResizeObserver(measure);
1627
+ observer.observe(element);
1628
+ return () => observer.disconnect();
1629
+ }, [text, isExpanded]);
1630
+ return { textRef, isOverflowing };
1631
+ }
1632
+
1633
+ // src/participant/useTranscriptionReadyAnnouncement.ts
1634
+ import { useEffect as useEffect9, useRef as useRef10 } from "react";
1635
+ function useTranscriptionReadyAnnouncement({ attachmentId, status, text }) {
1636
+ const announce = useAnnounceCopy();
1637
+ const previous = useRef10({ attachmentId, status });
1638
+ useEffect9(() => {
1639
+ const before = previous.current;
1640
+ previous.current = { attachmentId, status };
1641
+ if (before.attachmentId !== attachmentId) return;
1642
+ if (before.status === "pending" && status === "ready") announce(text);
1643
+ }, [announce, attachmentId, status, text]);
1644
+ }
1645
+
1646
+ // src/participant/ParticipantAudioTranscription.tsx
1647
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1648
+ var MILLISECONDS_PER_MINUTE = 6e4;
1649
+ function resolveTooLongMinutes(transcription) {
1650
+ if (transcription?.status !== "failed" || transcription.reason !== "too_long") return void 0;
1651
+ if (typeof transcription.maxDurationMs !== "number") return void 0;
1652
+ const minutes = Math.floor(transcription.maxDurationMs / MILLISECONDS_PER_MINUTE);
1653
+ return minutes >= 1 ? minutes : void 0;
1654
+ }
1655
+ function dropUnusableTranscription(transcription) {
1656
+ if (transcription?.status === "ready" && typeof transcription.text !== "string") return void 0;
1657
+ return transcription;
1658
+ }
1659
+ function ParticipantAudioTranscription({ attachmentId, transcription: received, labels }) {
1660
+ const transcription = dropUnusableTranscription(received);
1661
+ useTranscriptionReadyAnnouncement({
1662
+ attachmentId,
1663
+ status: transcription?.status,
1664
+ text: labels.transcriptionReady
1665
+ });
1666
+ if (transcription?.status === "pending") {
1667
+ return /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription cv-p-transcription--pending", children: labels.transcribing });
1668
+ }
1669
+ if (transcription?.status === "ready") return /* @__PURE__ */ jsx18(ReadyTranscriptionBlock, { transcription, labels });
1670
+ const minutes = resolveTooLongMinutes(transcription);
1671
+ if (minutes === void 0) return null;
1672
+ return /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription cv-p-transcription--too-long", children: labels.transcriptionTooLong.replace("{minutes}", String(minutes)) });
1673
+ }
1674
+ function ReadyTranscriptionBlock({ transcription, labels }) {
1675
+ const [isExpanded, setIsExpanded] = useState5(false);
1676
+ const { textRef, isOverflowing } = useTranscriptionOverflow(transcription.text, isExpanded);
1677
+ const hasText = transcription.text !== "";
1678
+ const showsToggle = hasText && (isOverflowing || isExpanded);
1679
+ return /* @__PURE__ */ jsxs13("div", { className: "cv-p-transcription", children: [
1680
+ /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription__notice", children: labels.machineTranscription }),
1681
+ hasText ? /* @__PURE__ */ jsx18(
1682
+ "p",
1683
+ {
1684
+ ref: textRef,
1685
+ className: "cv-p-transcription__text",
1686
+ lang: transcription.language,
1687
+ "data-expanded": isExpanded ? "true" : void 0,
1688
+ children: transcription.text
1689
+ }
1690
+ ) : /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription__empty", children: labels.emptyTranscription }),
1691
+ showsToggle ? /* @__PURE__ */ jsx18(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ className: "cv-p-transcription__toggle",
1696
+ "aria-expanded": isExpanded,
1697
+ onClick: () => setIsExpanded((current) => !current),
1698
+ children: isExpanded ? labels.collapseTranscription : labels.showFullTranscription
1699
+ }
1700
+ ) : null
1701
+ ] });
1702
+ }
1703
+
1704
+ // src/participant/useStableAttachmentUrl.ts
1705
+ import { useCallback as useCallback5, useEffect as useEffect10, useRef as useRef11, useState as useState6 } from "react";
1706
+
1707
+ // src/participant/participantAttachmentUrl.ts
1708
+ var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
1709
+ function isSafeAttachmentUrl(url) {
1710
+ try {
1711
+ return SAFE_ATTACHMENT_PROTOCOLS.has(new URL(url.trim()).protocol);
1712
+ } catch {
1713
+ return false;
1714
+ }
1715
+ }
1716
+
1717
+ // src/participant/useStableAttachmentUrl.ts
1718
+ var ATTACHMENT_URL_RETRY_DELAY_MS = 1e3;
1719
+ var PENDING_STATE = { status: "pending", url: void 0 };
1720
+ async function resolveSafely({ attachment, resolveAttachmentUrl }) {
1721
+ try {
1722
+ const resolved = await resolveAttachmentUrl(attachment, "inline");
1723
+ return isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0;
1724
+ } catch {
1725
+ return void 0;
1726
+ }
663
1727
  }
664
- function shouldShowAvatar({ previousAuthor, author, isMine }) {
665
- if (isMine) return false;
666
- if (previousAuthor === void 0) return true;
667
- return previousAuthor !== author;
1728
+ function useStableAttachmentUrl(params) {
1729
+ const latest = useRef11(params);
1730
+ const currentId = useRef11(params.attachment.id);
1731
+ const [state, setState2] = useState6(PENDING_STATE);
1732
+ const { id } = params.attachment;
1733
+ useEffect10(() => {
1734
+ latest.current = params;
1735
+ });
1736
+ useEffect10(() => {
1737
+ currentId.current = id;
1738
+ setState2(PENDING_STATE);
1739
+ let retryTimer;
1740
+ const settle = (url) => {
1741
+ if (currentId.current === id) setState2(url ? { status: "ready", url } : { status: "failed", url: void 0 });
1742
+ };
1743
+ resolveSafely(latest.current).then((url) => {
1744
+ if (url || currentId.current !== id) return settle(url);
1745
+ retryTimer = setTimeout(() => void resolveSafely(latest.current).then(settle), ATTACHMENT_URL_RETRY_DELAY_MS);
1746
+ });
1747
+ return () => {
1748
+ currentId.current = "";
1749
+ clearTimeout(retryTimer);
1750
+ };
1751
+ }, [id]);
1752
+ const resolveAgain = useCallback5(async () => {
1753
+ const requestedId = currentId.current;
1754
+ const url = await resolveSafely(latest.current);
1755
+ if (url && currentId.current === requestedId) setState2({ status: "ready", url });
1756
+ return url;
1757
+ }, []);
1758
+ return { ...state, resolveAgain };
668
1759
  }
669
1760
 
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 });
1761
+ // src/participant/ParticipantAudioAttachment.tsx
1762
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1763
+ function ParticipantAudioAttachment({ attachment, resolveAttachmentUrl, labels }) {
1764
+ const source = useStableAttachmentUrl({ attachment, resolveAttachmentUrl });
1765
+ const audioLabels = resolveParticipantAudioLabels(labels);
1766
+ return /* @__PURE__ */ jsxs14("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
1767
+ /* @__PURE__ */ jsx19(
1768
+ ParticipantAudioPlayer,
1769
+ {
1770
+ labels: audioLabels,
1771
+ url: source.url,
1772
+ durationMs: attachment.durationMs,
1773
+ filename: attachment.filename,
1774
+ isPending: source.status === "pending",
1775
+ isUnavailable: source.status === "failed",
1776
+ onSourceError: source.resolveAgain
1777
+ }
1778
+ ),
1779
+ /* @__PURE__ */ jsx19("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1780
+ /* @__PURE__ */ jsx19(ParticipantAudioTranscription, { attachmentId: attachment.id, transcription: attachment.transcription, labels: audioLabels })
1781
+ ] });
678
1782
  }
679
1783
 
680
- // src/participant/participantDay.ts
681
- function pad(value) {
682
- return String(value).padStart(2, "0");
1784
+ // src/participant/ParticipantAttachmentItem.tsx
1785
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1786
+ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
1787
+ const [url, setUrl] = useState7(void 0);
1788
+ const disposition = attachment.kind === "document" ? "attachment" : "inline";
1789
+ useEffect11(() => {
1790
+ let isCurrent = true;
1791
+ resolveAttachmentUrl(attachment, disposition).then(
1792
+ (resolved) => {
1793
+ if (isCurrent) setUrl(isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0);
1794
+ },
1795
+ () => {
1796
+ if (isCurrent) setUrl(void 0);
1797
+ }
1798
+ );
1799
+ return () => {
1800
+ isCurrent = false;
1801
+ };
1802
+ }, [attachment, disposition, resolveAttachmentUrl]);
1803
+ return url;
683
1804
  }
684
- function toDateTimeAttribute(date) {
685
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1805
+ function ParticipantAttachmentItem(props) {
1806
+ if (props.attachment.kind === "audio") return /* @__PURE__ */ jsx20(ParticipantAudioAttachment, { ...props });
1807
+ return /* @__PURE__ */ jsx20(ParticipantFileAttachment, { ...props });
686
1808
  }
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 }) });
1809
+ function ParticipantFileAttachment({ attachment, resolveAttachmentUrl, labels }) {
1810
+ const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
1811
+ if (attachment.kind === "image") {
1812
+ return /* @__PURE__ */ jsxs15("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
1813
+ url ? /* @__PURE__ */ jsx20("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
1814
+ /* @__PURE__ */ jsx20("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
1815
+ ] });
1816
+ }
1817
+ return /* @__PURE__ */ jsxs15(
1818
+ "a",
1819
+ {
1820
+ className: "cv-p-attachment cv-p-attachment--document",
1821
+ href: url,
1822
+ download: attachment.filename,
1823
+ "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
1824
+ children: [
1825
+ /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1826
+ /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
1827
+ ]
1828
+ }
1829
+ );
693
1830
  }
694
1831
 
695
1832
  // src/participant/ParticipantBubbleStatus.tsx
696
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1833
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
697
1834
  function statusText(status, labels) {
698
1835
  const byStatus = {
699
1836
  sending: labels.statusSending,
@@ -708,34 +1845,215 @@ function statusText(status, labels) {
708
1845
  function OwnStatus({ status, labels, onRetry }) {
709
1846
  const ticksStatus = status === "sending" ? "queued" : status;
710
1847
  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
1848
  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 })
1849
+ const isTextVisible = isVisibleText && !(status === "failed" && onRetry);
1850
+ return /* @__PURE__ */ jsxs16("span", { className: "cv-p-bubble__status", children: [
1851
+ /* @__PURE__ */ jsx21(StatusTicks, { status: ticksStatus, appearance: "stylesheet", title: isTextVisible ? void 0 : text }),
1852
+ /* @__PURE__ */ jsx21("span", { className: isTextVisible ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
1853
+ ] });
1854
+ }
1855
+ function RetryButton({ labels, onRetry, describedBy }) {
1856
+ return /* @__PURE__ */ jsx21(
1857
+ "button",
1858
+ {
1859
+ type: "button",
1860
+ className: "cv-p-failed-retry",
1861
+ "aria-label": labels.statusFailed,
1862
+ "aria-describedby": describedBy,
1863
+ "data-cv-tooltip": labels.statusFailed,
1864
+ onClick: onRetry,
1865
+ children: /* @__PURE__ */ jsx21("span", { className: "cv-p-failed-retry__icon", children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx21("path", { d: "M13 8a5 5 0 1 1-1.6-3.7M13 2.5v3h-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) }) })
1866
+ }
1867
+ );
1868
+ }
1869
+
1870
+ // src/participant/ParticipantFailedMenu.tsx
1871
+ import { useId as useId2 } from "react";
1872
+
1873
+ // src/participant/ParticipantFailedMenuList.tsx
1874
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
1875
+ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard }) {
1876
+ function runAndClose(action) {
1877
+ return () => {
1878
+ menu.close();
1879
+ action();
1880
+ };
1881
+ }
1882
+ const className = `cv-p-failed-menu__list${menu.placement === "below" ? " cv-p-failed-menu__list--below" : ""}`;
1883
+ return /* @__PURE__ */ jsxs17("div", { role: "menu", "aria-labelledby": labelledBy, ref: menu.menuRef, className, onKeyDown: menu.handleMenuKeyDown, children: [
1884
+ onEdit ? /* @__PURE__ */ jsx22("button", { type: "button", role: "menuitem", tabIndex: -1, className: "cv-p-failed-menu__item", onClick: runAndClose(onEdit), children: labels.edit }) : null,
1885
+ onDiscard ? /* @__PURE__ */ jsx22(
1886
+ "button",
1887
+ {
1888
+ type: "button",
1889
+ role: "menuitem",
1890
+ tabIndex: -1,
1891
+ className: "cv-p-failed-menu__item cv-p-failed-menu__item--danger",
1892
+ onClick: runAndClose(onDiscard),
1893
+ children: labels.discard
1894
+ }
1895
+ ) : null
721
1896
  ] });
722
1897
  }
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
1898
+
1899
+ // src/participant/useParticipantFailedMenu.ts
1900
+ import { useReducer as useReducer2, useRef as useRef12 } from "react";
1901
+
1902
+ // src/participant/participantFailedMenuState.ts
1903
+ var CLOSED_FAILED_MENU = { isOpen: false, initialFocus: "first" };
1904
+ function failedMenuReducer(state, action) {
1905
+ if (action.type === "open") return { isOpen: true, initialFocus: action.initialFocus };
1906
+ if (action.type === "close") return state.isOpen ? CLOSED_FAILED_MENU : state;
1907
+ return state.isOpen ? CLOSED_FAILED_MENU : { isOpen: true, initialFocus: "first" };
1908
+ }
1909
+ function resolveFailedMenuKey({ key, index, count }) {
1910
+ if (key === "Escape") return { type: "close", shouldRestoreFocus: true };
1911
+ if (key === "Tab") return { type: "close", shouldRestoreFocus: false };
1912
+ if (key === "ArrowDown") return { type: "focus", index: (index + 1) % count };
1913
+ if (key === "ArrowUp") return { type: "focus", index: (index - 1 + count) % count };
1914
+ if (key === "Home") return { type: "focus", index: 0 };
1915
+ if (key === "End") return { type: "focus", index: count - 1 };
1916
+ return void 0;
1917
+ }
1918
+ function resolveFailedMenuTriggerKey(key) {
1919
+ if (key === "ArrowDown") return "first";
1920
+ if (key === "ArrowUp") return "last";
1921
+ return void 0;
1922
+ }
1923
+ var MENU_GAP_PX = 8;
1924
+ function resolveFailedMenuPlacement({ anchorTop, scrollerTop, menuHeight }) {
1925
+ return anchorTop - scrollerTop >= menuHeight + MENU_GAP_PX ? "above" : "below";
1926
+ }
1927
+
1928
+ // src/participant/failedMenuKeyboard.ts
1929
+ function handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }) {
1930
+ const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
1931
+ const index = items.findIndex((item) => item === document.activeElement);
1932
+ const result = resolveFailedMenuKey({ key: event.key, index, count: items.length });
1933
+ if (!result) return;
1934
+ if (result.type === "focus") {
1935
+ event.preventDefault();
1936
+ items[result.index]?.focus();
1937
+ return;
1938
+ }
1939
+ if (result.shouldRestoreFocus) {
1940
+ event.preventDefault();
1941
+ event.stopPropagation();
1942
+ triggerRef.current?.focus();
1943
+ }
1944
+ dispatch({ type: "close" });
1945
+ }
1946
+
1947
+ // src/participant/useFailedMenuEffects.ts
1948
+ import { useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useState as useState8 } from "react";
1949
+ var SCROLLER_SELECTOR2 = ".cv-p-thread__scroll";
1950
+ function useFailedMenuInitialFocus(state, menuRef) {
1951
+ useEffect12(() => {
1952
+ if (!state.isOpen) return;
1953
+ const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
1954
+ const target = state.initialFocus === "last" ? items[items.length - 1] : items[0];
1955
+ target?.focus();
1956
+ }, [state, menuRef]);
1957
+ }
1958
+ function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
1959
+ useEffect12(() => {
1960
+ if (!isOpen) return;
1961
+ function handlePointerDown(event) {
1962
+ if (event.target instanceof Node && containerRef.current?.contains(event.target)) return;
1963
+ dispatch({ type: "close" });
1964
+ }
1965
+ document.addEventListener("pointerdown", handlePointerDown);
1966
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
1967
+ }, [isOpen, containerRef, dispatch]);
1968
+ }
1969
+ function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
1970
+ const [placement, setPlacement] = useState8("above");
1971
+ useLayoutEffect2(() => {
1972
+ const trigger = triggerRef.current;
1973
+ const menu = menuRef.current;
1974
+ if (!isOpen || !trigger || !menu) return;
1975
+ const anchor = trigger.closest(".cv-p-bubble") ?? trigger;
1976
+ const scroller = trigger.closest(SCROLLER_SELECTOR2);
1977
+ setPlacement(
1978
+ resolveFailedMenuPlacement({
1979
+ anchorTop: anchor.getBoundingClientRect().top,
1980
+ scrollerTop: scroller?.getBoundingClientRect().top ?? 0,
1981
+ menuHeight: menu.getBoundingClientRect().height
1982
+ })
1983
+ );
1984
+ }, [isOpen, triggerRef, menuRef]);
1985
+ return placement;
1986
+ }
1987
+
1988
+ // src/participant/useParticipantFailedMenu.ts
1989
+ function useParticipantFailedMenu() {
1990
+ const [state, dispatch] = useReducer2(failedMenuReducer, CLOSED_FAILED_MENU);
1991
+ const containerRef = useRef12(null);
1992
+ const triggerRef = useRef12(null);
1993
+ const menuRef = useRef12(null);
1994
+ useFailedMenuInitialFocus(state, menuRef);
1995
+ useFailedMenuOutsideClose(state.isOpen, containerRef, dispatch);
1996
+ const placement = useFailedMenuPlacement(state.isOpen, { triggerRef, menuRef });
1997
+ function handleTriggerKeyDown(event) {
1998
+ const initialFocus = resolveFailedMenuTriggerKey(event.key);
1999
+ if (!initialFocus) return;
2000
+ event.preventDefault();
2001
+ dispatch({ type: "open", initialFocus });
2002
+ }
2003
+ function handleBlur(event) {
2004
+ const next = event.relatedTarget;
2005
+ if (next instanceof Node && !containerRef.current?.contains(next)) dispatch({ type: "close" });
2006
+ }
2007
+ return {
2008
+ state,
2009
+ placement,
2010
+ containerRef,
2011
+ triggerRef,
2012
+ menuRef,
2013
+ handleTriggerClick: () => dispatch({ type: "toggle" }),
2014
+ handleTriggerKeyDown,
2015
+ handleMenuKeyDown: (event) => handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }),
2016
+ handleBlur,
2017
+ close: () => dispatch({ type: "close" })
2018
+ };
2019
+ }
2020
+
2021
+ // src/participant/ParticipantFailedMenu.tsx
2022
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
2023
+ function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
2024
+ const menu = useParticipantFailedMenu();
2025
+ const triggerId = useId2();
2026
+ if (!onEdit && !onDiscard) return null;
2027
+ const label = labels.messageOptions ?? DEFAULT_MESSAGE_OPTIONS_LABEL;
2028
+ return /* @__PURE__ */ jsxs18("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
2029
+ /* @__PURE__ */ jsx23(
2030
+ "button",
2031
+ {
2032
+ type: "button",
2033
+ id: triggerId,
2034
+ ref: menu.triggerRef,
2035
+ className: "cv-p-failed-menu__trigger",
2036
+ "aria-haspopup": "menu",
2037
+ "aria-expanded": menu.state.isOpen,
2038
+ "aria-label": label,
2039
+ "data-cv-tooltip": menu.state.isOpen ? void 0 : label,
2040
+ onClick: menu.handleTriggerClick,
2041
+ onKeyDown: menu.handleTriggerKeyDown,
2042
+ children: /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", children: "\u22EF" })
2043
+ }
2044
+ ),
2045
+ menu.state.isOpen ? /* @__PURE__ */ jsx23(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
728
2046
  ] });
729
2047
  }
730
2048
 
731
2049
  // src/participant/ParticipantMessageText.tsx
732
- import { useMemo } from "react";
2050
+ import { useMemo as useMemo2 } from "react";
733
2051
 
734
2052
  // src/participant/ParticipantCopyableToken.tsx
735
2053
  import { Check, Copy } from "lucide-react";
736
2054
 
737
2055
  // src/participant/useParticipantCopyFeedback.ts
738
- import { useEffect as useEffect3, useReducer as useReducer2 } from "react";
2056
+ import { useEffect as useEffect13, useReducer as useReducer3 } from "react";
739
2057
 
740
2058
  // src/participant/participantProtocolCopy.ts
741
2059
  async function copyProtocolToClipboard(protocol, clipboard) {
@@ -756,10 +2074,10 @@ function protocolCopyReducer(state, action) {
756
2074
  // src/participant/useParticipantCopyFeedback.ts
757
2075
  var COPIED_FEEDBACK_MS = 2e3;
758
2076
  function useParticipantCopyFeedback() {
759
- const [state, dispatch] = useReducer2(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
2077
+ const [state, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
760
2078
  const announce = useAnnounceCopy();
761
2079
  const { isCopied, copyCount } = state;
762
- useEffect3(() => {
2080
+ useEffect13(() => {
763
2081
  if (!isCopied) return void 0;
764
2082
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_FEEDBACK_MS);
765
2083
  return () => clearTimeout(timer);
@@ -774,10 +2092,10 @@ function useParticipantCopyFeedback() {
774
2092
  }
775
2093
 
776
2094
  // src/participant/ParticipantCopyableToken.tsx
777
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
2095
+ import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
778
2096
  function ParticipantCopyableToken({ copyKind, value, labels }) {
779
2097
  const { isCopied, copy } = useParticipantCopyFeedback();
780
- return /* @__PURE__ */ jsxs10(
2098
+ return /* @__PURE__ */ jsxs19(
781
2099
  "button",
782
2100
  {
783
2101
  type: "button",
@@ -786,8 +2104,8 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
786
2104
  onClick: () => copy(value, labels.valueCopied),
787
2105
  children: [
788
2106
  value,
789
- isCopied ? /* @__PURE__ */ jsxs10("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
790
- /* @__PURE__ */ jsx14(Check, { size: 12, "aria-hidden": true }),
2107
+ isCopied ? /* @__PURE__ */ jsxs19("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
2108
+ /* @__PURE__ */ jsx24(Check, { size: 12, "aria-hidden": true }),
791
2109
  " ",
792
2110
  labels.valueCopied
793
2111
  ] }) : null
@@ -797,33 +2115,34 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
797
2115
  }
798
2116
  function ParticipantLinkCopyButton({ value, labels }) {
799
2117
  const { isCopied, copy } = useParticipantCopyFeedback();
800
- return /* @__PURE__ */ jsx14(
2118
+ return /* @__PURE__ */ jsx24(
801
2119
  "button",
802
2120
  {
803
2121
  type: "button",
804
2122
  className: "cv-p-text__link-copy",
805
2123
  "aria-label": `${labels.copyValue} ${value}`,
2124
+ "data-cv-tooltip": labels.copyValue,
806
2125
  onClick: () => copy(value, labels.valueCopied),
807
- children: isCopied ? /* @__PURE__ */ jsx14(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx14(Copy, { size: 14, "aria-hidden": true })
2126
+ children: isCopied ? /* @__PURE__ */ jsx24(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx24(Copy, { size: 14, "aria-hidden": true })
808
2127
  }
809
2128
  );
810
2129
  }
811
2130
 
812
2131
  // src/participant/ParticipantMessageNodes.tsx
813
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
2132
+ import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
814
2133
  var LINK_REL = "noopener noreferrer nofollow ugc";
815
2134
  function renderLink(node, labels, key) {
816
2135
  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: [
2136
+ return /* @__PURE__ */ jsxs20("span", { className: "cv-p-text__link-group", children: [
2137
+ /* @__PURE__ */ jsxs20("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
819
2138
  node.label,
820
- isExternal ? /* @__PURE__ */ jsxs11("span", { className: "cv-p-sr-only", children: [
2139
+ isExternal ? /* @__PURE__ */ jsxs20("span", { className: "cv-p-sr-only", children: [
821
2140
  " (",
822
2141
  labels.externalLinkHint,
823
2142
  ")"
824
2143
  ] }) : null
825
2144
  ] }),
826
- /* @__PURE__ */ jsx15(ParticipantLinkCopyButton, { value: node.href, labels })
2145
+ /* @__PURE__ */ jsx25(ParticipantLinkCopyButton, { value: node.href, labels })
827
2146
  ] }, key);
828
2147
  }
829
2148
  function renderInlineNodes(nodes, labels) {
@@ -832,17 +2151,17 @@ function renderInlineNodes(nodes, labels) {
832
2151
  case "text":
833
2152
  return node.value;
834
2153
  case "strong":
835
- return /* @__PURE__ */ jsx15("strong", { children: renderInlineNodes(node.children, labels) }, key);
2154
+ return /* @__PURE__ */ jsx25("strong", { children: renderInlineNodes(node.children, labels) }, key);
836
2155
  case "em":
837
- return /* @__PURE__ */ jsx15("em", { children: renderInlineNodes(node.children, labels) }, key);
2156
+ return /* @__PURE__ */ jsx25("em", { children: renderInlineNodes(node.children, labels) }, key);
838
2157
  case "del":
839
- return /* @__PURE__ */ jsx15("del", { children: renderInlineNodes(node.children, labels) }, key);
2158
+ return /* @__PURE__ */ jsx25("del", { children: renderInlineNodes(node.children, labels) }, key);
840
2159
  case "code":
841
- return /* @__PURE__ */ jsx15("code", { className: "cv-p-text__code", children: node.value }, key);
2160
+ return /* @__PURE__ */ jsx25("code", { className: "cv-p-text__code", children: node.value }, key);
842
2161
  case "link":
843
2162
  return renderLink(node, labels, key);
844
2163
  case "copyable":
845
- return /* @__PURE__ */ jsx15(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
2164
+ return /* @__PURE__ */ jsx25(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
846
2165
  }
847
2166
  });
848
2167
  }
@@ -1228,90 +2547,56 @@ function startsFenceLine(line) {
1228
2547
  }
1229
2548
 
1230
2549
  // src/participant/ParticipantMessageText.tsx
1231
- import { jsx as jsx16 } from "react/jsx-runtime";
2550
+ import { jsx as jsx26 } from "react/jsx-runtime";
1232
2551
  function renderBlock(block, labels, key) {
1233
- if (block.kind === "paragraph") return /* @__PURE__ */ jsx16("span", { children: renderInlineNodes(block.children, labels) }, key);
2552
+ if (block.kind === "paragraph") return /* @__PURE__ */ jsx26("span", { children: renderInlineNodes(block.children, labels) }, key);
1234
2553
  if (block.kind === "pre") {
1235
- return /* @__PURE__ */ jsx16("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx16("code", { children: block.value }) }, key);
2554
+ return /* @__PURE__ */ jsx26("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx26("code", { children: block.value }) }, key);
1236
2555
  }
1237
2556
  if (block.kind === "quote") {
1238
- return /* @__PURE__ */ jsx16("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
2557
+ return /* @__PURE__ */ jsx26("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
1239
2558
  }
1240
- const items = block.items.map((item, index) => /* @__PURE__ */ jsx16("li", { children: renderInlineNodes(item, labels) }, index));
2559
+ const items = block.items.map((item, index) => /* @__PURE__ */ jsx26("li", { children: renderInlineNodes(item, labels) }, index));
1241
2560
  if (block.ordered) {
1242
- return /* @__PURE__ */ jsx16("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
2561
+ return /* @__PURE__ */ jsx26("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
1243
2562
  }
1244
- return /* @__PURE__ */ jsx16("ul", { className: "cv-p-text__list", children: items }, key);
2563
+ return /* @__PURE__ */ jsx26("ul", { className: "cv-p-text__list", children: items }, key);
1245
2564
  }
1246
- function ParticipantMessageText({ text, labels }) {
1247
- const blocks = useMemo(() => parseParticipantMessage(text), [text]);
2565
+ function ParticipantMessageText({ text, labels, id }) {
2566
+ const blocks = useMemo2(() => parseParticipantMessage(text), [text]);
1248
2567
  if (blocks.length === 0) return null;
1249
2568
  const [only] = blocks;
1250
2569
  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 });
2570
+ return /* @__PURE__ */ jsx26("div", { className: "cv-p-text", id, children });
1252
2571
  }
1253
2572
 
1254
- // src/participant/ParticipantAttachmentItem.tsx
1255
- import { useEffect as useEffect4, useState as useState2 } from "react";
1256
-
1257
- // src/participant/participantAttachmentUrl.ts
1258
- var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
1259
- function isSafeAttachmentUrl(url) {
1260
- try {
1261
- return SAFE_ATTACHMENT_PROTOCOLS.has(new URL(url.trim()).protocol);
1262
- } catch {
1263
- return false;
1264
- }
2573
+ // src/participant/ParticipantBubbleBody.tsx
2574
+ import { Fragment as Fragment6, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
2575
+ function BubbleHeading({ content, labels }) {
2576
+ if (content.isMine) return /* @__PURE__ */ jsx27("span", { className: "cv-p-sr-only", children: labels.me });
2577
+ if (!content.authorName) return null;
2578
+ return /* @__PURE__ */ jsx27("span", { className: "cv-p-bubble__author", children: content.authorName });
1265
2579
  }
1266
-
1267
- // src/participant/ParticipantAttachmentItem.tsx
1268
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1269
- function useAttachmentUrl(attachment, resolveAttachmentUrl) {
1270
- const [url, setUrl] = useState2(void 0);
1271
- const disposition = attachment.kind === "document" ? "attachment" : "inline";
1272
- useEffect4(() => {
1273
- let isCurrent = true;
1274
- resolveAttachmentUrl(attachment, disposition).then(
1275
- (resolved) => {
1276
- if (isCurrent) setUrl(isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0);
2580
+ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
2581
+ return /* @__PURE__ */ jsxs21(Fragment6, { children: [
2582
+ /* @__PURE__ */ jsx27(BubbleHeading, { content, labels }),
2583
+ content.text ? /* @__PURE__ */ jsx27(ParticipantMessageText, { text: content.text, labels, id: textId }) : null,
2584
+ content.attachments.map((attachment) => /* @__PURE__ */ jsx27(
2585
+ ParticipantAttachmentItem,
2586
+ {
2587
+ attachment,
2588
+ resolveAttachmentUrl,
2589
+ labels
1277
2590
  },
1278
- () => {
1279
- if (isCurrent) setUrl(void 0);
1280
- }
1281
- );
1282
- return () => {
1283
- isCurrent = false;
1284
- };
1285
- }, [attachment, disposition, resolveAttachmentUrl]);
1286
- return url;
1287
- }
1288
- function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
1289
- const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
1290
- if (attachment.kind === "image") {
1291
- return /* @__PURE__ */ jsxs12("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
1292
- url ? /* @__PURE__ */ jsx17("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
1293
- /* @__PURE__ */ jsx17("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
1294
- ] });
1295
- }
1296
- if (attachment.kind === "audio") {
1297
- return /* @__PURE__ */ jsxs12("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
1298
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1299
- /* @__PURE__ */ jsx17("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
1300
- ] });
1301
- }
1302
- return /* @__PURE__ */ jsxs12(
1303
- "a",
1304
- {
1305
- className: "cv-p-attachment cv-p-attachment--document",
1306
- href: url,
1307
- download: attachment.filename,
1308
- "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
1309
- children: [
1310
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1311
- /* @__PURE__ */ jsx17("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
1312
- ]
1313
- }
1314
- );
2591
+ attachment.id
2592
+ )),
2593
+ content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx27("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
2594
+ /* @__PURE__ */ jsxs21("span", { className: "cv-p-bubble__meta", children: [
2595
+ /* @__PURE__ */ jsx27("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
2596
+ ownStatus ? /* @__PURE__ */ jsx27(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
2597
+ ownStatus === "failed" ? /* @__PURE__ */ jsx27(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
2598
+ ] })
2599
+ ] });
1315
2600
  }
1316
2601
 
1317
2602
  // src/participant/participantPerspective.ts
@@ -1376,7 +2661,7 @@ function resolveOwnMessageStatus(params) {
1376
2661
  }
1377
2662
 
1378
2663
  // src/participant/ParticipantMessageBubble.tsx
1379
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2664
+ import { jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
1380
2665
  function describeItem(item, perspective) {
1381
2666
  if (item.kind === "server") {
1382
2667
  const { message } = item;
@@ -1399,49 +2684,27 @@ function describeItem(item, perspective) {
1399
2684
  displayState: item.displayState
1400
2685
  };
1401
2686
  }
1402
- function bubbleClassName(isMine, hasTail) {
2687
+ function bubbleClassName(isMine, hasTail, hasFailed) {
1403
2688
  const classes = ["cv-p-bubble"];
1404
2689
  if (isMine) classes.push("cv-p-bubble--mine");
2690
+ if (hasFailed) classes.push("cv-p-bubble--failed");
1405
2691
  if (!hasTail) classes.push("cv-p-bubble--no-tail");
1406
2692
  return classes.join(" ");
1407
2693
  }
1408
- function ParticipantMessageBubble({
1409
- item,
1410
- labels,
1411
- confirmsRead,
1412
- resolveAttachmentUrl,
1413
- onRetry,
1414
- onDiscard,
1415
- onEdit,
1416
- avatar,
1417
- tail = true,
1418
- perspective
1419
- }) {
2694
+ function ParticipantMessageBubble(props) {
2695
+ const { item, labels, confirmsRead, onRetry, avatar, tail = true, perspective } = props;
2696
+ const textId = useId3();
1420
2697
  const content = describeItem(item, perspective);
1421
- const { isMine } = content;
1422
- const ownStatus = isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1423
- const bubble = /* @__PURE__ */ jsxs13("div", { className: bubbleClassName(isMine, tail), children: [
1424
- isMine ? /* @__PURE__ */ jsx18("span", { className: "cv-p-sr-only", children: labels.me }) : content.authorName ? /* @__PURE__ */ jsx18("span", { className: "cv-p-bubble__author", children: content.authorName }) : null,
1425
- content.text ? /* @__PURE__ */ jsx18(ParticipantMessageText, { text: content.text, labels }) : null,
1426
- content.attachments.map((attachment) => /* @__PURE__ */ jsx18(
1427
- ParticipantAttachmentItem,
1428
- {
1429
- attachment,
1430
- resolveAttachmentUrl,
1431
- labels
1432
- },
1433
- attachment.id
1434
- )),
1435
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx18("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1436
- /* @__PURE__ */ jsxs13("span", { className: "cv-p-bubble__meta", children: [
1437
- /* @__PURE__ */ jsx18("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1438
- ownStatus ? /* @__PURE__ */ jsx18(OwnStatus, { status: ownStatus, labels, onRetry }) : null
1439
- ] }),
1440
- ownStatus === "failed" ? /* @__PURE__ */ jsx18(FailedActions, { labels, onDiscard, onEdit }) : null
2698
+ const ownStatus = content.isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
2699
+ const isFailed = ownStatus === "failed";
2700
+ const canRetry = isFailed && onRetry !== void 0;
2701
+ const bubble = /* @__PURE__ */ jsxs22("div", { className: bubbleClassName(content.isMine, tail, isFailed), children: [
2702
+ canRetry ? /* @__PURE__ */ jsx28(RetryButton, { labels, onRetry, describedBy: textId }) : null,
2703
+ /* @__PURE__ */ jsx28(BubbleBody, { ...props, content, ownStatus, textId: canRetry ? textId : void 0 })
1441
2704
  ] });
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 }),
2705
+ if (avatar === void 0 || content.isMine) return bubble;
2706
+ return /* @__PURE__ */ jsxs22("div", { className: "cv-p-bubble-row", children: [
2707
+ /* @__PURE__ */ jsx28("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1445
2708
  bubble
1446
2709
  ] });
1447
2710
  }
@@ -1463,7 +2726,7 @@ function resolveParticipantBubbleActions(item, handlers) {
1463
2726
  }
1464
2727
 
1465
2728
  // src/participant/ParticipantTimeline.tsx
1466
- import { jsx as jsx19 } from "react/jsx-runtime";
2729
+ import { jsx as jsx29 } from "react/jsx-runtime";
1467
2730
  function createdAtOf2(item) {
1468
2731
  return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1469
2732
  }
@@ -1480,7 +2743,7 @@ function resolveActions(item, pendingActions) {
1480
2743
  }
1481
2744
  function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1482
2745
  if (!isEnabled || isMine) return void 0;
1483
- return showAvatar ? /* @__PURE__ */ jsx19(ParticipantAuthorAvatar, { name: author, render }) : null;
2746
+ return showAvatar ? /* @__PURE__ */ jsx29(ParticipantAuthorAvatar, { name: author, render }) : null;
1484
2747
  }
1485
2748
  function ParticipantTimeline(props) {
1486
2749
  const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
@@ -1492,7 +2755,7 @@ function ParticipantTimeline(props) {
1492
2755
  for (const item of items) {
1493
2756
  const day = new Date(createdAtOf2(item));
1494
2757
  if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1495
- nodes.push(/* @__PURE__ */ jsx19(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
2758
+ nodes.push(/* @__PURE__ */ jsx29(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1496
2759
  previousAuthor = void 0;
1497
2760
  }
1498
2761
  previousDay = day;
@@ -1507,7 +2770,7 @@ function ParticipantTimeline(props) {
1507
2770
  render: props.renderAuthorAvatar
1508
2771
  });
1509
2772
  nodes.push(
1510
- /* @__PURE__ */ jsx19(
2773
+ /* @__PURE__ */ jsx29(
1511
2774
  ParticipantMessageBubble,
1512
2775
  {
1513
2776
  item,
@@ -1523,19 +2786,19 @@ function ParticipantTimeline(props) {
1523
2786
  )
1524
2787
  );
1525
2788
  }
1526
- return /* @__PURE__ */ jsx19("div", { className: "cv-p-thread__messages", children: nodes });
2789
+ return /* @__PURE__ */ jsx29("div", { className: "cv-p-thread__messages", children: nodes });
1527
2790
  }
1528
2791
 
1529
2792
  // src/participant/ParticipantConversationAvatar.tsx
1530
- import { jsx as jsx20 } from "react/jsx-runtime";
2793
+ import { jsx as jsx30 } from "react/jsx-runtime";
1531
2794
  function ParticipantConversationAvatar({ icon }) {
1532
- return /* @__PURE__ */ jsx20("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
2795
+ return /* @__PURE__ */ jsx30("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
1533
2796
  }
1534
2797
 
1535
2798
  // src/participant/ParticipantProtocolBadge.tsx
1536
2799
  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";
2800
+ import { useEffect as useEffect14, useReducer as useReducer4 } from "react";
2801
+ import { Fragment as Fragment7, jsx as jsx31, jsxs as jsxs23 } from "react/jsx-runtime";
1539
2802
  var COPIED_ANNOUNCEMENT_MS = 3e3;
1540
2803
  function ParticipantProtocolBadgeView({
1541
2804
  protocol,
@@ -1544,36 +2807,37 @@ function ParticipantProtocolBadgeView({
1544
2807
  onCopy
1545
2808
  }) {
1546
2809
  const { isCopied, copyCount } = copyState;
1547
- return /* @__PURE__ */ jsxs14(Fragment4, { children: [
1548
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1549
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-protocol__code", children: [
1550
- /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
2810
+ return /* @__PURE__ */ jsxs23(Fragment7, { children: [
2811
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
2812
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-protocol__code", children: [
2813
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-sr-only", children: [
1551
2814
  labels.protocolPrefix,
1552
2815
  " "
1553
2816
  ] }),
1554
2817
  protocol
1555
2818
  ] }),
1556
- /* @__PURE__ */ jsxs14(
2819
+ /* @__PURE__ */ jsxs23(
1557
2820
  "button",
1558
2821
  {
1559
2822
  type: "button",
1560
2823
  className: "cv-p-protocol__copy",
1561
2824
  "aria-label": `${labels.copyProtocol} ${protocol}`,
2825
+ "data-cv-tooltip": labels.copyProtocol,
1562
2826
  onClick: onCopy,
1563
2827
  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
2828
+ isCopied ? /* @__PURE__ */ jsx31(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx31(Copy2, { size: 18, "aria-hidden": true }),
2829
+ isCopied ? /* @__PURE__ */ jsx31("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
1566
2830
  ]
1567
2831
  }
1568
2832
  )
1569
2833
  ] }),
1570
- /* @__PURE__ */ jsx21("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx21("span", { children: labels.protocolCopied }, copyCount) : null })
2834
+ /* @__PURE__ */ jsx31("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx31("span", { children: labels.protocolCopied }, copyCount) : null })
1571
2835
  ] });
1572
2836
  }
1573
2837
  function ParticipantProtocolBadge({ protocol, labels }) {
1574
- const [copyState, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
2838
+ const [copyState, dispatch] = useReducer4(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1575
2839
  const { isCopied, copyCount } = copyState;
1576
- useEffect5(() => {
2840
+ useEffect14(() => {
1577
2841
  if (!isCopied) return void 0;
1578
2842
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
1579
2843
  return () => clearTimeout(timer);
@@ -1582,7 +2846,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1582
2846
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1583
2847
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
1584
2848
  }
1585
- return /* @__PURE__ */ jsx21(
2849
+ return /* @__PURE__ */ jsx31(
1586
2850
  ParticipantProtocolBadgeView,
1587
2851
  {
1588
2852
  protocol,
@@ -1594,19 +2858,19 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1594
2858
  }
1595
2859
 
1596
2860
  // src/participant/ParticipantThreadHeader.tsx
1597
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
2861
+ import { jsx as jsx32, jsxs as jsxs24 } from "react/jsx-runtime";
1598
2862
  function Title({ conversation, labels, onOpenSubject }) {
1599
2863
  const { subjectType, subjectId, subjectLabel } = conversation;
1600
- return /* @__PURE__ */ jsx22("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx22(
2864
+ return /* @__PURE__ */ jsx32("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx32(
1601
2865
  "button",
1602
2866
  {
1603
2867
  type: "button",
1604
2868
  className: "cv-p-thread__title-button",
1605
2869
  "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1606
2870
  onClick: () => onOpenSubject({ subjectType, subjectId }),
1607
- children: /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel })
2871
+ children: /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1608
2872
  }
1609
- ) : /* @__PURE__ */ jsx22("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
2873
+ ) : /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1610
2874
  }
1611
2875
  function ParticipantThreadHeader({
1612
2876
  conversation,
@@ -1621,30 +2885,30 @@ function ParticipantThreadHeader({
1621
2885
  const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1622
2886
  const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1623
2887
  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 })
2888
+ return /* @__PURE__ */ jsxs24("header", { className: "cv-p-thread__header", children: [
2889
+ onBack ? /* @__PURE__ */ jsx32(ParticipantBackButton, { label: labels.back, onBack }) : null,
2890
+ icon !== void 0 ? /* @__PURE__ */ jsx32(ParticipantConversationAvatar, { icon }) : null,
2891
+ /* @__PURE__ */ jsxs24("div", { className: "cv-p-thread__heading", children: [
2892
+ icon === void 0 && group ? /* @__PURE__ */ jsx32("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
2893
+ /* @__PURE__ */ jsx32(Title, { conversation, labels, onOpenSubject }),
2894
+ counterpartLabel ? /* @__PURE__ */ jsx32("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
2895
+ conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs24("div", { className: "cv-p-thread__meta", children: [
2896
+ conversation.protocol ? /* @__PURE__ */ jsx32(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
2897
+ /* @__PURE__ */ jsx32(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
1634
2898
  ] }) : null
1635
2899
  ] }),
1636
- actions ? /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__actions", children: actions }) : null
2900
+ actions ? /* @__PURE__ */ jsx32("div", { className: "cv-p-thread__actions", children: actions }) : null
1637
2901
  ] });
1638
2902
  }
1639
2903
 
1640
2904
  // src/participant/ParticipantThread.tsx
1641
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2905
+ import { jsx as jsx33, jsxs as jsxs25 } from "react/jsx-runtime";
1642
2906
  function Footer({ props }) {
1643
2907
  const { conversation, labels, draft } = props;
1644
2908
  if (conversation.status === "closed") {
1645
- return /* @__PURE__ */ jsx23("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
2909
+ return /* @__PURE__ */ jsx33("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
1646
2910
  }
1647
- return /* @__PURE__ */ jsx23(
2911
+ return /* @__PURE__ */ jsx33(
1648
2912
  ParticipantComposer,
1649
2913
  {
1650
2914
  value: draft.value,
@@ -1657,7 +2921,24 @@ function Footer({ props }) {
1657
2921
  isSending: props.isSending,
1658
2922
  maxLength: props.maxLength,
1659
2923
  acceptedTypes: props.acceptedTypes,
1660
- quickReplies: props.quickReplies
2924
+ quickReplies: props.quickReplies,
2925
+ audioRecording: props.audioRecording,
2926
+ onSendAudio: props.onSendAudio
2927
+ }
2928
+ );
2929
+ }
2930
+ function Header({ props }) {
2931
+ return /* @__PURE__ */ jsx33(
2932
+ ParticipantThreadHeader,
2933
+ {
2934
+ conversation: props.conversation,
2935
+ labels: props.labels,
2936
+ subjectGroups: props.subjectGroups,
2937
+ renderSubjectIcon: props.renderSubjectIcon,
2938
+ onBack: props.onBack,
2939
+ onOpenSubject: props.onOpenSubject,
2940
+ counterpartLabel: props.counterpartLabel,
2941
+ actions: props.headerActions
1661
2942
  }
1662
2943
  );
1663
2944
  }
@@ -1665,27 +2946,17 @@ function ThreadBody(props) {
1665
2946
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
1666
2947
  const subjectCard = renderSubjectCard?.(conversation);
1667
2948
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
1668
- return /* @__PURE__ */ jsxs16("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
1669
- /* @__PURE__ */ jsx23(
1670
- ParticipantThreadHeader,
1671
- {
1672
- conversation,
1673
- labels,
1674
- subjectGroups: props.subjectGroups,
1675
- renderSubjectIcon: props.renderSubjectIcon,
1676
- onBack: props.onBack,
1677
- onOpenSubject: props.onOpenSubject,
1678
- counterpartLabel: props.counterpartLabel,
1679
- actions: props.headerActions
1680
- }
1681
- ),
1682
- subjectCard ? /* @__PURE__ */ jsx23("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1683
- /* @__PURE__ */ jsxs16("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1684
- loadView.isLoading ? /* @__PURE__ */ jsx23(ParticipantLoading, { labels }) : null,
1685
- loadView.hasError ? /* @__PURE__ */ jsx23(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1686
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx23("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1687
- /* @__PURE__ */ jsx23(ParticipantTimeline, { ...props }),
1688
- /* @__PURE__ */ jsx23(
2949
+ const rootRef = useRef13(null);
2950
+ useSuppressTouchTooltips(rootRef);
2951
+ return /* @__PURE__ */ jsxs25("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
2952
+ /* @__PURE__ */ jsx33(Header, { props }),
2953
+ subjectCard ? /* @__PURE__ */ jsx33("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
2954
+ /* @__PURE__ */ jsxs25("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
2955
+ loadView.isLoading ? /* @__PURE__ */ jsx33(ParticipantLoading, { labels }) : null,
2956
+ loadView.hasError ? /* @__PURE__ */ jsx33(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
2957
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx33("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
2958
+ /* @__PURE__ */ jsx33(ParticipantTimeline, { ...props }),
2959
+ /* @__PURE__ */ jsx33(
1689
2960
  ParticipantScrollToLatest,
1690
2961
  {
1691
2962
  label: labels.scrollToLatest,
@@ -1696,16 +2967,19 @@ function ThreadBody(props) {
1696
2967
  }
1697
2968
  )
1698
2969
  ] }),
1699
- /* @__PURE__ */ jsx23(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1700
- /* @__PURE__ */ jsx23(Footer, { props })
2970
+ /* @__PURE__ */ jsx33(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
2971
+ /* @__PURE__ */ jsx33(Footer, { props })
1701
2972
  ] });
1702
2973
  }
1703
2974
  function ParticipantThread(props) {
1704
- return /* @__PURE__ */ jsx23(ParticipantCopyAnnouncer, { children: /* @__PURE__ */ jsx23(ThreadBody, { ...props }) });
2975
+ return /* @__PURE__ */ jsxs25(ParticipantCopyAnnouncer, { children: [
2976
+ /* @__PURE__ */ jsx33(ThreadBody, { ...props }),
2977
+ /* @__PURE__ */ jsx33(TooltipLayer, {})
2978
+ ] });
1705
2979
  }
1706
2980
 
1707
2981
  // src/participant/useParticipantThreadController.ts
1708
- import { useMemo as useMemo4 } from "react";
2982
+ import { useMemo as useMemo5 } from "react";
1709
2983
 
1710
2984
  // src/participant/bindAttachmentUrlResolver.ts
1711
2985
  function bindAttachmentUrlResolver(api) {
@@ -1789,7 +3063,7 @@ function appendEntryToDraft(entry, current) {
1789
3063
  }
1790
3064
 
1791
3065
  // src/participant/useParticipantConversationView.ts
1792
- import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
3066
+ import { useCallback as useCallback9, useMemo as useMemo4 } from "react";
1793
3067
 
1794
3068
  // src/participant/participantDrafts.ts
1795
3069
  var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
@@ -1833,7 +3107,7 @@ function participantSendStatesReducer(states, { subject, action }) {
1833
3107
  }
1834
3108
 
1835
3109
  // src/participant/useParticipantThread.ts
1836
- import { useCallback as useCallback4, useEffect as useEffect9, useMemo as useMemo2, useReducer as useReducer4, useRef as useRef7 } from "react";
3110
+ import { useCallback as useCallback8, useEffect as useEffect18, useMemo as useMemo3, useReducer as useReducer5, useRef as useRef17 } from "react";
1837
3111
 
1838
3112
  // src/participant/participantThreadState.ts
1839
3113
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -1869,7 +3143,7 @@ function participantThreadReducer(state, action) {
1869
3143
  }
1870
3144
 
1871
3145
  // src/participant/useParticipantMarkRead.ts
1872
- import { useEffect as useEffect7 } from "react";
3146
+ import { useEffect as useEffect16 } from "react";
1873
3147
 
1874
3148
  // src/participant/shouldMarkRead.ts
1875
3149
  function shouldMarkParticipantRead(params) {
@@ -1881,12 +3155,12 @@ function shouldMarkParticipantRead(params) {
1881
3155
  }
1882
3156
 
1883
3157
  // src/participant/useDocumentVisibility.ts
1884
- import { useEffect as useEffect6, useState as useState3 } from "react";
3158
+ import { useEffect as useEffect15, useState as useState9 } from "react";
1885
3159
  function useDocumentVisibility() {
1886
- const [visibilityState, setVisibilityState] = useState3(
3160
+ const [visibilityState, setVisibilityState] = useState9(
1887
3161
  () => typeof document === "undefined" ? "visible" : document.visibilityState
1888
3162
  );
1889
- useEffect6(() => {
3163
+ useEffect15(() => {
1890
3164
  function handleVisibilityChange() {
1891
3165
  setVisibilityState(document.visibilityState);
1892
3166
  }
@@ -1901,18 +3175,18 @@ function useDocumentVisibility() {
1901
3175
  function useParticipantMarkRead(params) {
1902
3176
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
1903
3177
  const visibilityState = useDocumentVisibility();
1904
- useEffect7(() => {
3178
+ useEffect16(() => {
1905
3179
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
1906
3180
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
1907
3181
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
1908
3182
  }
1909
3183
 
1910
3184
  // src/participant/useParticipantRevalidation.ts
1911
- import { useEffect as useEffect8, useRef as useRef4 } from "react";
3185
+ import { useEffect as useEffect17, useRef as useRef14 } from "react";
1912
3186
  function useParticipantRevalidation(api, onRevalidate) {
1913
- const handlerRef = useRef4(onRevalidate);
3187
+ const handlerRef = useRef14(onRevalidate);
1914
3188
  handlerRef.current = onRevalidate;
1915
- useEffect8(() => {
3189
+ useEffect17(() => {
1916
3190
  function handleFocus() {
1917
3191
  handlerRef.current();
1918
3192
  }
@@ -1939,12 +3213,12 @@ function useParticipantRevalidation(api, onRevalidate) {
1939
3213
  }
1940
3214
 
1941
3215
  // src/participant/useParticipantSend.ts
1942
- import { useCallback as useCallback2, useRef as useRef5 } from "react";
3216
+ import { useCallback as useCallback6, useRef as useRef15 } from "react";
1943
3217
  function useParticipantSend(params) {
1944
3218
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
1945
- const entriesRef = useRef5(params.entries);
3219
+ const entriesRef = useRef15(params.entries);
1946
3220
  entriesRef.current = params.entries;
1947
- const deliver = useCallback2(
3221
+ const deliver = useCallback6(
1948
3222
  async (entry) => {
1949
3223
  const { clientMessageId } = entry;
1950
3224
  try {
@@ -1968,7 +3242,7 @@ function useParticipantSend(params) {
1968
3242
  },
1969
3243
  [apiRef, dispatchSend, onSentMessage]
1970
3244
  );
1971
- const send = useCallback2(
3245
+ const send = useCallback6(
1972
3246
  async (content) => {
1973
3247
  const entry = createParticipantSendEntry({
1974
3248
  clientMessageId: crypto.randomUUID(),
@@ -1981,7 +3255,7 @@ function useParticipantSend(params) {
1981
3255
  },
1982
3256
  [deliver, dispatchSend, subject]
1983
3257
  );
1984
- const retry = useCallback2(
3258
+ const retry = useCallback6(
1985
3259
  async (clientMessageId) => {
1986
3260
  const entry = findFailedEntry(entriesRef.current, clientMessageId);
1987
3261
  if (!entry) return;
@@ -1994,13 +3268,13 @@ function useParticipantSend(params) {
1994
3268
  }
1995
3269
 
1996
3270
  // src/participant/useParticipantUnread.ts
1997
- import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
3271
+ import { useCallback as useCallback7, useRef as useRef16, useState as useState10 } from "react";
1998
3272
  function useParticipantUnread(params) {
1999
3273
  const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2000
- const [acknowledgedCount, setAcknowledgedCount] = useState4(0);
2001
- const messagesRef = useRef6(messages);
3274
+ const [acknowledgedCount, setAcknowledgedCount] = useState10(0);
3275
+ const messagesRef = useRef16(messages);
2002
3276
  messagesRef.current = messages;
2003
- const handleMarkedRead = useCallback3(
3277
+ const handleMarkedRead = useCallback7(
2004
3278
  (subject) => {
2005
3279
  if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective));
2006
3280
  onMarkedRead?.(subject);
@@ -2023,13 +3297,13 @@ function isForSubject(event, subject) {
2023
3297
  function useParticipantThread(params) {
2024
3298
  const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
2025
3299
  const { subjectType, subjectId } = subject;
2026
- const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2027
- const [state, dispatch] = useReducer4(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2028
- const apiRef = useRef7(api);
3300
+ const stableSubject = useMemo3(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
3301
+ const [state, dispatch] = useReducer5(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
3302
+ const apiRef = useRef17(api);
2029
3303
  apiRef.current = api;
2030
- const activeSubjectKey = useRef7("");
3304
+ const activeSubjectKey = useRef17("");
2031
3305
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
2032
- const refresh = useCallback4(async () => {
3306
+ const refresh = useCallback8(async () => {
2033
3307
  const requestKey = `${subjectType}:${subjectId}`;
2034
3308
  dispatch({ type: "started" });
2035
3309
  try {
@@ -2042,7 +3316,7 @@ function useParticipantThread(params) {
2042
3316
  }
2043
3317
  }, [stableSubject, subjectType, subjectId]);
2044
3318
  const oldestMessageId = state.messages[0]?.id;
2045
- const loadOlder = useCallback4(async () => {
3319
+ const loadOlder = useCallback8(async () => {
2046
3320
  if (!oldestMessageId) return;
2047
3321
  const requestKey = `${subjectType}:${subjectId}`;
2048
3322
  try {
@@ -2054,9 +3328,9 @@ function useParticipantThread(params) {
2054
3328
  if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
2055
3329
  }
2056
3330
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
2057
- const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
3331
+ const handleSentMessage = useCallback8((message) => dispatch({ type: "sendConfirmed", message }), []);
2058
3332
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
2059
- useEffect9(() => {
3333
+ useEffect18(() => {
2060
3334
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
2061
3335
  dispatch({ type: "reset" });
2062
3336
  void refresh();
@@ -2064,7 +3338,7 @@ function useParticipantThread(params) {
2064
3338
  useParticipantRevalidation(api, (event) => {
2065
3339
  if (isForSubject(event, stableSubject)) void refresh();
2066
3340
  });
2067
- useEffect9(() => {
3341
+ useEffect18(() => {
2068
3342
  if (sendEntries.length === 0) return;
2069
3343
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
2070
3344
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
@@ -2076,7 +3350,7 @@ function useParticipantThread(params) {
2076
3350
  messageCount: state.messages.length,
2077
3351
  onMarkedRead: unread.handleMarkedRead
2078
3352
  });
2079
- const localPending = useMemo2(() => toLocalPending(sendEntries), [sendEntries]);
3353
+ const localPending = useMemo3(() => toLocalPending(sendEntries), [sendEntries]);
2080
3354
  return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
2081
3355
  }
2082
3356
 
@@ -2085,7 +3359,7 @@ function useParticipantConversationView(params) {
2085
3359
  const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
2086
3360
  const { subjectType, subjectId } = subject;
2087
3361
  const sendEntries = selectSendEntries(sendStates, subject);
2088
- const dispatchSend = useCallback5(
3362
+ const dispatchSend = useCallback9(
2089
3363
  (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
2090
3364
  [dispatchSendStates, subjectType, subjectId]
2091
3365
  );
@@ -2100,7 +3374,7 @@ function useParticipantConversationView(params) {
2100
3374
  hostPending: pendingMessages,
2101
3375
  perspective: params.perspective
2102
3376
  });
2103
- const items = useMemo3(
3377
+ const items = useMemo4(
2104
3378
  () => mergeParticipantMessages({
2105
3379
  serverMessages: thread.messages,
2106
3380
  hostPending: pendingMessages ?? [],
@@ -2113,9 +3387,9 @@ function useParticipantConversationView(params) {
2113
3387
  }
2114
3388
 
2115
3389
  // src/participant/useParticipantDraft.ts
2116
- import { useState as useState5 } from "react";
3390
+ import { useState as useState11 } from "react";
2117
3391
  function useParticipantDraft(draftsRef, subject) {
2118
- const [draft, setDraftState] = useState5(() => getDraft(draftsRef.current, subject));
3392
+ const [draft, setDraftState] = useState11(() => getDraft(draftsRef.current, subject));
2119
3393
  function replaceDrafts(next) {
2120
3394
  draftsRef.current = next;
2121
3395
  setDraftState(getDraft(next, subject));
@@ -2134,8 +3408,29 @@ function useParticipantDraft(draftsRef, subject) {
2134
3408
  return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
2135
3409
  }
2136
3410
 
3411
+ // src/participant/useParticipantSendAudio.ts
3412
+ import { useCallback as useCallback10, useRef as useRef18 } from "react";
3413
+ function useParticipantSendAudio(send) {
3414
+ const sendRef = useRef18(send);
3415
+ sendRef.current = send;
3416
+ const inFlightRef = useRef18(/* @__PURE__ */ new Set());
3417
+ return useCallback10((file) => {
3418
+ const inFlight = inFlightRef.current;
3419
+ if (inFlight.has(file)) return;
3420
+ inFlight.add(file);
3421
+ void (async () => {
3422
+ try {
3423
+ await sendRef.current({ files: [file] });
3424
+ } catch {
3425
+ } finally {
3426
+ inFlight.delete(file);
3427
+ }
3428
+ })();
3429
+ }, []);
3430
+ }
3431
+
2137
3432
  // src/participant/useParticipantThreadScroll.ts
2138
- import { useEffect as useEffect10, useRef as useRef9 } from "react";
3433
+ import { useEffect as useEffect19, useRef as useRef20 } from "react";
2139
3434
 
2140
3435
  // src/participant/participantNewMessages.ts
2141
3436
  function collectServerMessageIds(items) {
@@ -2160,9 +3455,9 @@ function countNewIncomingMessages(previousIds, currentItems, perspective) {
2160
3455
  }
2161
3456
 
2162
3457
  // src/participant/useScrollAwayState.ts
2163
- import { useCallback as useCallback6 } from "react";
3458
+ import { useCallback as useCallback11 } from "react";
2164
3459
  function useScrollAwayState(ref, isNearBottom) {
2165
- const scrollToLatest = useCallback6(() => {
3460
+ const scrollToLatest = useCallback11(() => {
2166
3461
  const element = ref.current;
2167
3462
  if (!element) return;
2168
3463
  element.scrollTo(
@@ -2173,7 +3468,7 @@ function useScrollAwayState(ref, isNearBottom) {
2173
3468
  }
2174
3469
 
2175
3470
  // src/participant/useStickToBottom.ts
2176
- import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect2, useRef as useRef8, useState as useState6 } from "react";
3471
+ import { useCallback as useCallback12, useLayoutEffect as useLayoutEffect3, useRef as useRef19, useState as useState12 } from "react";
2177
3472
 
2178
3473
  // src/participant/participantScroll.ts
2179
3474
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -2195,17 +3490,17 @@ function resolveScrollAction(params) {
2195
3490
 
2196
3491
  // src/participant/useStickToBottom.ts
2197
3492
  function useStickToBottom({ firstKey, lastKey }) {
2198
- const ref = useRef8(null);
2199
- const snapshot = useRef8({
3493
+ const ref = useRef19(null);
3494
+ const snapshot = useRef19({
2200
3495
  hasPositioned: false,
2201
3496
  firstKey: void 0,
2202
3497
  lastKey: void 0,
2203
3498
  scrollTop: 0,
2204
3499
  scrollHeight: 0
2205
3500
  });
2206
- const [isNearBottom, setIsNearBottom] = useState6(true);
2207
- const isNearBottomRef = useRef8(true);
2208
- const onScroll = useCallback7((event) => {
3501
+ const [isNearBottom, setIsNearBottom] = useState12(true);
3502
+ const isNearBottomRef = useRef19(true);
3503
+ const onScroll = useCallback12((event) => {
2209
3504
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
2210
3505
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
2211
3506
  snapshot.current.scrollTop = scrollTop;
@@ -2213,7 +3508,7 @@ function useStickToBottom({ firstKey, lastKey }) {
2213
3508
  isNearBottomRef.current = isNear;
2214
3509
  setIsNearBottom(isNear);
2215
3510
  }, []);
2216
- useLayoutEffect2(() => {
3511
+ useLayoutEffect3(() => {
2217
3512
  const element = ref.current;
2218
3513
  const previous = snapshot.current;
2219
3514
  if (!element) return;
@@ -2260,8 +3555,8 @@ function useParticipantThreadScroll(items, perspective) {
2260
3555
  });
2261
3556
  const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2262
3557
  const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2263
- const seenIds = useRef9(/* @__PURE__ */ new Set());
2264
- useEffect10(() => {
3558
+ const seenIds = useRef20(/* @__PURE__ */ new Set());
3559
+ useEffect19(() => {
2265
3560
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
2266
3561
  }, [isNearBottom, items]);
2267
3562
  return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
@@ -2276,22 +3571,26 @@ function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending
2276
3571
  ...onRetryPending ? { retryHost: onRetryPending } : {}
2277
3572
  };
2278
3573
  }
3574
+ function buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry }) {
3575
+ return (clientMessageId) => {
3576
+ const entry = findFailedEntry(sendEntries, clientMessageId);
3577
+ if (!entry) return;
3578
+ restoreFromEntry(entry);
3579
+ dispatchSend({ type: "discarded", clientMessageId });
3580
+ };
3581
+ }
2279
3582
  function useParticipantThreadController(params) {
2280
3583
  const { api, subject, conversation, draftsRef } = params;
2281
3584
  const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject);
2282
3585
  const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params);
2283
- const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
3586
+ const resolveAttachmentUrl = useMemo5(() => bindAttachmentUrlResolver(api), [api]);
2284
3587
  const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective);
3588
+ const handleSendAudio = useParticipantSendAudio(thread.send);
3589
+ const handleEdit = buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry });
2285
3590
  function handleSend() {
2286
3591
  const content = takeForSend();
2287
3592
  if (content) void thread.send(content);
2288
3593
  }
2289
- function handleEdit(clientMessageId) {
2290
- const entry = findFailedEntry(sendEntries, clientMessageId);
2291
- if (!entry) return;
2292
- restoreFromEntry(entry);
2293
- dispatchSend({ type: "discarded", clientMessageId });
2294
- }
2295
3594
  return {
2296
3595
  conversation,
2297
3596
  items,
@@ -2309,17 +3608,18 @@ function useParticipantThreadController(params) {
2309
3608
  locale: params.locale,
2310
3609
  onLoadOlder: () => void thread.loadOlder(),
2311
3610
  pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
2312
- perspective: params.perspective
3611
+ perspective: params.perspective,
3612
+ onSendAudio: handleSendAudio
2313
3613
  };
2314
3614
  }
2315
3615
 
2316
3616
  // src/participant/ParticipantThreadScreen.tsx
2317
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3617
+ import { jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2318
3618
  function useOpenedConversation(api, subject, isNeeded) {
2319
- const [opened, setOpened] = useState7({ status: "idle" });
3619
+ const [opened, setOpened] = useState13({ status: "idle" });
2320
3620
  const { subjectType, subjectId } = subject;
2321
3621
  const canOpen = api.openConversation !== void 0;
2322
- useEffect11(() => {
3622
+ useEffect20(() => {
2323
3623
  if (!isNeeded || !canOpen) return;
2324
3624
  let isCurrent = true;
2325
3625
  setOpened({ status: "loading" });
@@ -2337,9 +3637,9 @@ function NotFound({
2337
3637
  labels,
2338
3638
  onBack
2339
3639
  }) {
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 })
3640
+ return /* @__PURE__ */ jsxs26("div", { className: "cv-p cv-p-thread", children: [
3641
+ /* @__PURE__ */ jsx34("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx34(ParticipantBackButton, { label: labels.back, onBack }) : null }),
3642
+ /* @__PURE__ */ jsx34("p", { className: "cv-p-empty", children: labels.notFound })
2343
3643
  ] });
2344
3644
  }
2345
3645
  function ParticipantThreadScreen(props) {
@@ -2349,12 +3649,12 @@ function ParticipantThreadScreen(props) {
2349
3649
  );
2350
3650
  const opened = useOpenedConversation(api, selected, listed === void 0);
2351
3651
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
2352
- if (conversation) return /* @__PURE__ */ jsx24(LoadedThread, { ...props, conversation });
3652
+ if (conversation) return /* @__PURE__ */ jsx34(LoadedThread, { ...props, conversation });
2353
3653
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
2354
3654
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
2355
3655
  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 });
3656
+ return /* @__PURE__ */ jsx34("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
3657
+ return /* @__PURE__ */ jsx34(NotFound, { labels, onBack: props.onBack });
2358
3658
  }
2359
3659
  function LoadedThread(props) {
2360
3660
  const core = useParticipantThreadController({
@@ -2362,7 +3662,7 @@ function LoadedThread(props) {
2362
3662
  subject: props.selected,
2363
3663
  onMarkedRead: props.inbox.markSubjectRead
2364
3664
  });
2365
- return /* @__PURE__ */ jsx24(
3665
+ return /* @__PURE__ */ jsx34(
2366
3666
  ParticipantThread,
2367
3667
  {
2368
3668
  ...core,
@@ -2374,7 +3674,8 @@ function LoadedThread(props) {
2374
3674
  renderSubjectIcon: props.renderSubjectIcon,
2375
3675
  avatars: props.avatars,
2376
3676
  renderAuthorAvatar: props.renderAuthorAvatar,
2377
- tail: props.tail
3677
+ tail: props.tail,
3678
+ audioRecording: props.audioRecording
2378
3679
  }
2379
3680
  );
2380
3681
  }
@@ -2500,16 +3801,16 @@ function buildParticipantThemeStyle(theme) {
2500
3801
  }
2501
3802
 
2502
3803
  // src/participant/ParticipantConversationsScreen.tsx
2503
- import { jsx as jsx25 } from "react/jsx-runtime";
3804
+ import { jsx as jsx35 } from "react/jsx-runtime";
2504
3805
  var ALL_FILTER3 = "all";
2505
3806
  function ParticipantConversationsScreen(props) {
2506
3807
  const { selected, inbox, subjectGroups, classNames } = props;
2507
- const [filter, setFilter] = useState8(ALL_FILTER3);
2508
- const [searchQuery, setSearchQuery] = useState8("");
2509
- const draftsRef = useRef10(/* @__PURE__ */ new Map());
2510
- const [sendStates, dispatchSendStates] = useReducer5(participantSendStatesReducer, /* @__PURE__ */ new Map());
2511
- const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2512
- const view = useMemo5(
3808
+ const [filter, setFilter] = useState14(ALL_FILTER3);
3809
+ const [searchQuery, setSearchQuery] = useState14("");
3810
+ const draftsRef = useRef21(/* @__PURE__ */ new Map());
3811
+ const [sendStates, dispatchSendStates] = useReducer6(participantSendStatesReducer, /* @__PURE__ */ new Map());
3812
+ const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
3813
+ const view = useMemo6(
2513
3814
  () => groupParticipantConversations({
2514
3815
  conversations: inbox.conversations,
2515
3816
  subjectGroups,
@@ -2520,7 +3821,7 @@ function ParticipantConversationsScreen(props) {
2520
3821
  );
2521
3822
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
2522
3823
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
2523
- return /* @__PURE__ */ jsx25("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
3824
+ return /* @__PURE__ */ jsx35("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx35("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(
2524
3825
  ParticipantInbox,
2525
3826
  {
2526
3827
  view,
@@ -2537,7 +3838,7 @@ function ParticipantConversationsScreen(props) {
2537
3838
  renderSubjectIcon: props.renderSubjectIcon,
2538
3839
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
2539
3840
  }
2540
- ) }) : /* @__PURE__ */ jsx25("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx25(
3841
+ ) }) : /* @__PURE__ */ jsx35("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(
2541
3842
  ParticipantThreadScreen,
2542
3843
  {
2543
3844
  ...props,
@@ -2552,7 +3853,7 @@ function ParticipantConversationsScreen(props) {
2552
3853
  }
2553
3854
 
2554
3855
  // src/participant/useParticipantInbox.ts
2555
- import { useCallback as useCallback8, useEffect as useEffect12, useReducer as useReducer6 } from "react";
3856
+ import { useCallback as useCallback13, useEffect as useEffect21, useReducer as useReducer7 } from "react";
2556
3857
 
2557
3858
  // src/participant/participantInboxState.ts
2558
3859
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -2602,8 +3903,8 @@ function describeError2(error) {
2602
3903
  return error instanceof Error ? error.message : "unknown error";
2603
3904
  }
2604
3905
  function useParticipantInbox(api) {
2605
- const [state, dispatch] = useReducer6(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2606
- const refresh = useCallback8(async () => {
3906
+ const [state, dispatch] = useReducer7(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
3907
+ const refresh = useCallback13(async () => {
2607
3908
  dispatch({ type: "started" });
2608
3909
  try {
2609
3910
  const page = await api.listConversations();
@@ -2613,7 +3914,7 @@ function useParticipantInbox(api) {
2613
3914
  }
2614
3915
  }, [api]);
2615
3916
  const { nextCursor } = state;
2616
- const loadMore = useCallback8(async () => {
3917
+ const loadMore = useCallback13(async () => {
2617
3918
  if (!nextCursor) return;
2618
3919
  try {
2619
3920
  const page = await api.listConversations({ cursor: nextCursor });
@@ -2622,10 +3923,10 @@ function useParticipantInbox(api) {
2622
3923
  dispatch({ type: "failed", error: describeError2(error) });
2623
3924
  }
2624
3925
  }, [api, nextCursor]);
2625
- const markSubjectRead = useCallback8((subject) => {
3926
+ const markSubjectRead = useCallback13((subject) => {
2626
3927
  dispatch({ type: "markedRead", subject });
2627
3928
  }, []);
2628
- useEffect12(() => {
3929
+ useEffect21(() => {
2629
3930
  void refresh();
2630
3931
  }, [refresh]);
2631
3932
  useParticipantRevalidation(api, () => {
@@ -2635,11 +3936,11 @@ function useParticipantInbox(api) {
2635
3936
  }
2636
3937
 
2637
3938
  // src/participant/ParticipantConversations.tsx
2638
- import { jsx as jsx26 } from "react/jsx-runtime";
3939
+ import { jsx as jsx36 } from "react/jsx-runtime";
2639
3940
  function ParticipantConversations(props) {
2640
3941
  const inbox = useParticipantInbox(props.api);
2641
3942
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
2642
- return /* @__PURE__ */ jsx26(
3943
+ return /* @__PURE__ */ jsx36(
2643
3944
  ParticipantConversationsScreen,
2644
3945
  {
2645
3946
  ...props,
@@ -2649,7 +3950,7 @@ function ParticipantConversations(props) {
2649
3950
  }
2650
3951
 
2651
3952
  // src/participant/ConversationThread.tsx
2652
- import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer7, useRef as useRef11 } from "react";
3953
+ import { useCallback as useCallback14, useMemo as useMemo7, useReducer as useReducer8, useRef as useRef22 } from "react";
2653
3954
 
2654
3955
  // src/participant/conversationThreadApi.ts
2655
3956
  function toParticipantApi(api) {
@@ -2689,20 +3990,20 @@ function buildThreadSummary({
2689
3990
  }
2690
3991
 
2691
3992
  // src/participant/ConversationThread.tsx
2692
- import { jsx as jsx27 } from "react/jsx-runtime";
3993
+ import { jsx as jsx37 } from "react/jsx-runtime";
2693
3994
  function useConversationThreadCore(props) {
2694
3995
  const { perspective = "participant" } = props;
2695
3996
  const { subjectType, subjectId } = props.subject;
2696
- const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2697
- const onMarkedReadRef = useRef11(props.onMarkedRead);
3997
+ const subject = useMemo7(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
3998
+ const onMarkedReadRef = useRef22(props.onMarkedRead);
2698
3999
  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]);
4000
+ const handleMarkedRead = useCallback14((marked) => onMarkedReadRef.current?.(marked), []);
4001
+ const api = useMemo7(() => toParticipantApi(props.api), [props.api]);
4002
+ const draftsRef = useRef22(/* @__PURE__ */ new Map());
4003
+ const [sendStates, dispatchSendStates] = useReducer8(participantSendStatesReducer, /* @__PURE__ */ new Map());
4004
+ const labels = useMemo7(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2704
4005
  const { title, protocol, channels, status } = props;
2705
- const conversation = useMemo6(
4006
+ const conversation = useMemo7(
2706
4007
  () => buildThreadSummary({ subject, title, protocol, channels, status }),
2707
4008
  [subject, title, protocol, channels, status]
2708
4009
  );
@@ -2724,12 +4025,12 @@ function useConversationThreadCore(props) {
2724
4025
  }
2725
4026
  function ConversationThreadBody(props) {
2726
4027
  const core = useConversationThreadCore(props);
2727
- return /* @__PURE__ */ jsx27(
4028
+ return /* @__PURE__ */ jsx37(
2728
4029
  "div",
2729
4030
  {
2730
4031
  className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2731
4032
  style: buildParticipantThemeStyle(props.theme),
2732
- children: /* @__PURE__ */ jsx27("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx27(
4033
+ children: /* @__PURE__ */ jsx37("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx37(
2733
4034
  ParticipantThread,
2734
4035
  {
2735
4036
  ...core,
@@ -2741,17 +4042,20 @@ function ConversationThreadBody(props) {
2741
4042
  renderAuthorAvatar: props.renderAuthorAvatar,
2742
4043
  tail: props.tail,
2743
4044
  counterpartLabel: props.counterpartLabel,
2744
- headerActions: props.headerActions
4045
+ headerActions: props.headerActions,
4046
+ audioRecording: props.audioRecording
2745
4047
  }
2746
4048
  ) })
2747
4049
  }
2748
4050
  );
2749
4051
  }
2750
4052
  function ConversationThread(props) {
2751
- return /* @__PURE__ */ jsx27(ConversationThreadBody, { ...props }, draftKey(props.subject));
4053
+ return /* @__PURE__ */ jsx37(ConversationThreadBody, { ...props }, draftKey(props.subject));
2752
4054
  }
2753
4055
  export {
2754
4056
  ConversationThread,
4057
+ DEFAULT_MAX_RECORDING_BYTES,
4058
+ DEFAULT_PARTICIPANT_AUDIO_LABELS,
2755
4059
  DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
2756
4060
  ParticipantConversations,
2757
4061
  groupParticipantConversations