@exode-team/react-recorder 1.1.1 → 1.1.3
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 +2 -0
- package/dist/components/AudioRecorder.styled.d.ts.map +1 -1
- package/dist/components/RecordingShell/RecordingShell.d.ts.map +1 -1
- 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/VoiceRecorderInput/PreviewBars.d.ts +1 -1
- package/dist/components/VoiceRecorderInput/PreviewBars.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/TimelineBars.d.ts +1 -1
- package/dist/components/VoiceRecorderInput/TimelineBars.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.d.ts.map +1 -1
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.d.ts.map +1 -1
- package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
- package/dist/hooks/useVoiceMessagePlayer.d.ts.map +1 -1
- package/dist/index.cjs +112 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +114 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import React, { useState, useRef, useCallback, useEffect, useMemo,
|
|
2
|
+
import React, { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useImperativeHandle } from 'react';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
|
-
import { Icon32Microphone, Icon16Microphone,
|
|
4
|
+
import { Icon32Microphone, Icon16Microphone, Icon16Pause, Icon16Play, Icon28DeleteOutlineAndroid, Icon28SendOutline, Icon20Voice, Icon24Pause, Icon24Play } from '@vkontakte/icons';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Math utilities
|
|
@@ -782,6 +782,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
782
782
|
const audioRef = useRef(null);
|
|
783
783
|
const objectUrlRef = useRef(null);
|
|
784
784
|
const abortControllerRef = useRef(null);
|
|
785
|
+
const failedSrcRef = useRef(null);
|
|
785
786
|
const callbacksRef = useRef(playbackCallbacks);
|
|
786
787
|
callbacksRef.current = playbackCallbacks;
|
|
787
788
|
const createAudioFromBlob = useCallback((blob) => {
|
|
@@ -818,7 +819,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
818
819
|
}, [waveformData, createAudioFromBlob]);
|
|
819
820
|
const ensureAudioLoaded = useCallback(async () => {
|
|
820
821
|
var _a;
|
|
821
|
-
if (audioRef.current) {
|
|
822
|
+
if (audioRef.current || failedSrcRef.current === src) {
|
|
822
823
|
return;
|
|
823
824
|
}
|
|
824
825
|
if (preloadedBlob) {
|
|
@@ -849,6 +850,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
849
850
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
850
851
|
return;
|
|
851
852
|
}
|
|
853
|
+
failedSrcRef.current = src;
|
|
852
854
|
console.warn('[voice-player] Failed to load audio', error);
|
|
853
855
|
}
|
|
854
856
|
finally {
|
|
@@ -859,6 +861,7 @@ const useAudioBlobLoader = (options) => {
|
|
|
859
861
|
}, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
|
|
860
862
|
/** Cleanup on src change or unmount */
|
|
861
863
|
useEffect(() => {
|
|
864
|
+
failedSrcRef.current = null;
|
|
862
865
|
return () => {
|
|
863
866
|
var _a;
|
|
864
867
|
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -960,6 +963,13 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
960
963
|
},
|
|
961
964
|
},
|
|
962
965
|
});
|
|
966
|
+
/** Pre-fetch audio without playing (for viewport preloading) */
|
|
967
|
+
const prefetch = useCallback(() => {
|
|
968
|
+
if (isLoading || audioRef.current) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
void ensureAudioLoaded();
|
|
972
|
+
}, [isLoading, ensureAudioLoaded]);
|
|
963
973
|
/** Load audio and auto-play after loaded */
|
|
964
974
|
const preload = useCallback(() => {
|
|
965
975
|
if (isLoading || audioRef.current) {
|
|
@@ -1030,6 +1040,7 @@ const useVoiceMessagePlayer = (options) => {
|
|
|
1030
1040
|
currentTime,
|
|
1031
1041
|
waveformLevels,
|
|
1032
1042
|
seekTo,
|
|
1043
|
+
prefetch,
|
|
1033
1044
|
preload,
|
|
1034
1045
|
formatTime,
|
|
1035
1046
|
togglePlayback,
|
|
@@ -1180,7 +1191,6 @@ const useAudioRecorderHandlers = (options) => {
|
|
|
1180
1191
|
const Container = styled.div `
|
|
1181
1192
|
position: relative;
|
|
1182
1193
|
width: 100%;
|
|
1183
|
-
padding: 16px;
|
|
1184
1194
|
transition: color 200ms ease, background-color 200ms ease;
|
|
1185
1195
|
color: var(--text_primary, #0a0a0a);
|
|
1186
1196
|
`;
|
|
@@ -1265,29 +1275,28 @@ const Wrapper$1 = styled.div `
|
|
|
1265
1275
|
gap: 12px;
|
|
1266
1276
|
padding: 10px 14px;
|
|
1267
1277
|
border-radius: 9999px;
|
|
1268
|
-
background: var(--
|
|
1269
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
|
|
1278
|
+
background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
|
|
1270
1279
|
`;
|
|
1271
1280
|
const CancelButton$1 = styled.button `
|
|
1272
|
-
width:
|
|
1273
|
-
height:
|
|
1281
|
+
width: 38px;
|
|
1282
|
+
height: 38px;
|
|
1274
1283
|
display: inline-flex;
|
|
1275
1284
|
align-items: center;
|
|
1276
1285
|
justify-content: center;
|
|
1277
|
-
border-radius:
|
|
1278
|
-
background: transparent;
|
|
1286
|
+
border-radius: 19px;
|
|
1287
|
+
background: color-mix(in srgb, var(--exode_red) 10%, transparent);
|
|
1279
1288
|
border: 0;
|
|
1289
|
+
flex-shrink: 0;
|
|
1280
1290
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
|
|
1281
1291
|
cursor: pointer;
|
|
1282
|
-
transition:
|
|
1292
|
+
transition: transform 160ms ease;
|
|
1283
1293
|
|
|
1284
|
-
&:hover {
|
|
1285
|
-
|
|
1286
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
|
|
1294
|
+
&:hover:not(:disabled) {
|
|
1295
|
+
transform: translateY(-1px);
|
|
1287
1296
|
}
|
|
1288
1297
|
|
|
1289
|
-
&:active {
|
|
1290
|
-
|
|
1298
|
+
&:active:not(:disabled) {
|
|
1299
|
+
transform: translateY(0);
|
|
1291
1300
|
}
|
|
1292
1301
|
`;
|
|
1293
1302
|
const Track = styled.div `
|
|
@@ -1295,7 +1304,7 @@ const Track = styled.div `
|
|
|
1295
1304
|
display: flex;
|
|
1296
1305
|
align-items: center;
|
|
1297
1306
|
gap: 12px;
|
|
1298
|
-
padding: 8px
|
|
1307
|
+
padding: 8px 10px;
|
|
1299
1308
|
border-radius: 9999px;
|
|
1300
1309
|
background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
|
|
1301
1310
|
`;
|
|
@@ -1459,7 +1468,9 @@ const RecordingShell = (props) => {
|
|
|
1459
1468
|
return formatTime(0);
|
|
1460
1469
|
}, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
|
|
1461
1470
|
const recordButtonDisabled = isSending;
|
|
1462
|
-
const recordButtonIcon = !isRecording || isPaused
|
|
1471
|
+
const recordButtonIcon = !isRecording || isPaused
|
|
1472
|
+
? jsx(Icon16Microphone, { width: 24, height: 24 })
|
|
1473
|
+
: jsx(Icon16Pause, { width: 24, height: 24 });
|
|
1463
1474
|
const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
|
|
1464
1475
|
const recordButtonAria = !isRecording
|
|
1465
1476
|
? startAriaLabel
|
|
@@ -1468,14 +1479,16 @@ const RecordingShell = (props) => {
|
|
|
1468
1479
|
: pauseRecordingAriaLabel || stopAriaLabel;
|
|
1469
1480
|
const canShowPlayback = !isRecording || isPaused;
|
|
1470
1481
|
const playbackDisabled = !hasRecording && !isRecording;
|
|
1471
|
-
const playbackIcon = isPlaying
|
|
1482
|
+
const playbackIcon = isPlaying
|
|
1483
|
+
? jsx(Icon16Pause, { width: 24, height: 24 })
|
|
1484
|
+
: jsx(Icon16Play, { width: 24, height: 24 });
|
|
1472
1485
|
const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
|
|
1473
1486
|
const sendDisabled = isSending || (!hasRecording && !isRecording);
|
|
1474
|
-
return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(
|
|
1487
|
+
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, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
|
|
1475
1488
|
height: `${12 + normalized / 2}px`,
|
|
1476
1489
|
maxHeight: '34px',
|
|
1477
1490
|
opacity: 0.38 + Math.min(0.5, normalized / 120),
|
|
1478
|
-
}) }) }), 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:
|
|
1491
|
+
}) }) }), 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 })) })] }));
|
|
1479
1492
|
};
|
|
1480
1493
|
|
|
1481
1494
|
/**
|
|
@@ -1735,8 +1748,8 @@ const useVoiceRecorderInput = (options) => {
|
|
|
1735
1748
|
|
|
1736
1749
|
const PreviewBars = (props) => {
|
|
1737
1750
|
var _a, _b;
|
|
1738
|
-
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels,
|
|
1739
|
-
const maxBars =
|
|
1751
|
+
const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
|
|
1752
|
+
const maxBars = visibleBarsCount;
|
|
1740
1753
|
const sourceLevels = timelineLevels.length > 0
|
|
1741
1754
|
? timelineLevels
|
|
1742
1755
|
: previewLevels;
|
|
@@ -1780,8 +1793,8 @@ const PreviewBars = (props) => {
|
|
|
1780
1793
|
};
|
|
1781
1794
|
|
|
1782
1795
|
const TimelineBars = (props) => {
|
|
1783
|
-
const { timelineLevels,
|
|
1784
|
-
const maxBars =
|
|
1796
|
+
const { timelineLevels, visibleBarsCount } = props;
|
|
1797
|
+
const maxBars = visibleBarsCount;
|
|
1785
1798
|
const visible = timelineLevels.slice(-maxBars);
|
|
1786
1799
|
const padded = [
|
|
1787
1800
|
...visible,
|
|
@@ -1826,15 +1839,11 @@ const CancelButton = styled.button `
|
|
|
1826
1839
|
border-radius: 16px;
|
|
1827
1840
|
background: transparent;
|
|
1828
1841
|
border: 0;
|
|
1842
|
+
margin-right: 4px;
|
|
1829
1843
|
flex-shrink: 0;
|
|
1830
1844
|
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
|
|
1831
1845
|
cursor: pointer;
|
|
1832
1846
|
transition: color 160ms ease, background-color 160ms ease;
|
|
1833
|
-
|
|
1834
|
-
&:hover {
|
|
1835
|
-
background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
|
|
1836
|
-
color: color-mix(in srgb, var(--text_primary, #0a0a0a) 72%, transparent);
|
|
1837
|
-
}
|
|
1838
1847
|
`;
|
|
1839
1848
|
const RecordTrack = styled.div `
|
|
1840
1849
|
flex: 1;
|
|
@@ -1850,7 +1859,8 @@ const RecordTrack = styled.div `
|
|
|
1850
1859
|
const ButtonSlot = styled.div `
|
|
1851
1860
|
display: flex;
|
|
1852
1861
|
align-items: center;
|
|
1853
|
-
gap:
|
|
1862
|
+
gap: 4px;
|
|
1863
|
+
padding: 2px;
|
|
1854
1864
|
flex-shrink: 0;
|
|
1855
1865
|
`;
|
|
1856
1866
|
const StopButton = styled.button `
|
|
@@ -1987,19 +1997,39 @@ const VoiceRecorderInput = (props) => {
|
|
|
1987
1997
|
const waveformRef = useRef(null);
|
|
1988
1998
|
const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
|
|
1989
1999
|
const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
|
|
1990
|
-
const
|
|
2000
|
+
const [visibleBarsCount, setVisibleBarsCount] = useState(0);
|
|
2001
|
+
const measureBars = () => {
|
|
1991
2002
|
const el = waveformRef.current;
|
|
1992
2003
|
if (!el) {
|
|
1993
|
-
return
|
|
2004
|
+
return;
|
|
1994
2005
|
}
|
|
1995
|
-
|
|
2006
|
+
setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
|
|
2007
|
+
};
|
|
2008
|
+
/** Sync recalc before paint — catches layout shifts from button changes */
|
|
2009
|
+
useLayoutEffect(measureBars);
|
|
2010
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2011
|
+
useEffect(() => {
|
|
2012
|
+
const el = waveformRef.current;
|
|
2013
|
+
if (!el) {
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
let rafId = 0;
|
|
2017
|
+
const observer = new ResizeObserver(() => {
|
|
2018
|
+
cancelAnimationFrame(rafId);
|
|
2019
|
+
rafId = requestAnimationFrame(measureBars);
|
|
2020
|
+
});
|
|
2021
|
+
observer.observe(el);
|
|
2022
|
+
return () => {
|
|
2023
|
+
cancelAnimationFrame(rafId);
|
|
2024
|
+
observer.disconnect();
|
|
2025
|
+
};
|
|
1996
2026
|
}, []);
|
|
1997
2027
|
const displayTime = (isPreview && isPlaying)
|
|
1998
2028
|
? formatTime(playbackTime)
|
|
1999
2029
|
: formatTime(recordingTime || lastDurationRef.current);
|
|
2000
|
-
return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(
|
|
2030
|
+
return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 28, height: 28, style: { minWidth: 28 } }) }), jsxs(RecordTrack, { children: [jsx(ButtonSlot, { children: isPreview ? (jsxs(Fragment, { children: [jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsx(Icon20Voice, { width: 14, height: 14 }) }), jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
|
|
2001
2031
|
? jsx(Icon16Pause, { width: 14, height: 14 })
|
|
2002
|
-
: jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(
|
|
2032
|
+
: jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(Icon16Pause, { width: 14, height: 14 }) })) }), jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, visibleBarsCount: visibleBarsCount })) : (jsx(TimelineBars, { timelineLevels: timelineLevels, visibleBarsCount: visibleBarsCount })) }), jsx(Timer, { children: displayTime })] }), jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsx(SpinnerIcon, {})) : (jsx(Icon28SendOutline, {})) })] }));
|
|
2003
2033
|
};
|
|
2004
2034
|
|
|
2005
2035
|
/**
|
|
@@ -2128,8 +2158,9 @@ const BAR_GAP = 2;
|
|
|
2128
2158
|
const BAR_MIN_HEIGHT = 2;
|
|
2129
2159
|
const BAR_MAX_HEIGHT = 24;
|
|
2130
2160
|
const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
2131
|
-
const { src, onClick, onEnded, chainId, duration, className, waveformData, } = props;
|
|
2161
|
+
const { src, onClick, onEnded, chainId, duration, className, waveformData, autoPreload = true, } = props;
|
|
2132
2162
|
const trackRef = useRef(null);
|
|
2163
|
+
const wrapperRef = useRef(null);
|
|
2133
2164
|
const [trackWidth, setTrackWidth] = useState(0);
|
|
2134
2165
|
const handleEnded = useCallback(() => {
|
|
2135
2166
|
onEnded === null || onEnded === void 0 ? void 0 : onEnded();
|
|
@@ -2137,25 +2168,66 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2137
2168
|
VoicePlaybackChain.playNext(chainId);
|
|
2138
2169
|
}
|
|
2139
2170
|
}, [onEnded, chainId]);
|
|
2140
|
-
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2171
|
+
const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
|
|
2141
2172
|
src,
|
|
2142
2173
|
duration,
|
|
2143
2174
|
waveformData,
|
|
2144
2175
|
preloadedBlob: props.preloadedBlob,
|
|
2145
2176
|
onEnded: handleEnded,
|
|
2146
2177
|
});
|
|
2147
|
-
useImperativeHandle(ref, () => ({ play }), [play]);
|
|
2178
|
+
useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
|
|
2179
|
+
/** Auto-preload audio when player enters viewport */
|
|
2180
|
+
useEffect(() => {
|
|
2181
|
+
if (!autoPreload || isLoaded || isLoading) {
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const el = wrapperRef.current;
|
|
2185
|
+
if (!el) {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
2189
|
+
if (entry.isIntersecting) {
|
|
2190
|
+
prefetch();
|
|
2191
|
+
observer.disconnect();
|
|
2192
|
+
}
|
|
2193
|
+
}, { threshold: 0.1 });
|
|
2194
|
+
observer.observe(el);
|
|
2195
|
+
return () => observer.disconnect();
|
|
2196
|
+
}, [
|
|
2197
|
+
isLoaded,
|
|
2198
|
+
prefetch,
|
|
2199
|
+
isLoading,
|
|
2200
|
+
autoPreload,
|
|
2201
|
+
]);
|
|
2148
2202
|
/** Register in playback chain for sequential playback */
|
|
2149
2203
|
useEffect(() => {
|
|
2150
2204
|
if (chainId != null) {
|
|
2151
2205
|
return VoicePlaybackChain.register(chainId, () => play());
|
|
2152
2206
|
}
|
|
2153
2207
|
}, [chainId, play]);
|
|
2154
|
-
|
|
2155
|
-
useLayoutEffect(() => {
|
|
2208
|
+
const measureTrackWidth = () => {
|
|
2156
2209
|
if (trackRef.current) {
|
|
2157
2210
|
setTrackWidth(trackRef.current.clientWidth);
|
|
2158
2211
|
}
|
|
2212
|
+
};
|
|
2213
|
+
/** Sync recalc before paint — catches layout shifts */
|
|
2214
|
+
useLayoutEffect(measureTrackWidth);
|
|
2215
|
+
/** ResizeObserver for external size changes (window resize, container resize) */
|
|
2216
|
+
useEffect(() => {
|
|
2217
|
+
const el = trackRef.current;
|
|
2218
|
+
if (!el) {
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
let rafId = 0;
|
|
2222
|
+
const observer = new ResizeObserver(() => {
|
|
2223
|
+
cancelAnimationFrame(rafId);
|
|
2224
|
+
rafId = requestAnimationFrame(measureTrackWidth);
|
|
2225
|
+
});
|
|
2226
|
+
observer.observe(el);
|
|
2227
|
+
return () => {
|
|
2228
|
+
cancelAnimationFrame(rafId);
|
|
2229
|
+
observer.disconnect();
|
|
2230
|
+
};
|
|
2159
2231
|
}, []);
|
|
2160
2232
|
const displayTime = isPlaying
|
|
2161
2233
|
? formatTime(currentTime)
|
|
@@ -2191,7 +2263,7 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
|
|
|
2191
2263
|
const peak = Math.max(...raw, 1);
|
|
2192
2264
|
return raw.map((v) => (v / peak) * 100);
|
|
2193
2265
|
}, [waveformLevels, trackWidth]);
|
|
2194
|
-
return (jsxs(PlayerWrapper, { onClick: onClick, className: className, children: [jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsx(LoadingSpinner, {})) : isPlaying ? (jsx(Icon24Pause, {})) : (jsx(Icon24Play, {})) }), jsxs(ContentColumn, { children: [jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
|
|
2266
|
+
return (jsxs(PlayerWrapper, { ref: wrapperRef, onClick: onClick, className: className, children: [jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsx(LoadingSpinner, {})) : isPlaying ? (jsx(Icon24Pause, {})) : (jsx(Icon24Play, {})) }), jsxs(ContentColumn, { children: [jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
|
|
2195
2267
|
const normalized = Math.min(100, level);
|
|
2196
2268
|
const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);
|
|
2197
2269
|
const barProgress = interpolatedBars.length > 0
|