@adatechnology/conversations-ui 0.9.0 → 0.11.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 (91) hide show
  1. package/README.md +107 -0
  2. package/dist/{ConversationSimulatorPanel-q-wVgBCN.d.ts → ConversationSimulatorPanel-P6eqTSAO.d.ts} +1 -16
  3. package/dist/{chunk-O6457XHK.js → chunk-WCZNIJ3M.js} +16 -1
  4. package/dist/{chunk-L3OFAQPS.js → chunk-Y326LLEP.js} +4 -14
  5. package/dist/index.d.ts +10 -3
  6. package/dist/index.js +3 -3
  7. package/dist/participant/index.d.ts +55 -3
  8. package/dist/participant/index.js +1369 -319
  9. package/dist/preview/index.d.ts +2 -2
  10. package/dist/preview/index.js +2 -2
  11. package/dist/styles.css +187 -0
  12. package/package.json +2 -2
  13. package/src/AudioRecorderButton.tsx +4 -28
  14. package/src/audioRecorderFormat.ts +27 -0
  15. package/src/participant/ConversationThread.tsx +7 -0
  16. package/src/participant/ParticipantAttachmentItem.tsx +7 -10
  17. package/src/participant/ParticipantAudioAttachment.tsx +34 -0
  18. package/src/participant/ParticipantAudioPlayer.tsx +68 -0
  19. package/src/participant/ParticipantAudioPlayerControls.tsx +68 -0
  20. package/src/participant/ParticipantAudioRecorderView.tsx +55 -0
  21. package/src/participant/ParticipantAudioTranscription.tsx +90 -0
  22. package/src/participant/ParticipantComposer.tsx +50 -15
  23. package/src/participant/ParticipantComposerAudio.tsx +95 -0
  24. package/src/participant/ParticipantConversations.tsx +5 -0
  25. package/src/participant/ParticipantRecorderParts.tsx +118 -0
  26. package/src/participant/ParticipantThread.test.tsx +46 -0
  27. package/src/participant/ParticipantThread.tsx +17 -0
  28. package/src/participant/ParticipantThreadScreen.tsx +2 -0
  29. package/src/participant/index.ts +3 -0
  30. package/src/participant/participantAudio.types.ts +10 -0
  31. package/src/participant/participantAudioCapture.test.ts +15 -0
  32. package/src/participant/participantAudioCapture.ts +85 -0
  33. package/src/participant/participantAudioCoordinator.ts +13 -0
  34. package/src/participant/participantAudioFormat.test.ts +93 -0
  35. package/src/participant/participantAudioFormat.ts +72 -0
  36. package/src/participant/participantAudioGolden.test.tsx +168 -0
  37. package/src/participant/participantAudioLabels.test.ts +22 -0
  38. package/src/participant/participantAudioLabels.ts +76 -0
  39. package/src/participant/participantAudioPlayerControlsDom.test.tsx +123 -0
  40. package/src/participant/participantAudioPlayerCss.test.ts +63 -0
  41. package/src/participant/participantAudioPlayerDom.test.tsx +154 -0
  42. package/src/participant/participantAudioPlayerDomKit.test-helper.tsx +84 -0
  43. package/src/participant/participantAudioPlayerState.ts +35 -0
  44. package/src/participant/participantAudioRecorderDom.test.tsx +127 -0
  45. package/src/participant/participantAudioRecorderInterruptionDom.test.tsx +135 -0
  46. package/src/participant/participantAudioRecorderSpecDom.test.tsx +110 -0
  47. package/src/participant/participantAudioRecorderState.test.ts +144 -0
  48. package/src/participant/participantAudioRecorderState.ts +93 -0
  49. package/src/participant/participantAudioRecorderView.test.tsx +98 -0
  50. package/src/participant/participantAudioSupport.ts +36 -0
  51. package/src/participant/participantAudioTranscriptionDom.test.tsx +198 -0
  52. package/src/participant/participantComposerAudioDom.test.tsx +185 -0
  53. package/src/participant/participantComposerAudioGolden.test.tsx +68 -0
  54. package/src/participant/participantComposerAudioSsr.test.tsx +74 -0
  55. package/src/participant/participantFileValidation.ts +1 -1
  56. package/src/participant/participantGolden.test.tsx +2 -0
  57. package/src/participant/participantGoldenScenarios.test-helper.tsx +14 -1
  58. package/src/participant/participantLabels.ts +4 -1
  59. package/src/participant/participantMediaFakes.test-helper.ts +82 -0
  60. package/src/participant/participantNoMicrophoneDom.test.tsx +44 -0
  61. package/src/participant/participantPublicTypes.test.ts +11 -0
  62. package/src/participant/participantRecorderDomKit.test-helper.tsx +131 -0
  63. package/src/participant/participantRecorderFakes.test-helper.ts +47 -0
  64. package/src/participant/participantSpecRecorderFake.test-helper.ts +52 -0
  65. package/src/participant/participantThemeContrast.test.ts +9 -0
  66. package/src/participant/test-fixtures/composer-audio-denied.golden.html +1 -0
  67. package/src/participant/test-fixtures/composer-audio-idle.golden.html +1 -0
  68. package/src/participant/test-fixtures/composer-audio-recording.golden.html +1 -0
  69. package/src/participant/test-fixtures/composer-audio-review.golden.html +1 -0
  70. package/src/participant/test-fixtures/thread-audio-native.golden.html +1 -0
  71. package/src/participant/test-fixtures/thread-audio-player.golden.html +1 -0
  72. package/src/participant/test-fixtures/thread-audio-transcription-empty.golden.html +1 -0
  73. package/src/participant/test-fixtures/thread-audio-transcription-pending.golden.html +1 -0
  74. package/src/participant/test-fixtures/thread-audio-transcription-ready.golden.html +1 -0
  75. package/src/participant/test-fixtures/thread-audio-transcription-too_long.golden.html +1 -0
  76. package/src/participant/test-fixtures/thread-composer-microphone-idle.golden.html +1 -0
  77. package/src/participant/test-fixtures/thread-empty-draft.golden.html +1 -0
  78. package/src/participant/useParticipantAudioMediaEvents.ts +46 -0
  79. package/src/participant/useParticipantAudioPlayer.ts +52 -0
  80. package/src/participant/useParticipantAudioPlayerActions.ts +47 -0
  81. package/src/participant/useParticipantAudioRecorder.ts +153 -0
  82. package/src/participant/useParticipantAudioRecorderCore.ts +55 -0
  83. package/src/participant/useParticipantAudioRecorderEffects.ts +70 -0
  84. package/src/participant/useParticipantAudioSourceRetry.ts +42 -0
  85. package/src/participant/useParticipantSendAudio.test.tsx +85 -0
  86. package/src/participant/useParticipantSendAudio.ts +28 -0
  87. package/src/participant/useParticipantThreadController.ts +20 -7
  88. package/src/participant/useStableAttachmentUrl.ts +71 -0
  89. package/src/participant/useTranscriptionOverflow.ts +25 -0
  90. package/src/participant/useTranscriptionReadyAnnouncement.ts +24 -0
  91. package/src/styles.css +30 -0
@@ -2,15 +2,17 @@ import {
2
2
  TooltipLayer
3
3
  } from "../chunk-NSOAUYNE.js";
4
4
  import {
5
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
5
6
  StatusTicks,
6
7
  channelCapabilityFor,
7
8
  formatFileSize,
8
9
  formatTimestamp,
9
- isSameDay
10
- } from "../chunk-O6457XHK.js";
10
+ isSameDay,
11
+ resolveRecordingFormat
12
+ } from "../chunk-WCZNIJ3M.js";
11
13
 
12
14
  // src/participant/ParticipantConversationsScreen.tsx
13
- import { useMemo as useMemo5, useReducer as useReducer6, useRef as useRef12, useState as useState9 } from "react";
15
+ import { useMemo as useMemo6, useReducer as useReducer6, useRef as useRef21, useState as useState14 } from "react";
14
16
 
15
17
  // src/participant/participantLoadView.ts
16
18
  function resolveLoadView({ status, hasItems }) {
@@ -21,6 +23,45 @@ function resolveLoadView({ status, hasItems }) {
21
23
  };
22
24
  }
23
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
+
24
65
  // src/participant/participantLabels.ts
25
66
  var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
26
67
  var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
@@ -80,7 +121,8 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
80
121
  channelWebchat: "Web chat",
81
122
  externalLinkHint: "opens in a new tab",
82
123
  copyValue: "Copy",
83
- valueCopied: "Copied"
124
+ valueCopied: "Copied",
125
+ ...DEFAULT_PARTICIPANT_AUDIO_LABELS
84
126
  };
85
127
  function formatParticipantLabel(template, count) {
86
128
  return template.replace("{count}", String(count));
@@ -382,7 +424,7 @@ function ParticipantInbox(props) {
382
424
  }
383
425
 
384
426
  // src/participant/ParticipantThreadScreen.tsx
385
- import { useEffect as useEffect13, useState as useState8 } from "react";
427
+ import { useEffect as useEffect20, useState as useState13 } from "react";
386
428
 
387
429
  // src/participant/ParticipantBackButton.tsx
388
430
  import { ArrowLeft } from "lucide-react";
@@ -392,14 +434,818 @@ function ParticipantBackButton({ label, onBack }) {
392
434
  }
393
435
 
394
436
  // src/participant/ParticipantThread.tsx
395
- import { useRef as useRef5 } from "react";
437
+ import { useRef as useRef13 } from "react";
396
438
 
397
439
  // src/participant/ParticipantComposer.tsx
398
- 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
+ }
399
1245
 
400
1246
  // src/participant/ParticipantComposerControls.tsx
401
- import { useLayoutEffect, useRef as useRef2 } from "react";
402
- import { Paperclip, Send } from "lucide-react";
1247
+ import { useLayoutEffect, useRef as useRef7 } from "react";
1248
+ import { Paperclip, Send as Send2 } from "lucide-react";
403
1249
 
404
1250
  // src/participant/participantAutoGrow.ts
