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