Skip to content

Commit aed0360

Browse files
authored
fix: adapt audio recording wave form to the available space (#2435)
1 parent 741e9ce commit aed0360

File tree

8 files changed

+233
-55
lines changed

8 files changed

+233
-55
lines changed

src/components/Attachment/__tests__/VoiceRecording.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
55
import { generateVoiceRecordingAttachment } from '../../../mock-builders';
66
import { VoiceRecording, VoiceRecordingPlayer } from '../VoiceRecording';
77
import { ChannelActionProvider } from '../../../context';
8+
import { ResizeObserverMock } from '../../../mock-builders/browser';
89

910
const AUDIO_RECORDING_PLAYER_TEST_ID = 'voice-recording-widget';
1011
const QUOTED_AUDIO_RECORDING_TEST_ID = 'quoted-voice-recording-widget';
@@ -13,6 +14,10 @@ const FALLBACK_TITLE = 'Voice message';
1314

1415
const attachment = generateVoiceRecordingAttachment();
1516

17+
window.ResizeObserver = ResizeObserverMock;
18+
19+
jest.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 120 });
20+
1621
const clickPlay = async () => {
1722
await act(async () => {
1823
await fireEvent.click(screen.queryByTestId('play-audio'));

src/components/Attachment/__tests__/WaveProgressBar.test.js

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,113 @@
11
import React from 'react';
2-
import { render, screen } from '@testing-library/react';
2+
import { act, render, screen } from '@testing-library/react';
33
import '@testing-library/jest-dom';
44
import { WaveProgressBar } from '../components';
5+
import { ResizeObserverMock } from '../../../mock-builders/browser';
56

67
jest.spyOn(console, 'warn').mockImplementation();
78
const originalSample = Array.from({ length: 10 }, (_, i) => i);
89

10+
const BAR_ROOT_TEST_ID = 'wave-progress-bar-track';
911
const PROGRESS_INDICATOR_TEST_ID = 'wave-progress-bar-progress-indicator';
12+
const AMPLITUDE_BAR_TEST_ID = 'amplitude-bar';
13+
window.ResizeObserver = ResizeObserverMock;
14+
15+
const getBoundingClientRect = jest
16+
.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect')
17+
.mockReturnValue({ width: 120 });
1018

1119
describe('WaveProgressBar', () => {
20+
beforeEach(() => {
21+
ResizeObserverMock.observers = [];
22+
});
23+
1224
it('is not rendered if waveform data is missing', () => {
1325
render(<WaveProgressBar seek={jest.fn()} waveformData={[]} />);
14-
expect(screen.queryByTestId('wave-progress-bar-track')).not.toBeInTheDocument();
26+
expect(screen.queryByTestId(BAR_ROOT_TEST_ID)).not.toBeInTheDocument();
27+
});
28+
29+
it('is not rendered if no space available', () => {
30+
getBoundingClientRect.mockReturnValueOnce({ width: 0 });
31+
render(<WaveProgressBar amplitudesCount={5} seek={jest.fn()} waveformData={originalSample} />);
32+
expect(screen.queryByTestId(BAR_ROOT_TEST_ID)).not.toBeInTheDocument();
33+
});
34+
35+
it('renders with default number of bars', () => {
36+
render(<WaveProgressBar seek={jest.fn()} waveformData={originalSample} />);
37+
const root = screen.getByTestId(BAR_ROOT_TEST_ID);
38+
expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe(
39+
'1px',
40+
);
41+
const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID);
42+
expect(
43+
bars.every(
44+
(b) =>
45+
b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '2px',
46+
),
47+
).toBeTruthy();
48+
expect(screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(40);
1549
});
50+
51+
it('adjusts the number of bars and gaps based on the custom ratio', () => {
52+
render(
53+
<WaveProgressBar
54+
relativeAmplitudeBarWidth={3}
55+
relativeAmplitudeGap={5}
56+
seek={jest.fn()}
57+
waveformData={originalSample}
58+
/>,
59+
);
60+
const root = screen.getByTestId(BAR_ROOT_TEST_ID);
61+
expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe(
62+
'5px',
63+
);
64+
const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID);
65+
expect(
66+
bars.every(
67+
(b) =>
68+
b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '3px',
69+
),
70+
).toBeTruthy();
71+
expect(bars).toHaveLength(15);
72+
});
73+
74+
it('recalculates the number of bars on root resize', async () => {
75+
render(<WaveProgressBar seek={jest.fn()} waveformData={originalSample} />);
76+
expect(ResizeObserverMock.observers).toHaveLength(1);
77+
const activeObserver = ResizeObserver.observers[0];
78+
expect(activeObserver.active).toBeTruthy();
79+
await act(() => {
80+
activeObserver.cb([{ contentRect: { width: 21 } }]);
81+
});
82+
const root = screen.getByTestId(BAR_ROOT_TEST_ID);
83+
expect(root.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-gap-width')).toBe(
84+
'1px',
85+
);
86+
const bars = screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID);
87+
expect(
88+
bars.every(
89+
(b) =>
90+
b.style.getPropertyValue('--str-chat__voice-recording-amplitude-bar-width') === '2px',
91+
),
92+
).toBeTruthy();
93+
expect(screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(7);
94+
});
95+
96+
it('does not recalculate the number of bars on root resize if ResizeObserver is unsupported', () => {
97+
window.ResizeObserver = undefined;
98+
render(<WaveProgressBar seek={jest.fn()} waveformData={originalSample} />);
99+
expect(ResizeObserverMock.observers).toHaveLength(0);
100+
window.ResizeObserver = ResizeObserverMock;
101+
});
102+
16103
it('is rendered with zero progress by default if waveform data is available', () => {
17104
const { container } = render(
18105
<WaveProgressBar amplitudesCount={5} seek={jest.fn()} waveformData={originalSample} />,
19106
);
20107
expect(container).toMatchSnapshot();
21-
expect(screen.queryByTestId(PROGRESS_INDICATOR_TEST_ID)).toBeInTheDocument();
108+
expect(screen.getByTestId(PROGRESS_INDICATOR_TEST_ID)).toBeInTheDocument();
22109
});
110+
23111
it('is rendered with highlighted bars with non-zero progress', () => {
24112
const { container } = render(
25113
<WaveProgressBar

src/components/Attachment/__tests__/__snapshots__/WaveProgressBar.test.js.snap

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,32 @@ exports[`WaveProgressBar is rendered with zero progress by default if waveform d
66
class="str-chat__wave-progress-bar__track"
77
data-testid="wave-progress-bar-track"
88
role="progressbar"
9+
style="--str-chat__voice-recording-amplitude-bar-gap-width: 8px;"
910
>
1011
<div
1112
class="str-chat__wave-progress-bar__amplitude-bar"
1213
data-testid="amplitude-bar"
13-
style="--str-chat__wave-progress-bar__amplitude-bar-height: 0%;"
14+
style="--str-chat__voice-recording-amplitude-bar-width: 16px; --str-chat__wave-progress-bar__amplitude-bar-height: 0%;"
1415
/>
1516
<div
1617
class="str-chat__wave-progress-bar__amplitude-bar"
1718
data-testid="amplitude-bar"
18-
style="--str-chat__wave-progress-bar__amplitude-bar-height: 200%;"
19+
style="--str-chat__voice-recording-amplitude-bar-width: 16px; --str-chat__wave-progress-bar__amplitude-bar-height: 200%;"
1920
/>
2021
<div
2122
class="str-chat__wave-progress-bar__amplitude-bar"
2223
data-testid="amplitude-bar"
23-
style="--str-chat__wave-progress-bar__amplitude-bar-height: 500%;"
24+
style="--str-chat__voice-recording-amplitude-bar-width: 16px; --str-chat__wave-progress-bar__amplitude-bar-height: 500%;"
2425
/>
2526
<div
2627
class="str-chat__wave-progress-bar__amplitude-bar"
2728
data-testid="amplitude-bar"
28-
style="--str-chat__wave-progress-bar__amplitude-bar-height: 700%;"
29+
style="--str-chat__voice-recording-amplitude-bar-width: 16px; --str-chat__wave-progress-bar__amplitude-bar-height: 700%;"
2930
/>
3031
<div
3132
class="str-chat__wave-progress-bar__amplitude-bar"
3233
data-testid="amplitude-bar"
33-
style="--str-chat__wave-progress-bar__amplitude-bar-height: 900%;"
34+
style="--str-chat__voice-recording-amplitude-bar-width: 16px; --str-chat__wave-progress-bar__amplitude-bar-height: 900%;"
3435
/>
3536
<div
3637
class="str-chat__wave-progress-bar__progress-indicator"

src/components/Attachment/components/WaveProgressBar.tsx

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import throttle from 'lodash.throttle';
12
import React, {
23
PointerEventHandler,
34
useCallback,
45
useEffect,
6+
useLayoutEffect,
57
useMemo,
68
useRef,
79
useState,
@@ -19,17 +21,27 @@ type WaveProgressBarProps = {
1921
amplitudesCount?: number;
2022
/** Progress expressed in fractional number value btw 0 and 100. */
2123
progress?: number;
24+
relativeAmplitudeBarWidth?: number;
25+
relativeAmplitudeGap?: number;
2226
};
2327

2428
export const WaveProgressBar = ({
2529
amplitudesCount = 40,
2630
progress = 0,
31+
relativeAmplitudeBarWidth = 2,
32+
relativeAmplitudeGap = 1,
2733
seek,
2834
waveformData,
2935
}: WaveProgressBarProps) => {
3036
const [progressIndicator, setProgressIndicator] = useState<HTMLDivElement | null>(null);
3137
const isDragging = useRef(false);
32-
const rootRef = useRef<HTMLDivElement | null>(null);
38+
const [root, setRoot] = useState<HTMLDivElement | null>(null);
39+
const [trackAxisX, setTrackAxisX] = useState<{
40+
barCount: number;
41+
barWidth: number;
42+
gap: number;
43+
}>();
44+
const lastRootWidth = useRef<number>();
3345

3446
const handleDragStart: PointerEventHandler<HTMLDivElement> = (e) => {
3547
e.preventDefault();
@@ -51,10 +63,33 @@ export const WaveProgressBar = ({
5163
progressIndicator.style.removeProperty('cursor');
5264
}, [progressIndicator]);
5365

54-
const resampledWaveformData = useMemo(() => resampleWaveformData(waveformData, amplitudesCount), [
55-
amplitudesCount,
56-
waveformData,
57-
]);
66+
const getTrackAxisX = useMemo(
67+
() =>
68+
throttle((rootWidth: number) => {
69+
if (rootWidth === lastRootWidth.current) return;
70+
lastRootWidth.current = rootWidth;
71+
const possibleAmpCount = Math.floor(
72+
rootWidth / (relativeAmplitudeGap + relativeAmplitudeBarWidth),
73+
);
74+
const tooManyAmplitudesToRender = possibleAmpCount < amplitudesCount;
75+
const barCount = tooManyAmplitudesToRender ? possibleAmpCount : amplitudesCount;
76+
const amplitudeBarWidthToGapRatio =
77+
relativeAmplitudeBarWidth / (relativeAmplitudeBarWidth + relativeAmplitudeGap);
78+
const barWidth = barCount && (rootWidth / barCount) * amplitudeBarWidthToGapRatio;
79+
80+
setTrackAxisX({
81+
barCount,
82+
barWidth,
83+
gap: barWidth * (relativeAmplitudeGap / relativeAmplitudeBarWidth),
84+
});
85+
}, 1),
86+
[relativeAmplitudeBarWidth, relativeAmplitudeGap, amplitudesCount],
87+
);
88+
89+
const resampledWaveformData = useMemo(
90+
() => (trackAxisX ? resampleWaveformData(waveformData, trackAxisX.barCount) : []),
91+
[trackAxisX, waveformData],
92+
);
5893

5994
useEffect(() => {
6095
document.addEventListener('pointerup', handleDragStop);
@@ -63,7 +98,25 @@ export const WaveProgressBar = ({
6398
};
6499
}, [handleDragStop]);
65100

66-
if (!waveformData.length) return null;
101+
useEffect(() => {
102+
if (!root || typeof ResizeObserver === 'undefined') return;
103+
const observer = new ResizeObserver(([entry]) => {
104+
getTrackAxisX(entry.contentRect.width);
105+
});
106+
observer.observe(root);
107+
108+
return () => {
109+
observer.disconnect();
110+
};
111+
}, [getTrackAxisX, root]);
112+
113+
useLayoutEffect(() => {
114+
if (!root) return;
115+
const { width: rootWidth } = root.getBoundingClientRect();
116+
getTrackAxisX(rootWidth);
117+
}, [getTrackAxisX, root]);
118+
119+
if (!waveformData.length || trackAxisX?.barCount === 0) return null;
67120

68121
return (
69122
<div
@@ -73,8 +126,13 @@ export const WaveProgressBar = ({
73126
onPointerDown={handleDragStart}
74127
onPointerMove={handleDrag}
75128
onPointerUp={handleDragStop}
76-
ref={rootRef}
129+
ref={setRoot}
77130
role='progressbar'
131+
style={
132+
{
133+
'--str-chat__voice-recording-amplitude-bar-gap-width': trackAxisX?.gap + 'px',
134+
} as React.CSSProperties
135+
}
78136
>
79137
{resampledWaveformData.map((amplitude, i) => (
80138
<div
@@ -86,6 +144,7 @@ export const WaveProgressBar = ({
86144
key={`amplitude-${i}`}
87145
style={
88146
{
147+
'--str-chat__voice-recording-amplitude-bar-width': trackAxisX?.barWidth + 'px',
89148
'--str-chat__wave-progress-bar__amplitude-bar-height': amplitude
90149
? amplitude * 100 + '%'
91150
: '0%',

src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
AudioContextMock,
2828
EventEmitterMock,
2929
MediaRecorderMock,
30+
ResizeObserverMock,
3031
} from '../../../../mock-builders/browser';
3132
import { generateDataavailableEvent } from '../../../../mock-builders/browser/events/dataavailable';
3233
import { AudioRecorder } from '../AudioRecorder';
@@ -55,6 +56,10 @@ const DEFAULT_RENDER_PARAMS = {
5556
componentCtx: {},
5657
};
5758

59+
window.ResizeObserver = ResizeObserverMock;
60+
61+
jest.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 120 });
62+
5863
const renderComponent = async ({
5964
channelActionCtx,
6065
channelStateCtx,

0 commit comments

Comments
 (0)