405
1251
  function fitTextareaHeight(target) {
@@ -408,11 +1254,11 @@ function fitTextareaHeight(target) {
408
1254
  }
409
1255
 
410
1256
  // src/participant/ParticipantComposerControls.tsx
411
- 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";
412
1258
  function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
413
- const fileInputRef = useRef2(null);
414
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
415
- /* @__PURE__ */ jsx7(
1259
+ const fileInputRef = useRef7(null);
1260
+ return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1261
+ /* @__PURE__ */ jsx12(
416
1262
  "input",
417
1263
  {
418
1264
  ref: fileInputRef,
@@ -424,7 +1270,7 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
424
1270
  onChange: onChosen
425
1271
  }
426
1272
  ),
427
- /* @__PURE__ */ jsx7(
1273
+ /* @__PURE__ */ jsx12(
428
1274
  "button",
429
1275
  {
430
1276
  type: "button",
@@ -433,13 +1279,13 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
433
1279
  "data-cv-tooltip": labels.attach,
434
1280
  disabled,
435
1281
  onClick: () => fileInputRef.current?.click(),
436
- children: /* @__PURE__ */ jsx7(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
1282
+ children: /* @__PURE__ */ jsx12(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
437
1283
  }
438
1284
  )
439
1285
  ] });
440
1286
  }
441
1287
  function SendButton({ label, disabled }) {
442
- return /* @__PURE__ */ jsx7(
1288
+ return /* @__PURE__ */ jsx12(
443
1289
  "button",
444
1290
  {
445
1291
  type: "submit",
@@ -447,16 +1293,16 @@ function SendButton({ label, disabled }) {
447
1293
  "aria-label": label,
448
1294
  "data-cv-tooltip": label,
449
1295
  disabled,
450
- children: /* @__PURE__ */ jsx7(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
1296
+ children: /* @__PURE__ */ jsx12(Send2, { size: 20, "aria-hidden": "true", focusable: "false" })
451
1297
  }
452
1298
  );
453
1299
  }
454
1300
  function MessageField({ value, label, placeholder, disabled, maxLength, onChange }) {
455
- const fieldRef = useRef2(null);
1301
+ const fieldRef = useRef7(null);
456
1302
  useLayoutEffect(() => {
457
1303
  if (fieldRef.current) fitTextareaHeight(fieldRef.current);
458
1304
  }, [value]);
459
- return /* @__PURE__ */ jsx7(
1305
+ return /* @__PURE__ */ jsx12(
460
1306
  "textarea",
461
1307
  {
462
1308
  ref: fieldRef,
@@ -472,29 +1318,20 @@ function MessageField({ value, label, placeholder, disabled, maxLength, onChange
472
1318
  );
473
1319
  }
474
1320
 
475
- // src/participant/participantFileValidation.ts
476
- function matchesAcceptedType(mimeType, acceptedTypes) {
477
- return acceptedTypes.some(
478
- (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
479
- );
480
- }
481
- function rejectionOf(file, params) {
482
- if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
483
- if (file.size > params.maxBytes) return "tooLarge";
484
- return void 0;
485
- }
486
- function reduceFilesSelection(params) {
487
- const files = [...params.current];
488
- const rejected = [];
489
- for (const file of params.incoming) {
490
- const reason = rejectionOf(file, params);
491
- if (reason === void 0) files.push(file);
492
- else rejected.push({ file, reason });
493
- }
494
- return { files, rejected };
1321
+ // src/participant/participantAudioSupport.ts
1322
+ function resolveBrowserRecordingFormat() {
1323
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) return void 0;
1324
+ return resolveRecordingFormat();
495
1325
  }
496
- function maxBytesForChannel(channel = "app") {
497
- 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 };
498
1335
  }
499
1336
 
500
1337
  // src/participant/participantQuickReply.ts
@@ -505,18 +1342,18 @@ function applyQuickReply(currentText, replyText) {
505
1342
  }
506
1343
 
507
1344
  // src/participant/ParticipantComposer.tsx
508
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1345
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
509
1346
  function QuickReplyStrip({ quickReplies, labels, onPick }) {
510
- 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)) });
511
1348
  }
512
1349
  function FileList({ files, labels, onRemove }) {
513
1350
  if (files.length === 0) return null;
514
- return /* @__PURE__ */ jsx8("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs7("li", { className: "cv-p-files__item", children: [
515
- /* @__PURE__ */ jsx8("span", { className: "cv-p-files__name", children: file.name }),
516
- /* @__PURE__ */ jsx8("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
517
- /* @__PURE__ */ jsxs7("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, "data-cv-tooltip": labels.removeAttachment, onClick: () => onRemove(file), children: [
518
- /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\xD7" }),
519
- /* @__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 })
520
1357
  ] })
521
1358
  ] }, `${file.name}-${file.size}-${index}`)) });
522
1359
  }
@@ -524,7 +1361,7 @@ function rejectionText(rejection, labels) {
524
1361
  return rejection === "tooLarge" ? labels.attachmentTooLarge : labels.attachmentTypeNotAccepted;
525
1362
  }
526
1363
  function useFileSelection(props) {
527
- const [rejection, setRejection] = useState(void 0);
1364
+ const [rejection, setRejection] = useState3(void 0);
528
1365
  const { files, onFilesChange, channel } = props;
529
1366
  const maxBytes = maxBytesForChannel(channel);
530
1367
  function handleFilesChosen(event) {
@@ -540,40 +1377,55 @@ function useFileSelection(props) {
540
1377
  }
541
1378
  return { rejection, handleFilesChosen, handleRemoveFile };
542
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
+ }
543
1409
  function ParticipantComposer(props) {
544
- 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;
545
1411
  const { rejection, handleFilesChosen, handleRemoveFile } = useFileSelection(props);
546
1412
  const canSend = !disabled && !isSending && (value.trim().length > 0 || files.length > 0);
547
1413
  function handleSubmit(event) {
548
1414
  event.preventDefault();
549
1415
  if (canSend) onSend();
550
1416
  }
551
- return /* @__PURE__ */ jsxs7("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
552
- props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx8(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
553
- /* @__PURE__ */ jsx8(FileList, { files, labels, onRemove: handleRemoveFile }),
554
- rejection ? /* @__PURE__ */ jsx8("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
555
- /* @__PURE__ */ jsxs7("div", { className: "cv-p-composer__row", children: [
556
- channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx8(AttachControl, { labels, disabled, acceptedTypes: props.acceptedTypes, onChosen: handleFilesChosen }) : null,
557
- /* @__PURE__ */ jsx8(
558
- MessageField,
559
- {
560
- value,
561
- label: labels.messageInputLabel,
562
- placeholder: labels.messageInputPlaceholder,
563
- disabled,
564
- maxLength: props.maxLength,
565
- onChange
566
- }
567
- ),
568
- /* @__PURE__ */ jsx8(SendButton, { label: labels.send, disabled: !canSend })
569
- ] })
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 })
570
1422
  ] });
571
1423
  }
572
1424
 
573
1425
  // src/participant/useSuppressTouchTooltips.ts
574
- import { useEffect as useEffect2 } from "react";
1426
+ import { useEffect as useEffect6 } from "react";
575
1427
  function useSuppressTouchTooltips(rootRef) {
576
- useEffect2(() => {
1428
+ useEffect6(() => {
577
1429
  const root = rootRef.current;
578
1430
  if (!root) return void 0;
579
1431
  let isTouching = false;
@@ -603,7 +1455,7 @@ function useSuppressTouchTooltips(rootRef) {
603
1455
  }
604
1456
 
605
1457
  // src/participant/ParticipantCopyAnnouncer.tsx
606
- import { createContext, useCallback, useContext, useEffect as useEffect3, useReducer, useRef as useRef3 } from "react";
1458
+ import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect7, useReducer, useRef as useRef8 } from "react";
607
1459
 
608
1460
  // src/participant/participantCopyAnnouncement.ts
609
1461
  var INITIAL_COPY_ANNOUNCEMENT = { text: "", tick: 0 };
@@ -623,7 +1475,7 @@ function resolveLiveText(params) {
623
1475
  }
624
1476
 
625
1477
  // src/participant/ParticipantCopyAnnouncer.tsx
626
- import { jsx as jsx9 } from "react/jsx-runtime";
1478
+ import { jsx as jsx14 } from "react/jsx-runtime";
627
1479
  var ANNOUNCEMENT_MS = 3e3;
628
1480
  var AnnounceContext = createContext(() => void 0);
629
1481
  var AnnouncementContext = createContext(INITIAL_COPY_ANNOUNCEMENT);
@@ -632,20 +1484,20 @@ function useAnnounceCopy() {
632
1484
  }
633
1485
  function ParticipantCopyAnnouncer({ children }) {
634
1486
  const [announcement, dispatch] = useReducer(copyAnnouncementReducer, INITIAL_COPY_ANNOUNCEMENT);
635
- const announce = useCallback((text) => dispatch({ type: "announce", text }), []);
636
- useEffect3(() => {
1487
+ const announce = useCallback4((text) => dispatch({ type: "announce", text }), []);
1488
+ useEffect7(() => {
637
1489
  if (announcement.text === "") return void 0;
638
1490
  const timer = setTimeout(() => dispatch({ type: "expired" }), ANNOUNCEMENT_MS);
639
1491
  return () => clearTimeout(timer);
640
1492
  }, [announcement.tick, announcement.text]);
641
- 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 }) });
642
1494
  }
643
1495
  function ParticipantThreadLive({ notice }) {
644
1496
  const copy = useContext(AnnouncementContext);
645
- const baselineNotice = useRef3(void 0);
1497
+ const baselineNotice = useRef8(void 0);
646
1498
  const resolution = resolveLiveText({ copy, notice, baselineNotice: baselineNotice.current });
647
1499
  baselineNotice.current = resolution.baselineNotice;
648
- 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 });
649
1501
  }
650
1502
 
651
1503
  // src/participant/ParticipantScrollToLatest.tsx
@@ -658,7 +1510,7 @@ function focusScrollerOf(button) {
658
1510
  }
659
1511
 
660
1512
  // src/participant/ParticipantScrollToLatest.tsx
661
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1513
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
662
1514
  function ParticipantScrollToLatest({
663
1515
  label,
664
1516
  isAwayFromBottom,
@@ -668,7 +1520,7 @@ function ParticipantScrollToLatest({
668
1520
  }) {
669
1521
  const { isVisible, badge } = resolveScrollToLatestView({ isAwayFromBottom, newMessagesCount, hasMessages });
670
1522
  if (!isVisible || onClick === void 0) return null;
671
- 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(
672
1524
  "button",
673
1525
  {
674
1526
  type: "button",
@@ -680,8 +1532,8 @@ function ParticipantScrollToLatest({
680
1532
  onClick();
681
1533
  },
682
1534
  children: [
683
- /* @__PURE__ */ jsx10(ChevronDown, { size: 24, "aria-hidden": "true", focusable: "false" }),
684
- 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
685
1537
  ]
686
1538
  }
687
1539
  ) });
@@ -727,13 +1579,13 @@ function shouldShowAvatar({ previousAuthor, author, isMine }) {
727
1579
  }
728
1580
 
729
1581
  // src/participant/ParticipantAuthorAvatar.tsx
730
- import { jsx as jsx11 } from "react/jsx-runtime";
1582
+ import { jsx as jsx16 } from "react/jsx-runtime";
731
1583
  function ParticipantAuthorAvatar({ name, render }) {
732
1584
  const fromHost = render?.({ name });
733
1585
  const hasHostContent = !(fromHost == null || fromHost === false || fromHost === "");
734
1586
  const initials = authorInitials(name);
735
- const fallback = initials ?? /* @__PURE__ */ jsx11(User, { size: 16, "aria-hidden": true });
736
- return /* @__PURE__ */ jsx11("span", { className: "cv-p-avatar", "aria-hidden": "true", children: hasHostContent ? fromHost : fallback });
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 });
737
1589
  }
738
1590
 
739
1591
  // src/participant/participantDay.ts
@@ -745,17 +1597,112 @@ function toDateTimeAttribute(date) {
745
1597
  }
746
1598
 
747
1599
  // src/participant/ParticipantDayDivider.tsx
748
- import { jsx as jsx12 } from "react/jsx-runtime";
1600
+ import { jsx as jsx17 } from "react/jsx-runtime";
749
1601
  function ParticipantDayDivider({ day, locale }) {
750
1602
  const label = day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" });
751
- return /* @__PURE__ */ jsx12("div", { className: "cv-p-day", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx12("time", { className: "cv-p-day__label", dateTime: toDateTimeAttribute(day), children: label }) });
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 }) });
752
1604
  }
753
1605
 
754
1606
  // src/participant/ParticipantMessageBubble.tsx
755
1607
  import { useId as useId3 } from "react";
756
1608
 
757
1609
  // src/participant/ParticipantAttachmentItem.tsx
758
- import { useEffect as useEffect4, useState as useState2 } from "react";
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";
759
1706
 
760
1707
  // src/participant/participantAttachmentUrl.ts
761
1708
  var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
@@ -767,12 +1714,79 @@ function isSafeAttachmentUrl(url) {
767
1714
  }
768
1715
  }
