@exode-team/react-recorder 1.1.7 → 1.1.9
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/dist/components/AudioRecorder.d.ts +1 -1
- package/dist/components/AudioRecorder.d.ts.map +1 -1
- package/dist/components/RecordingShell/RecordingShell.d.ts +1 -0
- package/dist/components/RecordingShell/RecordingShell.d.ts.map +1 -1
- package/dist/components/RecordingShell/RecordingShell.styled.d.ts +0 -2
- package/dist/components/RecordingShell/RecordingShell.styled.d.ts.map +1 -1
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts.map +1 -1
- package/dist/components/WaveformBars/WaveformBars.d.ts +4 -0
- package/dist/components/WaveformBars/WaveformBars.d.ts.map +1 -1
- package/dist/components/WaveformBars/WaveformBars.styled.d.ts +3 -0
- package/dist/components/WaveformBars/WaveformBars.styled.d.ts.map +1 -0
- package/dist/hooks/useAudioRecorderHandlers.d.ts.map +1 -1
- package/dist/hooks/useMediaRecorder.d.ts.map +1 -1
- package/dist/hooks/useRecordingSender.d.ts +2 -1
- package/dist/hooks/useRecordingSender.d.ts.map +1 -1
- package/dist/hooks/useRecordingTiming.d.ts +2 -0
- package/dist/hooks/useRecordingTiming.d.ts.map +1 -1
- package/dist/hooks/useVoiceMessagePlayer.d.ts.map +1 -1
- package/dist/hooks/useVoiceRecorder.d.ts.map +1 -1
- package/dist/hooks/useVoiceRecorderInput.d.ts +1 -1
- package/dist/hooks/useVoiceRecorderInput.d.ts.map +1 -1
- package/dist/index.cjs +149 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +149 -54
- package/dist/index.js.map +1 -1
- package/dist/types/recorder.d.ts +10 -2
- package/dist/types/recorder.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -274,6 +274,9 @@ const useMediaRecorder = (callbacks) => {
|
|
|
274
274
|
if (!mediaRecorderRef.current || !isRecording) {
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
|
+
if (mediaRecorderRef.current.state === 'paused') {
|
|
278
|
+
mediaRecorderRef.current.resume();
|
|
279
|
+
}
|
|
277
280
|
mediaRecorderRef.current.stop();
|
|
278
281
|
}, [isRecording]);
|
|
279
282
|
const cancelRecording = useCallback(() => {
|
|
@@ -432,6 +435,7 @@ const useAudioPlayback = () => {
|
|
|
432
435
|
* @author: exode <hello@exode.ru>
|
|
433
436
|
*/
|
|
434
437
|
const useRecordingTiming = () => {
|
|
438
|
+
const [lastDuration, setLastDuration] = useState(0);
|
|
435
439
|
const [recordingTime, setRecordingTime] = useState(0);
|
|
436
440
|
const timerRef = useRef(null);
|
|
437
441
|
const recordingStartRef = useRef(null);
|
|
@@ -459,6 +463,7 @@ const useRecordingTiming = () => {
|
|
|
459
463
|
const startNewSession = useCallback(() => {
|
|
460
464
|
recordingAccumulatedRef.current = 0;
|
|
461
465
|
setRecordingTime(0);
|
|
466
|
+
setLastDuration(0);
|
|
462
467
|
recordingStartRef.current = performance.now();
|
|
463
468
|
startTicker();
|
|
464
469
|
}, [startTicker]);
|
|
@@ -476,7 +481,21 @@ const useRecordingTiming = () => {
|
|
|
476
481
|
setRecordingTime(roundTo(recordingAccumulatedRef.current));
|
|
477
482
|
stopTicker();
|
|
478
483
|
}, [stopTicker]);
|
|
484
|
+
const getElapsedMs = useCallback(() => {
|
|
485
|
+
const accumulated = recordingAccumulatedRef.current;
|
|
486
|
+
const elapsed = recordingStartRef.current !== null
|
|
487
|
+
? (performance.now() - recordingStartRef.current) / 1000 + accumulated
|
|
488
|
+
: accumulated;
|
|
489
|
+
return Math.round(elapsed * 1000);
|
|
490
|
+
}, []);
|
|
479
491
|
const resetSession = useCallback(() => {
|
|
492
|
+
const accumulated = recordingAccumulatedRef.current;
|
|
493
|
+
const elapsed = recordingStartRef.current !== null
|
|
494
|
+
? (performance.now() - recordingStartRef.current) / 1000 + accumulated
|
|
495
|
+
: accumulated;
|
|
496
|
+
if (elapsed > 0) {
|
|
497
|
+
setLastDuration(roundTo(elapsed));
|
|
498
|
+
}
|
|
480
499
|
stopTicker();
|
|
481
500
|
recordingStartRef.current = null;
|
|
482
501
|
recordingAccumulatedRef.current = 0;
|
|
@@ -488,6 +507,8 @@ const useRecordingTiming = () => {
|
|
|
488
507
|
};
|
|
489
508
|
}, [stopTicker]);
|
|
490
509
|
return {
|
|
510
|
+
lastDuration,
|
|
511
|
+
getElapsedMs,
|
|
491
512
|
pauseSession,
|
|
492
513
|
resetSession,
|
|
493
514
|
recordingTime,
|
|
@@ -589,7 +610,7 @@ const useRecorderVisualization = () => {
|
|
|
589
610
|
* @author: exode <hello@exode.ru>
|
|
590
611
|
*/
|
|
591
612
|
const useRecordingSender = (options) => {
|
|
592
|
-
const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
|
|
613
|
+
const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, getDurationMs, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
|
|
593
614
|
const sendRecording = useCallback(async () => {
|
|
594
615
|
if (!audioBlob) {
|
|
595
616
|
const error = new Error('Voice recording is not available');
|
|
@@ -602,11 +623,13 @@ const useRecordingSender = (options) => {
|
|
|
602
623
|
throw error;
|
|
603
624
|
}
|
|
604
625
|
try {
|
|
626
|
+
const durationMs = getDurationMs();
|
|
627
|
+
const durationSec = durationMs / 1000;
|
|
605
628
|
const file = new File([audioBlob], fileName, {
|
|
606
629
|
type: audioBlob.type || mimeType,
|
|
607
630
|
});
|
|
608
631
|
const uploaded = await uploader(file, { uploadId });
|
|
609
|
-
await onUploaded(uploaded);
|
|
632
|
+
await onUploaded(uploaded, durationSec);
|
|
610
633
|
if (autoResetAfterUpload) {
|
|
611
634
|
deleteRecording();
|
|
612
635
|
}
|
|
@@ -622,8 +645,9 @@ const useRecordingSender = (options) => {
|
|
|
622
645
|
audioBlob,
|
|
623
646
|
mimeType,
|
|
624
647
|
onUploaded,
|
|
625
|
-
|
|
648
|
+
getDurationMs,
|
|
626
649
|
deleteRecording,
|
|
650
|
+
onRecordingError,
|
|
627
651
|
autoResetAfterUpload,
|
|
628
652
|
]);
|
|
629
653
|
return { sendRecording };
|
|
@@ -639,7 +663,7 @@ function useVoiceRecorder(props) {
|
|
|
639
663
|
const [audioBlob, setAudioBlob] = useState(null);
|
|
640
664
|
const { audioURL, isPlaying, mountBlob, playbackTime, stopPlayback, resetPlayback, togglePlayback: _togglePlayback, playbackDuration, } = useAudioPlayback();
|
|
641
665
|
const { audioLevels, previewLevels, resetAudioLevels, stopVisualization, resetPreviewLevels, resumeVisualization, cleanupVisualization, generatePreviewLevels, initializeVisualization, } = useRecorderVisualization();
|
|
642
|
-
const { recordingTime, pauseSession, resetSession, resumeSession, startNewSession, } = useRecordingTiming();
|
|
666
|
+
const { lastDuration, getElapsedMs, recordingTime, pauseSession, resetSession, resumeSession, startNewSession, } = useRecordingTiming();
|
|
643
667
|
const resetCurrentRecording = useCallback((opts) => {
|
|
644
668
|
var _a;
|
|
645
669
|
setAudioBlob(null);
|
|
@@ -667,10 +691,11 @@ function useVoiceRecorder(props) {
|
|
|
667
691
|
mimeType,
|
|
668
692
|
uploadId,
|
|
669
693
|
uploader,
|
|
670
|
-
autoResetAfterUpload,
|
|
671
694
|
onUploaded,
|
|
672
|
-
onRecordingError,
|
|
673
695
|
deleteRecording,
|
|
696
|
+
onRecordingError,
|
|
697
|
+
autoResetAfterUpload,
|
|
698
|
+
getDurationMs: getElapsedMs,
|
|
674
699
|
});
|
|
675
700
|
const mountRecordingBlob = useCallback((blob) => {
|
|
676
701
|
setAudioBlob(blob);
|
|
@@ -753,6 +778,7 @@ function useVoiceRecorder(props) {
|
|
|
753
778
|
isPlaying,
|
|
754
779
|
isRecording,
|
|
755
780
|
audioLevels,
|
|
781
|
+
lastDuration,
|
|
756
782
|
playbackTime,
|
|
757
783
|
recordingTime,
|
|
758
784
|
previewLevels,
|
|
@@ -1017,15 +1043,44 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
1017
1043
|
setIsPlaying(false);
|
|
1018
1044
|
}
|
|
1019
1045
|
}, [isLoading, stopThis, audioRef]);
|
|
1020
|
-
const seekTo = useCallback((
|
|
1046
|
+
const seekTo = useCallback((seekPercent) => {
|
|
1021
1047
|
const audio = audioRef.current;
|
|
1022
1048
|
if (!audio || !isFinite(audio.duration)) {
|
|
1023
1049
|
return;
|
|
1024
1050
|
}
|
|
1025
|
-
const time =
|
|
1051
|
+
const time = seekPercent * audio.duration;
|
|
1026
1052
|
audio.currentTime = time;
|
|
1027
1053
|
setCurrentTime(time);
|
|
1028
1054
|
}, [audioRef]);
|
|
1055
|
+
/** Seek to a specific time in seconds and start playback */
|
|
1056
|
+
const seekToTime = useCallback((seconds) => {
|
|
1057
|
+
const audio = audioRef.current;
|
|
1058
|
+
if (audio && isFinite(audio.duration)) {
|
|
1059
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
1060
|
+
audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));
|
|
1061
|
+
setCurrentTime(audio.currentTime);
|
|
1062
|
+
audio.play()
|
|
1063
|
+
.then(() => setIsPlaying(true))
|
|
1064
|
+
.catch(() => setIsPlaying(false));
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
void ensureAudioLoaded().then(() => {
|
|
1068
|
+
const loadedAudio = audioRef.current;
|
|
1069
|
+
if (loadedAudio) {
|
|
1070
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
1071
|
+
loadedAudio.currentTime = Math.max(0, Math.min(seconds, loadedAudio.duration || seconds));
|
|
1072
|
+
setCurrentTime(loadedAudio.currentTime);
|
|
1073
|
+
loadedAudio.play()
|
|
1074
|
+
.then(() => setIsPlaying(true))
|
|
1075
|
+
.catch(() => setIsPlaying(false));
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}, [
|
|
1080
|
+
audioRef,
|
|
1081
|
+
stopThis,
|
|
1082
|
+
ensureAudioLoaded,
|
|
1083
|
+
]);
|
|
1029
1084
|
/** Sync external duration */
|
|
1030
1085
|
useEffect(() => {
|
|
1031
1086
|
if (externalDuration) {
|
|
@@ -1042,6 +1097,7 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
1042
1097
|
currentTime,
|
|
1043
1098
|
waveformLevels,
|
|
1044
1099
|
seekTo,
|
|
1100
|
+
seekToTime,
|
|
1045
1101
|
prefetch,
|
|
1046
1102
|
preload,
|
|
1047
1103
|
formatTime,
|
|
@@ -1099,9 +1155,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1099
1155
|
toggleRecordingPause,
|
|
1100
1156
|
]);
|
|
1101
1157
|
const handleSend = useCallback(async () => {
|
|
1102
|
-
if (isSending) {
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
1158
|
shouldSendAfterStopRef.current = false;
|
|
1106
1159
|
setIsSending(true);
|
|
1107
1160
|
try {
|
|
@@ -1118,7 +1171,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1118
1171
|
setIsSending(false);
|
|
1119
1172
|
}
|
|
1120
1173
|
}, [
|
|
1121
|
-
isSending,
|
|
1122
1174
|
stopPlayback,
|
|
1123
1175
|
onUploadError,
|
|
1124
1176
|
sendRecording,
|
|
@@ -1129,6 +1181,7 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1129
1181
|
return;
|
|
1130
1182
|
}
|
|
1131
1183
|
if (isRecording) {
|
|
1184
|
+
setIsSending(true);
|
|
1132
1185
|
shouldSendAfterStopRef.current = true;
|
|
1133
1186
|
stopRecording();
|
|
1134
1187
|
return;
|
|
@@ -1266,6 +1319,44 @@ const IdleControl = (props) => {
|
|
|
1266
1319
|
return (jsxs(IdleWrapper, { children: [jsxs(IdleButton, { type: "button", onClick: onClick, "aria-label": startAriaLabel, children: [jsx(IdleButtonOutline, { "aria-hidden": true }), jsx(IdleButtonGlow, { "aria-hidden": true }), jsx(Icon32Microphone, { height: 48, width: 48 })] }), jsx(IdleInstruction, { children: instruction })] }));
|
|
1267
1320
|
};
|
|
1268
1321
|
|
|
1322
|
+
const Bars = styled.div `
|
|
1323
|
+
position: absolute;
|
|
1324
|
+
inset: 0;
|
|
1325
|
+
display: flex;
|
|
1326
|
+
align-items: flex-end;
|
|
1327
|
+
gap: 3px;
|
|
1328
|
+
`;
|
|
1329
|
+
const Bar = styled.div `
|
|
1330
|
+
flex: 1;
|
|
1331
|
+
min-width: 2px;
|
|
1332
|
+
border-radius: 9999px;
|
|
1333
|
+
background: var(--white, #ffffff);
|
|
1334
|
+
opacity: 0.6;
|
|
1335
|
+
transition: height 120ms ease, opacity 120ms ease;
|
|
1336
|
+
`;
|
|
1337
|
+
|
|
1338
|
+
const WaveformBars = (props) => {
|
|
1339
|
+
const { values, progress, styleInterpolator, scale = 40, minHeight = 6, divider = 100, } = props;
|
|
1340
|
+
return (jsx(Bars, { children: values.map((value, index) => {
|
|
1341
|
+
const normalized = clampValue(value);
|
|
1342
|
+
const height = Math.max(minHeight, (normalized / divider) * scale);
|
|
1343
|
+
const barProgress = values.length > 1 ? index / (values.length - 1) : 0;
|
|
1344
|
+
const isPlayed = (progress !== null && progress !== void 0 ? progress : 0) > 0 && barProgress <= (progress !== null && progress !== void 0 ? progress : 0);
|
|
1345
|
+
const interpolated = styleInterpolator === null || styleInterpolator === void 0 ? void 0 : styleInterpolator({
|
|
1346
|
+
normalized,
|
|
1347
|
+
height,
|
|
1348
|
+
index,
|
|
1349
|
+
progress: progress !== null && progress !== void 0 ? progress : 0,
|
|
1350
|
+
barProgress,
|
|
1351
|
+
isPlayed,
|
|
1352
|
+
});
|
|
1353
|
+
return (jsx(Bar, { style: {
|
|
1354
|
+
height: `${height}px`,
|
|
1355
|
+
...interpolated,
|
|
1356
|
+
} }, index));
|
|
1357
|
+
}) }));
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1269
1360
|
/**
|
|
1270
1361
|
* RecordingShell styles
|
|
1271
1362
|
*
|
|
@@ -1360,21 +1451,6 @@ const Waveform = styled.div `
|
|
|
1360
1451
|
align-items: center;
|
|
1361
1452
|
overflow: hidden;
|
|
1362
1453
|
`;
|
|
1363
|
-
const Bars = styled.div `
|
|
1364
|
-
position: absolute;
|
|
1365
|
-
inset: 0;
|
|
1366
|
-
display: flex;
|
|
1367
|
-
align-items: flex-end;
|
|
1368
|
-
gap: 3px;
|
|
1369
|
-
`;
|
|
1370
|
-
const Bar = styled.div `
|
|
1371
|
-
flex: 1;
|
|
1372
|
-
min-width: 2px;
|
|
1373
|
-
border-radius: 9999px;
|
|
1374
|
-
background: var(--white, #ffffff);
|
|
1375
|
-
opacity: 0.6;
|
|
1376
|
-
transition: height 120ms ease, opacity 120ms ease;
|
|
1377
|
-
`;
|
|
1378
1454
|
const Timer$1 = styled.span `
|
|
1379
1455
|
text-align: right;
|
|
1380
1456
|
font-size: 13px;
|
|
@@ -1436,31 +1512,33 @@ const Spinner = styled.span `
|
|
|
1436
1512
|
}
|
|
1437
1513
|
`;
|
|
1438
1514
|
|
|
1439
|
-
const WaveformBars = (props) => {
|
|
1440
|
-
const { values, styleInterpolator, scale = 40, minHeight = 6, divider = 100, } = props;
|
|
1441
|
-
return (jsx(Bars, { children: values.map((value, index) => {
|
|
1442
|
-
const normalized = clampValue(value);
|
|
1443
|
-
const height = Math.max(minHeight, (normalized / divider) * scale);
|
|
1444
|
-
const interpolated = styleInterpolator === null || styleInterpolator === void 0 ? void 0 : styleInterpolator({ normalized, height, index });
|
|
1445
|
-
return (jsx(Bar, { style: {
|
|
1446
|
-
height: `${height}px`,
|
|
1447
|
-
...interpolated,
|
|
1448
|
-
} }, index));
|
|
1449
|
-
}) }));
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
1515
|
const RecordingShell = (props) => {
|
|
1453
1516
|
const { isPaused, isPlaying, isSending, isRecording, audioLevels, hasRecording, playbackTime, previewLevels, recordingTime, stopAriaLabel, playAriaLabel, sendAriaLabel, startAriaLabel, cancelAriaLabel, playbackDuration, pausePlaybackAriaLabel, pauseRecordingAriaLabel, resumeRecordingAriaLabel, onSend, onCancel, formatTime, onRecordToggle, onTogglePlayback, } = props;
|
|
1517
|
+
const { lastDuration } = props;
|
|
1518
|
+
const isPreview = !isRecording || isPaused;
|
|
1454
1519
|
const waveformValues = isRecording ? audioLevels : (hasRecording ? previewLevels : audioLevels);
|
|
1520
|
+
const effectiveDuration = recordingTime
|
|
1521
|
+
|| playbackDuration
|
|
1522
|
+
|| lastDuration
|
|
1523
|
+
|| 1;
|
|
1524
|
+
const progress = (isPreview && isPlaying)
|
|
1525
|
+
? Math.min(1, playbackTime / effectiveDuration)
|
|
1526
|
+
: 0;
|
|
1455
1527
|
const displayTime = useMemo(() => {
|
|
1456
|
-
if (
|
|
1528
|
+
if (isPlaying) {
|
|
1529
|
+
return formatTime(playbackTime);
|
|
1530
|
+
}
|
|
1531
|
+
if (isRecording && !isPaused) {
|
|
1532
|
+
return formatTime(recordingTime);
|
|
1533
|
+
}
|
|
1534
|
+
if (isRecording && isPaused) {
|
|
1457
1535
|
return formatTime(recordingTime);
|
|
1458
1536
|
}
|
|
1537
|
+
if (lastDuration > 0) {
|
|
1538
|
+
return formatTime(lastDuration);
|
|
1539
|
+
}
|
|
1459
1540
|
if (hasRecording) {
|
|
1460
|
-
if (
|
|
1461
|
-
return formatTime(playbackTime);
|
|
1462
|
-
}
|
|
1463
|
-
if (playbackDuration > 0) {
|
|
1541
|
+
if (playbackDuration > 0.5) {
|
|
1464
1542
|
return formatTime(playbackDuration);
|
|
1465
1543
|
}
|
|
1466
1544
|
if (recordingTime > 0) {
|
|
@@ -1468,7 +1546,17 @@ const RecordingShell = (props) => {
|
|
|
1468
1546
|
}
|
|
1469
1547
|
}
|
|
1470
1548
|
return formatTime(0);
|
|
1471
|
-
}, [
|
|
1549
|
+
}, [
|
|
1550
|
+
isPaused,
|
|
1551
|
+
isPlaying,
|
|
1552
|
+
formatTime,
|
|
1553
|
+
isRecording,
|
|
1554
|
+
lastDuration,
|
|
1555
|
+
hasRecording,
|
|
1556
|
+
playbackTime,
|
|
1557
|
+
recordingTime,
|
|
1558
|
+
playbackDuration,
|
|
1559
|
+
]);
|
|
1472
1560
|
const recordButtonDisabled = isSending;
|
|
1473
1561
|
const recordButtonIcon = !isRecording || isPaused
|
|
1474
1562
|
? jsx(Icon16Microphone, { width: 24, height: 24 })
|
|
@@ -1486,10 +1574,13 @@ const RecordingShell = (props) => {
|
|
|
1486
1574
|
: jsx(Icon16Play, { width: 24, height: 24 });
|
|
1487
1575
|
const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
|
|
1488
1576
|
const sendDisabled = isSending || (!hasRecording && !isRecording);
|
|
1489
|
-
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, {
|
|
1577
|
+
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { scale: 42, minHeight: 6, divider: 100, progress: progress, values: waveformValues, styleInterpolator: ({ normalized, isPlayed, progress: p }) => ({
|
|
1490
1578
|
height: `${12 + normalized / 2}px`,
|
|
1491
1579
|
maxHeight: '34px',
|
|
1492
|
-
opacity:
|
|
1580
|
+
opacity: p > 0
|
|
1581
|
+
? (isPlayed ? 0.80 : 0.22)
|
|
1582
|
+
: 0.38 + Math.min(0.5, normalized / 120),
|
|
1583
|
+
transition: 'opacity 80ms ease',
|
|
1493
1584
|
}) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 24, height: 24 })) })] }));
|
|
1494
1585
|
};
|
|
1495
1586
|
|
|
@@ -1572,9 +1663,9 @@ const AudioRecorder = (props) => {
|
|
|
1572
1663
|
messages,
|
|
1573
1664
|
onUploadError,
|
|
1574
1665
|
});
|
|
1575
|
-
const { isPaused, isPlaying, formatTime, audioLevels, isRecording, playbackTime, hasRecording, previewLevels, recordingTime, playbackDuration, } = recorder;
|
|
1576
|
-
const showRecorderBar = isRecording || hasRecording;
|
|
1577
|
-
return (jsx(Container, { className: className, children: showRecorderBar ? (jsx(RecordingShell, { isPaused: isPaused, isSending: isSending, isPlaying: isPlaying, formatTime: formatTime, onCancel: handleCancel, isRecording: isRecording, audioLevels: audioLevels, onSend: handleSendRequest, playbackTime: playbackTime, hasRecording: hasRecording, previewLevels: previewLevels, recordingTime: recordingTime, playbackDuration: playbackDuration, onRecordToggle: handleRecordToggle, sendAriaLabel: messages.buttons.send, onTogglePlayback: handlePlaybackToggle, cancelAriaLabel: messages.buttons.cancel, playAriaLabel: messages.aria.playRecording, stopAriaLabel: messages.aria.stopRecording, startAriaLabel: messages.aria.startRecording, pauseRecordingAriaLabel: messages.aria.pauseRecording, resumeRecordingAriaLabel: messages.aria.resumeRecording, pausePlaybackAriaLabel: messages.aria.pausePlayback })) : (jsx(IdleControl, { onClick: handleRecordToggle, startAriaLabel: messages.aria.startRecording, instruction: messages.instruction })) }));
|
|
1666
|
+
const { isPaused, isPlaying, formatTime, audioLevels, isRecording, lastDuration, playbackTime, hasRecording, previewLevels, recordingTime, playbackDuration, } = recorder;
|
|
1667
|
+
const showRecorderBar = isRecording || hasRecording || isSending;
|
|
1668
|
+
return (jsx(Container, { className: className, children: showRecorderBar ? (jsx(RecordingShell, { isPaused: isPaused, isSending: isSending, isPlaying: isPlaying, formatTime: formatTime, onCancel: handleCancel, isRecording: isRecording, audioLevels: audioLevels, onSend: handleSendRequest, playbackTime: playbackTime, lastDuration: lastDuration, hasRecording: hasRecording, previewLevels: previewLevels, recordingTime: recordingTime, playbackDuration: playbackDuration, onRecordToggle: handleRecordToggle, sendAriaLabel: messages.buttons.send, onTogglePlayback: handlePlaybackToggle, cancelAriaLabel: messages.buttons.cancel, playAriaLabel: messages.aria.playRecording, stopAriaLabel: messages.aria.stopRecording, startAriaLabel: messages.aria.startRecording, pauseRecordingAriaLabel: messages.aria.pauseRecording, resumeRecordingAriaLabel: messages.aria.resumeRecording, pausePlaybackAriaLabel: messages.aria.pausePlayback })) : (jsx(IdleControl, { onClick: handleRecordToggle, startAriaLabel: messages.aria.startRecording, instruction: messages.instruction })) }));
|
|
1578
1669
|
};
|
|
1579
1670
|
|
|
1580
1671
|
/**
|
|
@@ -2152,14 +2243,18 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2152
2243
|
VoicePlaybackChain.playNext(chainId);
|
|
2153
2244
|
}
|
|
2154
2245
|
}, [onEnded, chainId]);
|
|
2155
|
-
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2246
|
+
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2156
2247
|
src,
|
|
2157
2248
|
duration,
|
|
2158
2249
|
waveformData,
|
|
2159
2250
|
preloadedBlob: props.preloadedBlob,
|
|
2160
2251
|
onEnded: handleEnded,
|
|
2161
2252
|
});
|
|
2162
|
-
useImperativeHandle(ref, () => ({ play, prefetch }), [
|
|
2253
|
+
useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
|
|
2254
|
+
play,
|
|
2255
|
+
prefetch,
|
|
2256
|
+
seekToTime,
|
|
2257
|
+
]);
|
|
2163
2258
|
/** Auto-preload audio when player enters viewport */
|
|
2164
2259
|
useEffect(() => {
|
|
2165
2260
|
if (!autoPreload || isLoaded || isLoading) {
|
|
@@ -2213,7 +2308,7 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2213
2308
|
observer.disconnect();
|
|
2214
2309
|
};
|
|
2215
2310
|
}, []);
|
|
2216
|
-
const displayTime = isPlaying
|
|
2311
|
+
const displayTime = (isPlaying || currentTime > 0)
|
|
2217
2312
|
? formatTime(currentTime)
|
|
2218
2313
|
: formatTime(resolvedDuration);
|
|
2219
2314
|
const handleTrackClick = useCallback((e) => {
|