769
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
+ }
1727
+ }
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 };
1759
+ }
1760
+
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
+ ] });
1782
+ }
1783
+
770
1784
  // src/participant/ParticipantAttachmentItem.tsx
771
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1785
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
772
1786
  function useAttachmentUrl(attachment, resolveAttachmentUrl) {
773
- const [url, setUrl] = useState2(void 0);
1787
+ const [url, setUrl] = useState7(void 0);
774
1788
  const disposition = attachment.kind === "document" ? "attachment" : "inline";
775
- useEffect4(() => {
1789
+ useEffect11(() => {
776
1790
  let isCurrent = true;
777
1791
  resolveAttachmentUrl(attachment, disposition).then(
778
1792
  (resolved) => {
@@ -788,21 +1802,19 @@ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
788
1802
  }, [attachment, disposition, resolveAttachmentUrl]);
789
1803
  return url;
790
1804
  }
791
- function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
1805
+ function ParticipantAttachmentItem(props) {
1806
+ if (props.attachment.kind === "audio") return /* @__PURE__ */ jsx20(ParticipantAudioAttachment, { ...props });
1807
+ return /* @__PURE__ */ jsx20(ParticipantFileAttachment, { ...props });
1808
+ }
1809
+ function ParticipantFileAttachment({ attachment, resolveAttachmentUrl, labels }) {
792
1810
  const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
793
1811
  if (attachment.kind === "image") {
794
- return /* @__PURE__ */ jsxs9("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
795
- url ? /* @__PURE__ */ jsx13("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
796
- /* @__PURE__ */ jsx13("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
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 })
797
1815
  ] });
798
1816
  }
799
- if (attachment.kind === "audio") {
800
- return /* @__PURE__ */ jsxs9("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
801
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
802
- /* @__PURE__ */ jsx13("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
803
- ] });
804
- }
805
- return /* @__PURE__ */ jsxs9(
1817
+ return /* @__PURE__ */ jsxs15(
806
1818
  "a",
807
1819
  {
808
1820
  className: "cv-p-attachment cv-p-attachment--document",
@@ -810,15 +1822,15 @@ function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels })
810
1822
  download: attachment.filename,
811
1823
  "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
812
1824
  children: [
813
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
814
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
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) })
815
1827
  ]
816
1828
  }
817
1829
  );
818
1830
  }
819
1831
 
820
1832
  // src/participant/ParticipantBubbleStatus.tsx
821
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1833
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
822
1834
  function statusText(status, labels) {
823
1835
  const byStatus = {
824
1836
  sending: labels.statusSending,
@@ -835,13 +1847,13 @@ function OwnStatus({ status, labels, onRetry }) {
835
1847
  const isVisibleText = status === "sending" || status === "queued" || status === "failed";
836
1848
  const text = status === "failed" ? labels.statusFailedShort : statusText(status, labels);
837
1849
  const isTextVisible = isVisibleText && !(status === "failed" && onRetry);
838
- return /* @__PURE__ */ jsxs10("span", { className: "cv-p-bubble__status", children: [
839
- /* @__PURE__ */ jsx14(StatusTicks, { status: ticksStatus, appearance: "stylesheet", title: isTextVisible ? void 0 : text }),
840
- /* @__PURE__ */ jsx14("span", { className: isTextVisible ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
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 })
841
1853
  ] });
842
1854
  }
843
1855
  function RetryButton({ labels, onRetry, describedBy }) {
844
- return /* @__PURE__ */ jsx14(
1856
+ return /* @__PURE__ */ jsx21(
845
1857
  "button",
846
1858
  {
847
1859
  type: "button",
@@ -850,7 +1862,7 @@ function RetryButton({ labels, onRetry, describedBy }) {
850
1862
  "aria-describedby": describedBy,
851
1863
  "data-cv-tooltip": labels.statusFailed,
852
1864
  onClick: onRetry,
853
- children: /* @__PURE__ */ jsx14("span", { className: "cv-p-failed-retry__icon", children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx14("path", { d: "M13 8a5 5 0 1 1-1.6-3.7M13 2.5v3h-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) }) })
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" }) }) })
854
1866
  }
855
1867
  );
856
1868
  }
@@ -859,7 +1871,7 @@ function RetryButton({ labels, onRetry, describedBy }) {
859
1871
  import { useId as useId2 } from "react";
860
1872
 
861
1873
  // src/participant/ParticipantFailedMenuList.tsx
862
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1874
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
863
1875
  function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard }) {
864
1876
  function runAndClose(action) {
865
1877
  return () => {
@@ -868,9 +1880,9 @@ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard
868
1880
  };
869
1881
  }
870
1882
  const className = `cv-p-failed-menu__list${menu.placement === "below" ? " cv-p-failed-menu__list--below" : ""}`;
871
- return /* @__PURE__ */ jsxs11("div", { role: "menu", "aria-labelledby": labelledBy, ref: menu.menuRef, className, onKeyDown: menu.handleMenuKeyDown, children: [
872
- onEdit ? /* @__PURE__ */ jsx15("button", { type: "button", role: "menuitem", tabIndex: -1, className: "cv-p-failed-menu__item", onClick: runAndClose(onEdit), children: labels.edit }) : null,
873
- onDiscard ? /* @__PURE__ */ jsx15(
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(
874
1886
  "button",
875
1887
  {
876
1888
  type: "button",
@@ -885,7 +1897,7 @@ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard
885
1897
  }
886
1898
 
887
1899
  // src/participant/useParticipantFailedMenu.ts
888
- import { useReducer as useReducer2, useRef as useRef4 } from "react";
1900
+ import { useReducer as useReducer2, useRef as useRef12 } from "react";
889
1901
 
890
1902
  // src/participant/participantFailedMenuState.ts
891
1903
  var CLOSED_FAILED_MENU = { isOpen: false, initialFocus: "first" };
@@ -933,10 +1945,10 @@ function handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }) {
933
1945
  }
934
1946
 
935
1947
  // src/participant/useFailedMenuEffects.ts
936
- import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect2, useState as useState3 } from "react";
1948
+ import { useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useState as useState8 } from "react";
937
1949
  var SCROLLER_SELECTOR2 = ".cv-p-thread__scroll";
938
1950
  function useFailedMenuInitialFocus(state, menuRef) {
939
- useEffect5(() => {
1951
+ useEffect12(() => {
940
1952
  if (!state.isOpen) return;
941
1953
  const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
942
1954
  const target = state.initialFocus === "last" ? items[items.length - 1] : items[0];
@@ -944,7 +1956,7 @@ function useFailedMenuInitialFocus(state, menuRef) {
944
1956
  }, [state, menuRef]);
945
1957
  }
946
1958
  function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
947
- useEffect5(() => {
1959
+ useEffect12(() => {
948
1960
  if (!isOpen) return;
949
1961
  function handlePointerDown(event) {
950
1962
  if (event.target instanceof Node && containerRef.current?.contains(event.target)) return;
@@ -955,7 +1967,7 @@ function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
955
1967
  }, [isOpen, containerRef, dispatch]);
956
1968
  }
957
1969
  function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
958
- const [placement, setPlacement] = useState3("above");
1970
+ const [placement, setPlacement] = useState8("above");
959
1971
  useLayoutEffect2(() => {
960
1972
  const trigger = triggerRef.current;
961
1973
  const menu = menuRef.current;
@@ -976,9 +1988,9 @@ function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
976
1988
  // src/participant/useParticipantFailedMenu.ts
977
1989
  function useParticipantFailedMenu() {
978
1990
  const [state, dispatch] = useReducer2(failedMenuReducer, CLOSED_FAILED_MENU);
979
- const containerRef = useRef4(null);
980
- const triggerRef = useRef4(null);
981
- const menuRef = useRef4(null);
1991
+ const containerRef = useRef12(null);
1992
+ const triggerRef = useRef12(null);
1993
+ const menuRef = useRef12(null);
982
1994
  useFailedMenuInitialFocus(state, menuRef);
983
1995
  useFailedMenuOutsideClose(state.isOpen, containerRef, dispatch);
984
1996
  const placement = useFailedMenuPlacement(state.isOpen, { triggerRef, menuRef });
@@ -1007,14 +2019,14 @@ function useParticipantFailedMenu() {
1007
2019
  }
1008
2020
 
1009
2021
  // src/participant/ParticipantFailedMenu.tsx
1010
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
2022
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
1011
2023
  function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
1012
2024
  const menu = useParticipantFailedMenu();
1013
2025
  const triggerId = useId2();
1014
2026
  if (!onEdit && !onDiscard) return null;
1015
2027
  const label = labels.messageOptions ?? DEFAULT_MESSAGE_OPTIONS_LABEL;
1016
- return /* @__PURE__ */ jsxs12("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
1017
- /* @__PURE__ */ jsx16(
2028
+ return /* @__PURE__ */ jsxs18("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
2029
+ /* @__PURE__ */ jsx23(
1018
2030
  "button",
1019
2031
  {
1020
2032
  type: "button",
@@ -1027,21 +2039,21 @@ function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
1027
2039
  "data-cv-tooltip": menu.state.isOpen ? void 0 : label,
1028
2040
  onClick: menu.handleTriggerClick,
1029
2041
  onKeyDown: menu.handleTriggerKeyDown,
1030
- children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u22EF" })
2042
+ children: /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", children: "\u22EF" })
1031
2043
  }
1032
2044
  ),
1033
- menu.state.isOpen ? /* @__PURE__ */ jsx16(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
2045
+ menu.state.isOpen ? /* @__PURE__ */ jsx23(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
1034
2046
  ] });
1035
2047
  }
1036
2048
 
1037
2049
  // src/participant/ParticipantMessageText.tsx
1038
- import { useMemo } from "react";
2050
+ import { useMemo as useMemo2 } from "react";
1039
2051
 
1040
2052
  // src/participant/ParticipantCopyableToken.tsx
1041
2053
  import { Check, Copy } from "lucide-react";
1042
2054
 
1043
2055
  // src/participant/useParticipantCopyFeedback.ts
1044
- import { useEffect as useEffect6, useReducer as useReducer3 } from "react";
2056
+ import { useEffect as useEffect13, useReducer as useReducer3 } from "react";
1045
2057
 
1046
2058
  // src/participant/participantProtocolCopy.ts
1047
2059
  async function copyProtocolToClipboard(protocol, clipboard) {
@@ -1065,7 +2077,7 @@ function useParticipantCopyFeedback() {
1065
2077
  const [state, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1066
2078
  const announce = useAnnounceCopy();
1067
2079
  const { isCopied, copyCount } = state;
1068
- useEffect6(() => {
2080
+ useEffect13(() => {
1069
2081
  if (!isCopied) return void 0;
1070
2082
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_FEEDBACK_MS);
1071
2083
  return () => clearTimeout(timer);
@@ -1080,10 +2092,10 @@ function useParticipantCopyFeedback() {
1080
2092
  }
1081
2093
 
1082
2094
  // src/participant/ParticipantCopyableToken.tsx
1083
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
2095
+ import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
1084
2096
  function ParticipantCopyableToken({ copyKind, value, labels }) {
1085
2097
  const { isCopied, copy } = useParticipantCopyFeedback();
1086
- return /* @__PURE__ */ jsxs13(
2098
+ return /* @__PURE__ */ jsxs19(
1087
2099
  "button",
1088
2100
  {
1089
2101
  type: "button",
@@ -1092,8 +2104,8 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
1092
2104
  onClick: () => copy(value, labels.valueCopied),
1093
2105
  children: [
1094
2106
  value,
1095
- isCopied ? /* @__PURE__ */ jsxs13("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
1096
- /* @__PURE__ */ jsx17(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 }),
1097
2109
  " ",
1098
2110
  labels.valueCopied
1099
2111
  ] }) : null
@@ -1103,7 +2115,7 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
1103
2115
  }
1104
2116
  function ParticipantLinkCopyButton({ value, labels }) {
1105
2117
  const { isCopied, copy } = useParticipantCopyFeedback();
1106
- return /* @__PURE__ */ jsx17(
2118
+ return /* @__PURE__ */ jsx24(
1107
2119
  "button",
1108
2120
  {
1109
2121
  type: "button",
@@ -1111,26 +2123,26 @@ function ParticipantLinkCopyButton({ value, labels }) {
1111
2123
  "aria-label": `${labels.copyValue} ${value}`,
1112
2124
  "data-cv-tooltip": labels.copyValue,
1113
2125
  onClick: () => copy(value, labels.valueCopied),
1114
- children: isCopied ? /* @__PURE__ */ jsx17(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx17(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 })
1115
2127
  }
1116
2128
  );
1117
2129
  }
1118
2130
 
1119
2131
  // src/participant/ParticipantMessageNodes.tsx
1120
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2132
+ import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
1121
2133
  var LINK_REL = "noopener noreferrer nofollow ugc";
1122
2134
  function renderLink(node, labels, key) {
1123
2135
  const isExternal = /^https?:/i.test(node.href);
1124
- return /* @__PURE__ */ jsxs14("span", { className: "cv-p-text__link-group", children: [
1125
- /* @__PURE__ */ jsxs14("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
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: [
1126
2138
  node.label,
1127
- isExternal ? /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
2139
+ isExternal ? /* @__PURE__ */ jsxs20("span", { className: "cv-p-sr-only", children: [
1128
2140
  " (",
1129
2141
  labels.externalLinkHint,
1130
2142
  ")"
1131
2143
  ] }) : null
1132
2144
  ] }),
1133
- /* @__PURE__ */ jsx18(ParticipantLinkCopyButton, { value: node.href, labels })
2145
+ /* @__PURE__ */ jsx25(ParticipantLinkCopyButton, { value: node.href, labels })
1134
2146
  ] }, key);
1135
2147
  }
1136
2148
  function renderInlineNodes(nodes, labels) {
@@ -1139,17 +2151,17 @@ function renderInlineNodes(nodes, labels) {
1139
2151
  case "text":
1140
2152
  return node.value;
1141
2153
  case "strong":
1142
- return /* @__PURE__ */ jsx18("strong", { children: renderInlineNodes(node.children, labels) }, key);
2154
+ return /* @__PURE__ */ jsx25("strong", { children: renderInlineNodes(node.children, labels) }, key);
1143
2155
  case "em":
1144
- return /* @__PURE__ */ jsx18("em", { children: renderInlineNodes(node.children, labels) }, key);
2156
+ return /* @__PURE__ */ jsx25("em", { children: renderInlineNodes(node.children, labels) }, key);
1145
2157
  case "del":
1146
- return /* @__PURE__ */ jsx18("del", { children: renderInlineNodes(node.children, labels) }, key);
2158
+ return /* @__PURE__ */ jsx25("del", { children: renderInlineNodes(node.children, labels) }, key);
1147
2159
  case "code":
1148
- return /* @__PURE__ */ jsx18("code", { className: "cv-p-text__code", children: node.value }, key);
2160
+ return /* @__PURE__ */ jsx25("code", { className: "cv-p-text__code", children: node.value }, key);
1149
2161
  case "link":
1150
2162
  return renderLink(node, labels, key);
1151
2163
  case "copyable":
1152
- return /* @__PURE__ */ jsx18(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
2164
+ return /* @__PURE__ */ jsx25(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
1153
2165
  }
1154
2166
  });
1155
2167
  }
@@ -1535,41 +2547,41 @@ function startsFenceLine(line) {
1535
2547
  }
1536
2548
 
1537
2549
  // src/participant/ParticipantMessageText.tsx
1538
- import { jsx as jsx19 } from "react/jsx-runtime";
2550
+ import { jsx as jsx26 } from "react/jsx-runtime";
1539
2551
  function renderBlock(block, labels, key) {
1540
- if (block.kind === "paragraph") return /* @__PURE__ */ jsx19("span", { children: renderInlineNodes(block.children, labels) }, key);
2552
+ if (block.kind === "paragraph") return /* @__PURE__ */ jsx26("span", { children: renderInlineNodes(block.children, labels) }, key);
1541
2553
  if (block.kind === "pre") {
1542
- return /* @__PURE__ */ jsx19("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx19("code", { children: block.value }) }, key);
2554
+ return /* @__PURE__ */ jsx26("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx26("code", { children: block.value }) }, key);
1543
2555
  }
1544
2556
  if (block.kind === "quote") {
1545
- return /* @__PURE__ */ jsx19("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);
1546
2558
  }
1547
- const items = block.items.map((item, index) => /* @__PURE__ */ jsx19("li", { children: renderInlineNodes(item, labels) }, index));
2559
+ const items = block.items.map((item, index) => /* @__PURE__ */ jsx26("li", { children: renderInlineNodes(item, labels) }, index));
1548
2560
  if (block.ordered) {
1549
- return /* @__PURE__ */ jsx19("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);
1550
2562
  }
1551
- return /* @__PURE__ */ jsx19("ul", { className: "cv-p-text__list", children: items }, key);
2563
+ return /* @__PURE__ */ jsx26("ul", { className: "cv-p-text__list", children: items }, key);
1552
2564
  }
1553
2565
  function ParticipantMessageText({ text, labels, id }) {
1554
- const blocks = useMemo(() => parseParticipantMessage(text), [text]);
2566
+ const blocks = useMemo2(() => parseParticipantMessage(text), [text]);
1555
2567
  if (blocks.length === 0) return null;
1556
2568
  const [only] = blocks;
1557
2569
  const children = blocks.length === 1 && only?.kind === "paragraph" ? renderInlineNodes(only.children, labels) : blocks.map((block, key) => renderBlock(block, labels, key));
1558
- return /* @__PURE__ */ jsx19("div", { className: "cv-p-text", id, children });
2570
+ return /* @__PURE__ */ jsx26("div", { className: "cv-p-text", id, children });
1559
2571
  }
1560
2572
 
1561
2573
  // src/participant/ParticipantBubbleBody.tsx
1562
- import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2574
+ import { Fragment as Fragment6, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
1563
2575
  function BubbleHeading({ content, labels }) {
1564
- if (content.isMine) return /* @__PURE__ */ jsx20("span", { className: "cv-p-sr-only", children: labels.me });
2576
+ if (content.isMine) return /* @__PURE__ */ jsx27("span", { className: "cv-p-sr-only", children: labels.me });
1565
2577
  if (!content.authorName) return null;
1566
- return /* @__PURE__ */ jsx20("span", { className: "cv-p-bubble__author", children: content.authorName });
2578
+ return /* @__PURE__ */ jsx27("span", { className: "cv-p-bubble__author", children: content.authorName });
1567
2579
  }
1568
2580
  function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
1569
- return /* @__PURE__ */ jsxs15(Fragment4, { children: [
1570
- /* @__PURE__ */ jsx20(BubbleHeading, { content, labels }),
1571
- content.text ? /* @__PURE__ */ jsx20(ParticipantMessageText, { text: content.text, labels, id: textId }) : null,
1572
- content.attachments.map((attachment) => /* @__PURE__ */ jsx20(
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(
1573
2585
  ParticipantAttachmentItem,
1574
2586
  {
1575
2587
  attachment,
@@ -1578,11 +2590,11 @@ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl,
1578
2590
  },
1579
2591
  attachment.id
1580
2592
  )),
1581
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1582
- /* @__PURE__ */ jsxs15("span", { className: "cv-p-bubble__meta", children: [
1583
- /* @__PURE__ */ jsx20("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1584
- ownStatus ? /* @__PURE__ */ jsx20(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
1585
- ownStatus === "failed" ? /* @__PURE__ */ jsx20(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
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
1586
2598
  ] })
1587
2599
  ] });
1588
2600
  }
@@ -1649,7 +2661,7 @@ function resolveOwnMessageStatus(params) {
1649
2661
  }
1650
2662
 
1651
2663
  // src/participant/ParticipantMessageBubble.tsx
1652
- import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
2664
+ import { jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
1653
2665
  function describeItem(item, perspective) {
1654
2666
  if (item.kind === "server") {
1655
2667
  const { message } = item;
@@ -1686,13 +2698,13 @@ function ParticipantMessageBubble(props) {
1686
2698
  const ownStatus = content.isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1687
2699
  const isFailed = ownStatus === "failed";
1688
2700
  const canRetry = isFailed && onRetry !== void 0;
1689
- const bubble = /* @__PURE__ */ jsxs16("div", { className: bubbleClassName(content.isMine, tail, isFailed), children: [
1690
- canRetry ? /* @__PURE__ */ jsx21(RetryButton, { labels, onRetry, describedBy: textId }) : null,
1691
- /* @__PURE__ */ jsx21(BubbleBody, { ...props, content, ownStatus, textId: canRetry ? textId : void 0 })
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 })
1692
2704
  ] });
1693
2705
  if (avatar === void 0 || content.isMine) return bubble;
1694
- return /* @__PURE__ */ jsxs16("div", { className: "cv-p-bubble-row", children: [
1695
- /* @__PURE__ */ jsx21("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
2706
+ return /* @__PURE__ */ jsxs22("div", { className: "cv-p-bubble-row", children: [
2707
+ /* @__PURE__ */ jsx28("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1696
2708
  bubble
1697
2709
  ] });
1698
2710
  }
@@ -1714,7 +2726,7 @@ function resolveParticipantBubbleActions(item, handlers) {
1714
2726
  }
1715
2727
 
1716
2728
  // src/participant/ParticipantTimeline.tsx
1717
- import { jsx as jsx22 } from "react/jsx-runtime";
2729
+ import { jsx as jsx29 } from "react/jsx-runtime";
1718
2730
  function createdAtOf2(item) {
1719
2731
  return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1720
2732
  }
@@ -1731,7 +2743,7 @@ function resolveActions(item, pendingActions) {
1731
2743
  }
1732
2744
  function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1733
2745
  if (!isEnabled || isMine) return void 0;
1734
- return showAvatar ? /* @__PURE__ */ jsx22(ParticipantAuthorAvatar, { name: author, render }) : null;
2746
+ return showAvatar ? /* @__PURE__ */ jsx29(ParticipantAuthorAvatar, { name: author, render }) : null;
1735
2747
  }
1736
2748
  function ParticipantTimeline(props) {
1737
2749
  const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
@@ -1743,7 +2755,7 @@ function ParticipantTimeline(props) {
1743
2755
  for (const item of items) {
1744
2756
  const day = new Date(createdAtOf2(item));
1745
2757
  if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1746
- nodes.push(/* @__PURE__ */ jsx22(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
2758
+ nodes.push(/* @__PURE__ */ jsx29(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1747
2759
  previousAuthor = void 0;
1748
2760
  }
1749
2761
  previousDay = day;
@@ -1758,7 +2770,7 @@ function ParticipantTimeline(props) {
1758
2770
  render: props.renderAuthorAvatar
1759
2771
  });
1760
2772
  nodes.push(
1761
- /* @__PURE__ */ jsx22(
2773
+ /* @__PURE__ */ jsx29(
1762
2774
  ParticipantMessageBubble,
1763
2775
  {
1764
2776
  item,
@@ -1774,19 +2786,19 @@ function ParticipantTimeline(props) {
1774
2786
  )
1775
2787
  );
1776
2788
  }
1777
- return /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__messages", children: nodes });
2789
+ return /* @__PURE__ */ jsx29("div", { className: "cv-p-thread__messages", children: nodes });
1778
2790
  }
1779
2791
 
1780
2792
  // src/participant/ParticipantConversationAvatar.tsx
1781
- import { jsx as jsx23 } from "react/jsx-runtime";
2793
+ import { jsx as jsx30 } from "react/jsx-runtime";
1782
2794
  function ParticipantConversationAvatar({ icon }) {
1783
- return /* @__PURE__ */ jsx23("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 });
1784
2796
  }
1785
2797
 
1786
2798
  // src/participant/ParticipantProtocolBadge.tsx
1787
2799
  import { Check as Check2, Copy as Copy2 } from "lucide-react";
1788
- import { useEffect as useEffect7, useReducer as useReducer4 } from "react";
1789
- import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
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";
1790
2802
  var COPIED_ANNOUNCEMENT_MS = 3e3;
1791
2803
  function ParticipantProtocolBadgeView({
1792
2804
  protocol,
@@ -1795,16 +2807,16 @@ function ParticipantProtocolBadgeView({
1795
2807
  onCopy
1796
2808
  }) {
1797
2809
  const { isCopied, copyCount } = copyState;
1798
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
1799
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1800
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol__code", children: [
1801
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-sr-only", children: [
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: [
1802
2814
  labels.protocolPrefix,
1803
2815
  " "
1804
2816
  ] }),
1805
2817
  protocol
1806
2818
  ] }),
1807
- /* @__PURE__ */ jsxs17(
2819
+ /* @__PURE__ */ jsxs23(
1808
2820
  "button",
1809
2821
  {
1810
2822
  type: "button",
@@ -1813,19 +2825,19 @@ function ParticipantProtocolBadgeView({
1813
2825
  "data-cv-tooltip": labels.copyProtocol,
1814
2826
  onClick: onCopy,
1815
2827
  children: [
1816
- isCopied ? /* @__PURE__ */ jsx24(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx24(Copy2, { size: 18, "aria-hidden": true }),
1817
- isCopied ? /* @__PURE__ */ jsx24("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
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
1818
2830
  ]
1819
2831
  }
1820
2832
  )
1821
2833
  ] }),
1822
- /* @__PURE__ */ jsx24("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx24("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 })
1823
2835
  ] });
1824
2836
  }
1825
2837
  function ParticipantProtocolBadge({ protocol, labels }) {
1826
2838
  const [copyState, dispatch] = useReducer4(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1827
2839
  const { isCopied, copyCount } = copyState;
1828
- useEffect7(() => {
2840
+ useEffect14(() => {
1829
2841
  if (!isCopied) return void 0;
1830
2842
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
1831
2843
  return () => clearTimeout(timer);
@@ -1834,7 +2846,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1834
2846
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1835
2847
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
1836
2848
  }
1837
- return /* @__PURE__ */ jsx24(
2849
+ return /* @__PURE__ */ jsx31(
1838
2850
  ParticipantProtocolBadgeView,
1839
2851
  {
1840
2852
  protocol,
@@ -1846,19 +2858,19 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1846
2858
  }
1847
2859
 
1848
2860
  // src/participant/ParticipantThreadHeader.tsx
1849
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2861
+ import { jsx as jsx32, jsxs as jsxs24 } from "react/jsx-runtime";
1850
2862
  function Title({ conversation, labels, onOpenSubject }) {
1851
2863
  const { subjectType, subjectId, subjectLabel } = conversation;
1852
- return /* @__PURE__ */ jsx25("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx25(
2864
+ return /* @__PURE__ */ jsx32("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx32(
1853
2865
  "button",
1854
2866
  {
1855
2867
  type: "button",
1856
2868
  className: "cv-p-thread__title-button",
1857
2869
  "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1858
2870
  onClick: () => onOpenSubject({ subjectType, subjectId }),
1859
- children: /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel })
2871
+ children: /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1860
2872
  }
1861
- ) : /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
2873
+ ) : /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1862
2874
  }
1863
2875
  function ParticipantThreadHeader({
1864
2876
  conversation,
@@ -1873,30 +2885,34 @@ function ParticipantThreadHeader({
1873
2885
  const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1874
2886
  const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1875
2887
  const hasChannels = resolveParticipantChannels(conversation.channels).length > 0;
1876
- return /* @__PURE__ */ jsxs18("header", { className: "cv-p-thread__header", children: [
1877
- onBack ? /* @__PURE__ */ jsx25(ParticipantBackButton, { label: labels.back, onBack }) : null,
1878
- icon !== void 0 ? /* @__PURE__ */ jsx25(ParticipantConversationAvatar, { icon }) : null,
1879
- /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__heading", children: [
1880
- icon === void 0 && group ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1881
- /* @__PURE__ */ jsx25(Title, { conversation, labels, onOpenSubject }),
1882
- counterpartLabel ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
1883
- conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__meta", children: [
1884
- conversation.protocol ? /* @__PURE__ */ jsx25(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1885
- /* @__PURE__ */ jsx25(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
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 })
1886
2898
  ] }) : null
1887
2899
  ] }),
1888
- actions ? /* @__PURE__ */ jsx25("div", { className: "cv-p-thread__actions", children: actions }) : null
2900
+ actions ? /* @__PURE__ */ jsx32("div", { className: "cv-p-thread__actions", children: actions }) : null
1889
2901
  ] });
1890
2902
  }
1891
2903
 
1892
2904
  // src/participant/ParticipantThread.tsx
1893
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2905
+ import { jsx as jsx33, jsxs as jsxs25 } from "react/jsx-runtime";
1894
2906
  function Footer({ props }) {
1895
2907
  const { conversation, labels, draft } = props;
1896
2908
  if (conversation.status === "closed") {
1897
- return /* @__PURE__ */ jsx26("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
2909
+ return /* @__PURE__ */ jsx33("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
2910
+ }
2911
+ const lock = props.renderComposerLock?.(conversation);
2912
+ if (lock !== void 0 && lock !== null) {
2913
+ return /* @__PURE__ */ jsx33("div", { className: "cv-p-thread__lock", role: "region", "aria-live": "polite", children: lock });
1898
2914
  }
1899
- return /* @__PURE__ */ jsx26(
2915
+ return /* @__PURE__ */ jsx33(
1900
2916
  ParticipantComposer,
1901
2917
  {
1902
2918
  value: draft.value,
@@ -1909,12 +2925,14 @@ function Footer({ props }) {
1909
2925
  isSending: props.isSending,
1910
2926
  maxLength: props.maxLength,
1911
2927
  acceptedTypes: props.acceptedTypes,
1912
- quickReplies: props.quickReplies
2928
+ quickReplies: props.quickReplies,
2929
+ audioRecording: props.audioRecording,
2930
+ onSendAudio: props.onSendAudio
1913
2931
  }
1914
2932
  );
1915
2933
  }
1916
2934
  function Header({ props }) {
1917
- return /* @__PURE__ */ jsx26(
2935
+ return /* @__PURE__ */ jsx33(
1918
2936
  ParticipantThreadHeader,
1919
2937
  {
1920
2938
  conversation: props.conversation,
@@ -1932,17 +2950,17 @@ function ThreadBody(props) {
1932
2950
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
1933
2951
  const subjectCard = renderSubjectCard?.(conversation);
1934
2952
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
1935
- const rootRef = useRef5(null);
2953
+ const rootRef = useRef13(null);
1936
2954
  useSuppressTouchTooltips(rootRef);
1937
- return /* @__PURE__ */ jsxs19("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
1938
- /* @__PURE__ */ jsx26(Header, { props }),
1939
- subjectCard ? /* @__PURE__ */ jsx26("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1940
- /* @__PURE__ */ jsxs19("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1941
- loadView.isLoading ? /* @__PURE__ */ jsx26(ParticipantLoading, { labels }) : null,
1942
- loadView.hasError ? /* @__PURE__ */ jsx26(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1943
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx26("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1944
- /* @__PURE__ */ jsx26(ParticipantTimeline, { ...props }),
1945
- /* @__PURE__ */ jsx26(
2955
+ return /* @__PURE__ */ jsxs25("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
2956
+ /* @__PURE__ */ jsx33(Header, { props }),
2957
+ subjectCard ? /* @__PURE__ */ jsx33("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
2958
+ /* @__PURE__ */ jsxs25("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
2959
+ loadView.isLoading ? /* @__PURE__ */ jsx33(ParticipantLoading, { labels }) : null,
2960
+ loadView.hasError ? /* @__PURE__ */ jsx33(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
2961
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx33("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
2962
+ /* @__PURE__ */ jsx33(ParticipantTimeline, { ...props }),
2963
+ /* @__PURE__ */ jsx33(
1946
2964
  ParticipantScrollToLatest,
1947
2965
  {
1948
2966
  label: labels.scrollToLatest,
@@ -1953,19 +2971,19 @@ function ThreadBody(props) {
1953
2971
  }
1954
2972
  )
1955
2973
  ] }),
1956
- /* @__PURE__ */ jsx26(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1957
- /* @__PURE__ */ jsx26(Footer, { props })
2974
+ /* @__PURE__ */ jsx33(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
2975
+ /* @__PURE__ */ jsx33(Footer, { props })
1958
2976
  ] });
1959
2977
  }
1960
2978
  function ParticipantThread(props) {
1961
- return /* @__PURE__ */ jsxs19(ParticipantCopyAnnouncer, { children: [
1962
- /* @__PURE__ */ jsx26(ThreadBody, { ...props }),
1963
- /* @__PURE__ */ jsx26(TooltipLayer, {})
2979
+ return /* @__PURE__ */ jsxs25(ParticipantCopyAnnouncer, { children: [
2980
+ /* @__PURE__ */ jsx33(ThreadBody, { ...props }),
2981
+ /* @__PURE__ */ jsx33(TooltipLayer, {})
1964
2982
  ] });
1965
2983
  }
1966
2984
 
1967
2985
  // src/participant/useParticipantThreadController.ts
1968
- import { useMemo as useMemo4 } from "react";
2986
+ import { useMemo as useMemo5 } from "react";
1969
2987
 
1970
2988
  // src/participant/bindAttachmentUrlResolver.ts
1971
2989
  function bindAttachmentUrlResolver(api) {
@@ -2049,7 +3067,7 @@ function appendEntryToDraft(entry, current) {
2049
3067
  }
2050
3068
 
2051
3069
  // src/participant/useParticipantConversationView.ts
2052
- import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
3070
+ import { useCallback as useCallback9, useMemo as useMemo4 } from "react";
2053
3071
 
2054
3072
  // src/participant/participantDrafts.ts
2055
3073
  var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
@@ -2093,7 +3111,7 @@ function participantSendStatesReducer(states, { subject, action }) {
2093
3111
  }
2094
3112
 
2095
3113
  // src/participant/useParticipantThread.ts
2096
- import { useCallback as useCallback4, useEffect as useEffect11, useMemo as useMemo2, useReducer as useReducer5, useRef as useRef9 } from "react";
3114
+ import { useCallback as useCallback8, useEffect as useEffect18, useMemo as useMemo3, useReducer as useReducer5, useRef as useRef17 } from "react";
2097
3115
 
2098
3116
  // src/participant/participantThreadState.ts
2099
3117
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -2129,7 +3147,7 @@ function participantThreadReducer(state, action) {
2129
3147
  }
2130
3148
 
2131
3149
  // src/participant/useParticipantMarkRead.ts
2132
- import { useEffect as useEffect9 } from "react";
3150
+ import { useEffect as useEffect16 } from "react";
2133
3151
 
2134
3152
  // src/participant/shouldMarkRead.ts
2135
3153
  function shouldMarkParticipantRead(params) {
@@ -2141,12 +3159,12 @@ function shouldMarkParticipantRead(params) {
2141
3159
  }
2142
3160
 
2143
3161
  // src/participant/useDocumentVisibility.ts
2144
- import { useEffect as useEffect8, useState as useState4 } from "react";
3162
+ import { useEffect as useEffect15, useState as useState9 } from "react";
2145
3163
  function useDocumentVisibility() {
2146
- const [visibilityState, setVisibilityState] = useState4(
3164
+ const [visibilityState, setVisibilityState] = useState9(
2147
3165
  () => typeof document === "undefined" ? "visible" : document.visibilityState
2148
3166
  );
2149
- useEffect8(() => {
3167
+ useEffect15(() => {
2150
3168
  function handleVisibilityChange() {
2151
3169
  setVisibilityState(document.visibilityState);
2152
3170
  }
@@ -2161,18 +3179,18 @@ function useDocumentVisibility() {
2161
3179
  function useParticipantMarkRead(params) {
2162
3180
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
2163
3181
  const visibilityState = useDocumentVisibility();
2164
- useEffect9(() => {
3182
+ useEffect16(() => {
2165
3183
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
2166
3184
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
2167
3185
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
2168
3186
  }
2169
3187
 
2170
3188
  // src/participant/useParticipantRevalidation.ts
2171
- import { useEffect as useEffect10, useRef as useRef6 } from "react";
3189
+ import { useEffect as useEffect17, useRef as useRef14 } from "react";
2172
3190
  function useParticipantRevalidation(api, onRevalidate) {
2173
- const handlerRef = useRef6(onRevalidate);
3191
+ const handlerRef = useRef14(onRevalidate);
2174
3192
  handlerRef.current = onRevalidate;
2175
- useEffect10(() => {
3193
+ useEffect17(() => {
2176
3194
  function handleFocus() {
2177
3195
  handlerRef.current();
2178
3196
  }
@@ -2199,12 +3217,12 @@ function useParticipantRevalidation(api, onRevalidate) {
2199
3217
  }
2200
3218
 
2201
3219
  // src/participant/useParticipantSend.ts
2202
- import { useCallback as useCallback2, useRef as useRef7 } from "react";
3220
+ import { useCallback as useCallback6, useRef as useRef15 } from "react";
2203
3221
  function useParticipantSend(params) {
2204
3222
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
2205
- const entriesRef = useRef7(params.entries);
3223
+ const entriesRef = useRef15(params.entries);
2206
3224
  entriesRef.current = params.entries;
2207
- const deliver = useCallback2(
3225
+ const deliver = useCallback6(
2208
3226
  async (entry) => {
2209
3227
  const { clientMessageId } = entry;
2210
3228
  try {
@@ -2228,7 +3246,7 @@ function useParticipantSend(params) {
2228
3246
  },
2229
3247
  [apiRef, dispatchSend, onSentMessage]
2230
3248
  );
2231
- const send = useCallback2(
3249
+ const send = useCallback6(
2232
3250
  async (content) => {
2233
3251
  const entry = createParticipantSendEntry({
2234
3252
  clientMessageId: crypto.randomUUID(),
@@ -2241,7 +3259,7 @@ function useParticipantSend(params) {
2241
3259
  },
2242
3260
  [deliver, dispatchSend, subject]
2243
3261
  );
2244
- const retry = useCallback2(
3262
+ const retry = useCallback6(
2245
3263
  async (clientMessageId) => {
2246
3264
  const entry = findFailedEntry(entriesRef.current, clientMessageId);
2247
3265
  if (!entry) return;
@@ -2254,13 +3272,13 @@ function useParticipantSend(params) {
2254
3272
  }
2255
3273
 
2256
3274
  // src/participant/useParticipantUnread.ts
2257
- import { useCallback as useCallback3, useRef as useRef8, useState as useState5 } from "react";
3275
+ import { useCallback as useCallback7, useRef as useRef16, useState as useState10 } from "react";
2258
3276
  function useParticipantUnread(params) {
2259
3277
  const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2260
- const [acknowledgedCount, setAcknowledgedCount] = useState5(0);
2261
- const messagesRef = useRef8(messages);
3278
+ const [acknowledgedCount, setAcknowledgedCount] = useState10(0);
3279
+ const messagesRef = useRef16(messages);
2262
3280
  messagesRef.current = messages;
2263
- const handleMarkedRead = useCallback3(
3281
+ const handleMarkedRead = useCallback7(
2264
3282
  (subject) => {
2265
3283
  if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective));
2266
3284
  onMarkedRead?.(subject);
@@ -2283,13 +3301,13 @@ function isForSubject(event, subject) {
2283
3301
  function useParticipantThread(params) {
2284
3302
  const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
2285
3303
  const { subjectType, subjectId } = subject;
2286
- const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
3304
+ const stableSubject = useMemo3(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2287
3305
  const [state, dispatch] = useReducer5(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2288
- const apiRef = useRef9(api);
3306
+ const apiRef = useRef17(api);
2289
3307
  apiRef.current = api;
2290
- const activeSubjectKey = useRef9("");
3308
+ const activeSubjectKey = useRef17("");
2291
3309
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
2292
- const refresh = useCallback4(async () => {
3310
+ const refresh = useCallback8(async () => {
2293
3311
  const requestKey = `${subjectType}:${subjectId}`;
2294
3312
  dispatch({ type: "started" });
2295
3313
  try {
@@ -2302,7 +3320,7 @@ function useParticipantThread(params) {
2302
3320
  }
2303
3321
  }, [stableSubject, subjectType, subjectId]);
2304
3322
  const oldestMessageId = state.messages[0]?.id;
2305
- const loadOlder = useCallback4(async () => {
3323
+ const loadOlder = useCallback8(async () => {
2306
3324
  if (!oldestMessageId) return;
2307
3325
  const requestKey = `${subjectType}:${subjectId}`;
2308
3326
  try {
@@ -2314,9 +3332,9 @@ function useParticipantThread(params) {
2314
3332
  if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
2315
3333
  }
2316
3334
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
2317
- const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
3335
+ const handleSentMessage = useCallback8((message) => dispatch({ type: "sendConfirmed", message }), []);
2318
3336
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
2319
- useEffect11(() => {
3337
+ useEffect18(() => {
2320
3338
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
2321
3339
  dispatch({ type: "reset" });
2322
3340
  void refresh();
@@ -2324,7 +3342,7 @@ function useParticipantThread(params) {
2324
3342
  useParticipantRevalidation(api, (event) => {
2325
3343
  if (isForSubject(event, stableSubject)) void refresh();
2326
3344
  });
2327
- useEffect11(() => {
3345
+ useEffect18(() => {
2328
3346
  if (sendEntries.length === 0) return;
2329
3347
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
2330
3348
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
@@ -2336,7 +3354,7 @@ function useParticipantThread(params) {
2336
3354
  messageCount: state.messages.length,
2337
3355
  onMarkedRead: unread.handleMarkedRead
2338
3356
  });
2339
- const localPending = useMemo2(() => toLocalPending(sendEntries), [sendEntries]);
3357
+ const localPending = useMemo3(() => toLocalPending(sendEntries), [sendEntries]);
2340
3358
  return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
2341
3359
  }
2342
3360
 
@@ -2345,7 +3363,7 @@ function useParticipantConversationView(params) {
2345
3363
  const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
2346
3364
  const { subjectType, subjectId } = subject;
2347
3365
  const sendEntries = selectSendEntries(sendStates, subject);
2348
- const dispatchSend = useCallback5(
3366
+ const dispatchSend = useCallback9(
2349
3367
  (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
2350
3368
  [dispatchSendStates, subjectType, subjectId]
2351
3369
  );
@@ -2360,7 +3378,7 @@ function useParticipantConversationView(params) {
2360
3378
  hostPending: pendingMessages,
2361
3379
  perspective: params.perspective
2362
3380
  });
2363
- const items = useMemo3(
3381
+ const items = useMemo4(
2364
3382
  () => mergeParticipantMessages({
2365
3383
  serverMessages: thread.messages,
2366
3384
  hostPending: pendingMessages ?? [],
@@ -2373,9 +3391,9 @@ function useParticipantConversationView(params) {
2373
3391
  }
2374
3392
 
2375
3393
  // src/participant/useParticipantDraft.ts
2376
- import { useState as useState6 } from "react";
3394
+ import { useState as useState11 } from "react";
2377
3395
  function useParticipantDraft(draftsRef, subject) {
2378
- const [draft, setDraftState] = useState6(() => getDraft(draftsRef.current, subject));
3396
+ const [draft, setDraftState] = useState11(() => getDraft(draftsRef.current, subject));
2379
3397
  function replaceDrafts(next) {
2380
3398
  draftsRef.current = next;
2381
3399
  setDraftState(getDraft(next, subject));
@@ -2394,8 +3412,29 @@ function useParticipantDraft(draftsRef, subject) {
2394
3412
  return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
2395
3413
  }
2396
3414
 
3415
+ // src/participant/useParticipantSendAudio.ts
3416
+ import { useCallback as useCallback10, useRef as useRef18 } from "react";
3417
+ function useParticipantSendAudio(send) {
3418
+ const sendRef = useRef18(send);
3419
+ sendRef.current = send;
3420
+ const inFlightRef = useRef18(/* @__PURE__ */ new Set());
3421
+ return useCallback10((file) => {
3422
+ const inFlight = inFlightRef.current;
3423
+ if (inFlight.has(file)) return;
3424
+ inFlight.add(file);
3425
+ void (async () => {
3426
+ try {
3427
+ await sendRef.current({ files: [file] });
3428
+ } catch {
3429
+ } finally {
3430
+ inFlight.delete(file);
3431
+ }
3432
+ })();
3433
+ }, []);
3434
+ }
3435
+
2397
3436
  // src/participant/useParticipantThreadScroll.ts
2398
- import { useEffect as useEffect12, useRef as useRef11 } from "react";
3437
+ import { useEffect as useEffect19, useRef as useRef20 } from "react";
2399
3438
 
2400
3439
  // src/participant/participantNewMessages.ts
2401
3440
  function collectServerMessageIds(items) {
@@ -2420,9 +3459,9 @@ function countNewIncomingMessages(previousIds, currentItems, perspective) {
2420
3459
  }
2421
3460
 
2422
3461
  // src/participant/useScrollAwayState.ts
2423
- import { useCallback as useCallback6 } from "react";
3462
+ import { useCallback as useCallback11 } from "react";
2424
3463
  function useScrollAwayState(ref, isNearBottom) {
2425
- const scrollToLatest = useCallback6(() => {
3464
+ const scrollToLatest = useCallback11(() => {
2426
3465
  const element = ref.current;
2427
3466
  if (!element) return;
2428
3467
  element.scrollTo(
@@ -2433,7 +3472,7 @@ function useScrollAwayState(ref, isNearBottom) {
2433
3472
  }
2434
3473
 
2435
3474
  // src/participant/useStickToBottom.ts
2436
- import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
3475
+ import { useCallback as useCallback12, useLayoutEffect as useLayoutEffect3, useRef as useRef19, useState as useState12 } from "react";
2437
3476
 
2438
3477
  // src/participant/participantScroll.ts
2439
3478
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -2455,17 +3494,17 @@ function resolveScrollAction(params) {
2455
3494
 
2456
3495
  // src/participant/useStickToBottom.ts
2457
3496
  function useStickToBottom({ firstKey, lastKey }) {
2458
- const ref = useRef10(null);
2459
- const snapshot = useRef10({
3497
+ const ref = useRef19(null);
3498
+ const snapshot = useRef19({
2460
3499
  hasPositioned: false,
2461
3500
  firstKey: void 0,
2462
3501
  lastKey: void 0,
2463
3502
  scrollTop: 0,
2464
3503
  scrollHeight: 0
2465
3504
  });
2466
- const [isNearBottom, setIsNearBottom] = useState7(true);
2467
- const isNearBottomRef = useRef10(true);
2468
- const onScroll = useCallback7((event) => {
3505
+ const [isNearBottom, setIsNearBottom] = useState12(true);
3506
+ const isNearBottomRef = useRef19(true);
3507
+ const onScroll = useCallback12((event) => {
2469
3508
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
2470
3509
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
2471
3510
  snapshot.current.scrollTop = scrollTop;
@@ -2520,8 +3559,8 @@ function useParticipantThreadScroll(items, perspective) {
2520
3559
  });
2521
3560
  const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2522
3561
  const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2523
- const seenIds = useRef11(/* @__PURE__ */ new Set());
2524
- useEffect12(() => {
3562
+ const seenIds = useRef20(/* @__PURE__ */ new Set());
3563
+ useEffect19(() => {
2525
3564
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
2526
3565
  }, [isNearBottom, items]);
2527
3566
  return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
@@ -2536,22 +3575,26 @@ function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending
2536
3575
  ...onRetryPending ? { retryHost: onRetryPending } : {}
2537
3576
  };
2538
3577
  }
3578
+ function buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry }) {
3579
+ return (clientMessageId) => {
3580
+ const entry = findFailedEntry(sendEntries, clientMessageId);
3581
+ if (!entry) return;
3582
+ restoreFromEntry(entry);
3583
+ dispatchSend({ type: "discarded", clientMessageId });
3584
+ };
3585
+ }
2539
3586
  function useParticipantThreadController(params) {
2540
3587
  const { api, subject, conversation, draftsRef } = params;
2541
3588
  const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject);
2542
3589
  const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params);
2543
- const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
3590
+ const resolveAttachmentUrl = useMemo5(() => bindAttachmentUrlResolver(api), [api]);
2544
3591
  const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective);
3592
+ const handleSendAudio = useParticipantSendAudio(thread.send);
3593
+ const handleEdit = buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry });
2545
3594
  function handleSend() {
2546
3595
  const content = takeForSend();
2547
3596
  if (content) void thread.send(content);
2548
3597
  }
2549
- function handleEdit(clientMessageId) {
2550
- const entry = findFailedEntry(sendEntries, clientMessageId);
2551
- if (!entry) return;
2552
- restoreFromEntry(entry);
2553
- dispatchSend({ type: "discarded", clientMessageId });
2554
- }
2555
3598
  return {
2556
3599
  conversation,
2557
3600
  items,
@@ -2569,17 +3612,18 @@ function useParticipantThreadController(params) {
2569
3612
  locale: params.locale,
2570
3613
  onLoadOlder: () => void thread.loadOlder(),
2571
3614
  pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
2572
- perspective: params.perspective
3615
+ perspective: params.perspective,
3616
+ onSendAudio: handleSendAudio
2573
3617
  };
2574
3618
  }
2575
3619
 
2576
3620
  // src/participant/ParticipantThreadScreen.tsx
2577
- import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
3621
+ import { jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2578
3622
  function useOpenedConversation(api, subject, isNeeded) {
2579
- const [opened, setOpened] = useState8({ status: "idle" });
3623
+ const [opened, setOpened] = useState13({ status: "idle" });
2580
3624
  const { subjectType, subjectId } = subject;
2581
3625
  const canOpen = api.openConversation !== void 0;
2582
- useEffect13(() => {
3626
+ useEffect20(() => {
2583
3627
  if (!isNeeded || !canOpen) return;
2584
3628
  let isCurrent = true;
2585
3629
  setOpened({ status: "loading" });
@@ -2597,9 +3641,9 @@ function NotFound({
2597
3641
  labels,
2598
3642
  onBack
2599
3643
  }) {
2600
- return /* @__PURE__ */ jsxs20("div", { className: "cv-p cv-p-thread", children: [
2601
- /* @__PURE__ */ jsx27("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx27(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2602
- /* @__PURE__ */ jsx27("p", { className: "cv-p-empty", children: labels.notFound })
3644
+ return /* @__PURE__ */ jsxs26("div", { className: "cv-p cv-p-thread", children: [
3645
+ /* @__PURE__ */ jsx34("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx34(ParticipantBackButton, { label: labels.back, onBack }) : null }),
3646
+ /* @__PURE__ */ jsx34("p", { className: "cv-p-empty", children: labels.notFound })
2603
3647
  ] });
2604
3648
  }
2605
3649
  function ParticipantThreadScreen(props) {
@@ -2609,12 +3653,12 @@ function ParticipantThreadScreen(props) {
2609
3653
  );
2610
3654
  const opened = useOpenedConversation(api, selected, listed === void 0);
2611
3655
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
2612
- if (conversation) return /* @__PURE__ */ jsx27(LoadedThread, { ...props, conversation });
3656
+ if (conversation) return /* @__PURE__ */ jsx34(LoadedThread, { ...props, conversation });
2613
3657
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
2614
3658
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
2615
3659
  if (isResolving || api.openConversation === void 0 && isListLoading)
2616
- return /* @__PURE__ */ jsx27("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2617
- return /* @__PURE__ */ jsx27(NotFound, { labels, onBack: props.onBack });
3660
+ return /* @__PURE__ */ jsx34("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
3661
+ return /* @__PURE__ */ jsx34(NotFound, { labels, onBack: props.onBack });
2618
3662
  }
2619
3663
  function LoadedThread(props) {
2620
3664
  const core = useParticipantThreadController({
@@ -2622,7 +3666,7 @@ function LoadedThread(props) {
2622
3666
  subject: props.selected,
2623
3667
  onMarkedRead: props.inbox.markSubjectRead
2624
3668
  });
2625
- return /* @__PURE__ */ jsx27(
3669
+ return /* @__PURE__ */ jsx34(
2626
3670
  ParticipantThread,
2627
3671
  {
2628
3672
  ...core,
@@ -2634,7 +3678,9 @@ function LoadedThread(props) {
2634
3678
  renderSubjectIcon: props.renderSubjectIcon,
2635
3679
  avatars: props.avatars,
2636
3680
  renderAuthorAvatar: props.renderAuthorAvatar,
2637
- tail: props.tail
3681
+ tail: props.tail,
3682
+ audioRecording: props.audioRecording,
3683
+ renderComposerLock: props.renderComposerLock
2638
3684
  }
2639
3685
  );
2640
3686
  }
@@ -2760,16 +3806,16 @@ function buildParticipantThemeStyle(theme) {
2760
3806
  }
2761
3807
 
2762
3808
  // src/participant/ParticipantConversationsScreen.tsx
2763
- import { jsx as jsx28 } from "react/jsx-runtime";
3809
+ import { jsx as jsx35 } from "react/jsx-runtime";
2764
3810
  var ALL_FILTER3 = "all";
2765
3811
  function ParticipantConversationsScreen(props) {
2766
3812
  const { selected, inbox, subjectGroups, classNames } = props;
2767
- const [filter, setFilter] = useState9(ALL_FILTER3);
2768
- const [searchQuery, setSearchQuery] = useState9("");
2769
- const draftsRef = useRef12(/* @__PURE__ */ new Map());
3813
+ const [filter, setFilter] = useState14(ALL_FILTER3);
3814
+ const [searchQuery, setSearchQuery] = useState14("");
3815
+ const draftsRef = useRef21(/* @__PURE__ */ new Map());
2770
3816
  const [sendStates, dispatchSendStates] = useReducer6(participantSendStatesReducer, /* @__PURE__ */ new Map());
2771
- const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2772
- const view = useMemo5(
3817
+ const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
3818
+ const view = useMemo6(
2773
3819
  () => groupParticipantConversations({
2774
3820
  conversations: inbox.conversations,
2775
3821
  subjectGroups,
@@ -2780,7 +3826,7 @@ function ParticipantConversationsScreen(props) {
2780
3826
  );
2781
3827
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
2782
3828
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
2783
- return /* @__PURE__ */ jsx28("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
3829
+ 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(
2784
3830
  ParticipantInbox,
2785
3831
  {
2786
3832
  view,
@@ -2797,7 +3843,7 @@ function ParticipantConversationsScreen(props) {
2797
3843
  renderSubjectIcon: props.renderSubjectIcon,
2798
3844
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
2799
3845
  }
2800
- ) }) : /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
3846
+ ) }) : /* @__PURE__ */ jsx35("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(
2801
3847
  ParticipantThreadScreen,
2802
3848
  {
2803
3849
  ...props,
@@ -2812,7 +3858,7 @@ function ParticipantConversationsScreen(props) {
2812
3858
  }
2813
3859
 
2814
3860
  // src/participant/useParticipantInbox.ts
2815
- import { useCallback as useCallback8, useEffect as useEffect14, useReducer as useReducer7 } from "react";
3861
+ import { useCallback as useCallback13, useEffect as useEffect21, useReducer as useReducer7 } from "react";
2816
3862
 
2817
3863
  // src/participant/participantInboxState.ts
2818
3864
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -2863,7 +3909,7 @@ function describeError2(error) {
2863
3909
  }
2864
3910
  function useParticipantInbox(api) {
2865
3911
  const [state, dispatch] = useReducer7(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2866
- const refresh = useCallback8(async () => {
3912
+ const refresh = useCallback13(async () => {
2867
3913
  dispatch({ type: "started" });
2868
3914
  try {
2869
3915
  const page = await api.listConversations();
@@ -2873,7 +3919,7 @@ function useParticipantInbox(api) {
2873
3919
  }
2874
3920
  }, [api]);
2875
3921
  const { nextCursor } = state;
2876
- const loadMore = useCallback8(async () => {
3922
+ const loadMore = useCallback13(async () => {
2877
3923
  if (!nextCursor) return;
2878
3924
  try {
2879
3925
  const page = await api.listConversations({ cursor: nextCursor });
@@ -2882,10 +3928,10 @@ function useParticipantInbox(api) {
2882
3928
  dispatch({ type: "failed", error: describeError2(error) });
2883
3929
  }
2884
3930
  }, [api, nextCursor]);
2885
- const markSubjectRead = useCallback8((subject) => {
3931
+ const markSubjectRead = useCallback13((subject) => {
2886
3932
  dispatch({ type: "markedRead", subject });
2887
3933
  }, []);
2888
- useEffect14(() => {
3934
+ useEffect21(() => {
2889
3935
  void refresh();
2890
3936
  }, [refresh]);
2891
3937
  useParticipantRevalidation(api, () => {
@@ -2895,11 +3941,11 @@ function useParticipantInbox(api) {
2895
3941
  }
2896
3942
 
2897
3943
  // src/participant/ParticipantConversations.tsx
2898
- import { jsx as jsx29 } from "react/jsx-runtime";
3944
+ import { jsx as jsx36 } from "react/jsx-runtime";
2899
3945
  function ParticipantConversations(props) {
2900
3946
  const inbox = useParticipantInbox(props.api);
2901
3947
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
2902
- return /* @__PURE__ */ jsx29(
3948
+ return /* @__PURE__ */ jsx36(
2903
3949
  ParticipantConversationsScreen,
2904
3950
  {
2905
3951
  ...props,
@@ -2909,7 +3955,7 @@ function ParticipantConversations(props) {
2909
3955
  }
2910
3956
 
2911
3957
  // src/participant/ConversationThread.tsx
2912
- import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer8, useRef as useRef13 } from "react";
3958
+ import { useCallback as useCallback14, useMemo as useMemo7, useReducer as useReducer8, useRef as useRef22 } from "react";
2913
3959
 
2914
3960
  // src/participant/conversationThreadApi.ts
2915
3961
  function toParticipantApi(api) {
@@ -2949,20 +3995,20 @@ function buildThreadSummary({
2949
3995
  }
2950
3996
 
2951
3997
  // src/participant/ConversationThread.tsx
2952
- import { jsx as jsx30 } from "react/jsx-runtime";
3998
+ import { jsx as jsx37 } from "react/jsx-runtime";
2953
3999
  function useConversationThreadCore(props) {
2954
4000
  const { perspective = "participant" } = props;
2955
4001
  const { subjectType, subjectId } = props.subject;
2956
- const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2957
- const onMarkedReadRef = useRef13(props.onMarkedRead);
4002
+ const subject = useMemo7(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
4003
+ const onMarkedReadRef = useRef22(props.onMarkedRead);
2958
4004
  onMarkedReadRef.current = props.onMarkedRead;
2959
- const handleMarkedRead = useCallback9((marked) => onMarkedReadRef.current?.(marked), []);
2960
- const api = useMemo6(() => toParticipantApi(props.api), [props.api]);
2961
- const draftsRef = useRef13(/* @__PURE__ */ new Map());
4005
+ const handleMarkedRead = useCallback14((marked) => onMarkedReadRef.current?.(marked), []);
4006
+ const api = useMemo7(() => toParticipantApi(props.api), [props.api]);
4007
+ const draftsRef = useRef22(/* @__PURE__ */ new Map());
2962
4008
  const [sendStates, dispatchSendStates] = useReducer8(participantSendStatesReducer, /* @__PURE__ */ new Map());
2963
- const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
4009
+ const labels = useMemo7(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2964
4010
  const { title, protocol, channels, status } = props;
2965
- const conversation = useMemo6(
4011
+ const conversation = useMemo7(
2966
4012
  () => buildThreadSummary({ subject, title, protocol, channels, status }),
2967
4013
  [subject, title, protocol, channels, status]
2968
4014
  );
@@ -2984,12 +4030,12 @@ function useConversationThreadCore(props) {
2984
4030
  }
2985
4031
  function ConversationThreadBody(props) {
2986
4032
  const core = useConversationThreadCore(props);
2987
- return /* @__PURE__ */ jsx30(
4033
+ return /* @__PURE__ */ jsx37(
2988
4034
  "div",
2989
4035
  {
2990
4036
  className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2991
4037
  style: buildParticipantThemeStyle(props.theme),
2992
- children: /* @__PURE__ */ jsx30("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx30(
4038
+ children: /* @__PURE__ */ jsx37("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx37(
2993
4039
  ParticipantThread,
2994
4040
  {
2995
4041
  ...core,
@@ -3001,17 +4047,21 @@ function ConversationThreadBody(props) {
3001
4047
  renderAuthorAvatar: props.renderAuthorAvatar,
3002
4048
  tail: props.tail,
3003
4049
  counterpartLabel: props.counterpartLabel,
3004
- headerActions: props.headerActions
4050
+ headerActions: props.headerActions,
4051
+ audioRecording: props.audioRecording,
4052
+ renderComposerLock: props.renderComposerLock
3005
4053
  }
3006
4054
  ) })
3007
4055
  }
3008
4056
  );
3009
4057
  }
3010
4058
  function ConversationThread(props) {
3011
- return /* @__PURE__ */ jsx30(ConversationThreadBody, { ...props }, draftKey(props.subject));
4059
+ return /* @__PURE__ */ jsx37(ConversationThreadBody, { ...props }, draftKey(props.subject));
3012
4060
  }
3013
4061
  export {
3014
4062
  ConversationThread,
4063
+ DEFAULT_MAX_RECORDING_BYTES,
4064
+ DEFAULT_PARTICIPANT_AUDIO_LABELS,
3015
4065
  DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
3016
4066
  ParticipantConversations,
3017
4067
  groupParticipantConversations