Skip to content

Commit e889046

Browse files
committed
feat: play the audio recordings outside and independently of message lists
1 parent e8142da commit e889046

25 files changed

+1630
-255
lines changed

src/components/Attachment/Audio.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,55 @@ import React from 'react';
22
import type { Attachment } from 'stream-chat';
33

44
import { DownloadButton, FileSizeIndicator, PlayButton, ProgressBar } from './components';
5-
import { useAudioController } from './hooks/useAudioController';
5+
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
6+
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';
7+
import { useStateStore } from '../../store';
8+
import { useMessageContext } from '../../context';
69

710
export type AudioProps = {
811
// fixme: rename og to attachment
912
og: Attachment;
1013
};
1114

15+
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
16+
isPlaying: state.isPlaying,
17+
progress: state.progressPercent,
18+
});
19+
1220
const UnMemoizedAudio = (props: AudioProps) => {
1321
const {
1422
og: { asset_url, file_size, mime_type, title },
1523
} = props;
16-
const { audioRef, isPlaying, progress, seek, togglePlay } = useAudioController({
24+
25+
/**
26+
* Introducing message context. This could be breaking change, therefore the fallback to {} is provided.
27+
* If this component is used outside the message context, then there will be no audio player namespacing
28+
* => scrolling away from the message in virtualized ML would create a new AudioPlayer instance.
29+
*
30+
* Edge case: the requester (message) has multiple attachments with the same assetURL - does not happen
31+
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
32+
* widgets will share the state.
33+
*/
34+
const { message } = useMessageContext() ?? {};
35+
36+
const audioPlayer = useAudioPlayer({
1737
mimeType: mime_type,
38+
requester: message?.id && `${message.parent_id}${message.id}`,
39+
src: asset_url,
1840
});
1941

20-
if (!asset_url) return null;
42+
const { isPlaying, progress } =
43+
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
44+
45+
if (!audioPlayer) return null;
2146

2247
const dataTestId = 'audio-widget';
2348
const rootClassName = 'str-chat__message-attachment-audio-widget';
2449

2550
return (
2651
<div className={rootClassName} data-testid={dataTestId}>
27-
<audio ref={audioRef}>
28-
<source data-testid='audio-source' src={asset_url} type='audio/mp3' />
29-
</audio>
3052
<div className='str-chat__message-attachment-audio-widget--play-controls'>
31-
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
53+
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
3254
</div>
3355
<div className='str-chat__message-attachment-audio-widget--text'>
3456
<div className='str-chat__message-attachment-audio-widget--text-first-row'>
@@ -37,7 +59,7 @@ const UnMemoizedAudio = (props: AudioProps) => {
3759
</div>
3860
<div className='str-chat__message-attachment-audio-widget--text-second-row'>
3961
<FileSizeIndicator fileSize={file_size} />
40-
<ProgressBar onClick={seek} progress={progress} />
62+
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
4163
</div>
4264
</div>
4365
</div>

src/components/Attachment/Card.tsx

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import type { AudioProps } from './Audio';
66
import { ImageComponent } from '../Gallery';
77
import { SafeAnchor } from '../SafeAnchor';
88
import { PlayButton, ProgressBar } from './components';
9-
import { useAudioController } from './hooks/useAudioController';
109
import { useChannelStateContext } from '../../context/ChannelStateContext';
1110
import { useTranslationContext } from '../../context/TranslationContext';
1211

1312
import type { Attachment } from 'stream-chat';
1413
import type { RenderAttachmentProps } from './utils';
1514
import type { Dimensions } from '../../types/types';
15+
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
16+
import { useStateStore } from '../../store';
17+
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';
18+
import { useMessageContext } from '../../context';
1619

1720
const getHostFromURL = (url?: string | null) => {
1821
if (url !== undefined && url !== null) {
@@ -126,31 +129,53 @@ const CardContent = (props: CardContentProps) => {
126129
);
127130
};
128131

132+
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
133+
isPlaying: state.isPlaying,
134+
progress: state.progressPercent,
135+
});
136+
137+
const AudioWidget = ({ mimeType, src }: { src: string; mimeType?: string }) => {
138+
/**
139+
* Introducing message context. This could be breaking change, therefore the fallback to {} is provided.
140+
* If this component is used outside the message context, then there will be no audio player namespacing
141+
* => scrolling away from the message in virtualized ML would create a new AudioPlayer instance.
142+
*
143+
* Edge case: the requester (message) has multiple attachments with the same assetURL - does not happen
144+
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
145+
* widgets will share the state.
146+
*/
147+
const { message } = useMessageContext() ?? {};
148+
149+
const audioPlayer = useAudioPlayer({
150+
mimeType,
151+
requester: message?.id && `${message.parent_id}${message.id}`,
152+
src,
153+
});
154+
155+
const { isPlaying, progress } =
156+
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
157+
158+
if (!audioPlayer) return;
159+
160+
return (
161+
<div className='str-chat__message-attachment-card-audio-widget--first-row'>
162+
<div className='str-chat__message-attachment-audio-widget--play-controls'>
163+
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
164+
</div>
165+
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
166+
</div>
167+
);
168+
};
169+
129170
export const CardAudio = ({
130171
og: { asset_url, author_name, mime_type, og_scrape_url, text, title, title_link },
131172
}: AudioProps) => {
132-
const { audioRef, isPlaying, progress, seek, togglePlay } = useAudioController({
133-
mimeType: mime_type,
134-
});
135-
136173
const url = title_link || og_scrape_url;
137174
const dataTestId = 'card-audio-widget';
138175
const rootClassName = 'str-chat__message-attachment-card-audio-widget';
139176
return (
140177
<div className={rootClassName} data-testid={dataTestId}>
141-
{asset_url && (
142-
<>
143-
<audio ref={audioRef}>
144-
<source data-testid='audio-source' src={asset_url} type='audio/mp3' />
145-
</audio>
146-
<div className='str-chat__message-attachment-card-audio-widget--first-row'>
147-
<div className='str-chat__message-attachment-audio-widget--play-controls'>
148-
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
149-
</div>
150-
<ProgressBar onClick={seek} progress={progress} />
151-
</div>
152-
</>
153-
)}
178+
{asset_url && <AudioWidget mimeType={mime_type} src={asset_url} />}
154179
<div className='str-chat__message-attachment-audio-widget--second-row'>
155180
{url && <SourceLink author_name={author_name} url={url} />}
156181
{title && (

src/components/Attachment/VoiceRecording.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,23 @@ import {
77
PlayButton,
88
WaveProgressBar,
99
} from './components';
10-
import { useAudioController } from './hooks/useAudioController';
1110
import { displayDuration } from './utils';
1211
import { FileIcon } from '../ReactFileUtilities';
13-
import { useTranslationContext } from '../../context';
12+
import { useMessageContext, useTranslationContext } from '../../context';
13+
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
14+
import { useStateStore } from '../../store';
15+
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';
1416

1517
const rootClassName = 'str-chat__message-attachment__voice-recording-widget';
1618

19+
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
20+
canPlayRecord: state.canPlayRecord,
21+
isPlaying: state.isPlaying,
22+
playbackRate: state.currentPlaybackRate,
23+
progress: state.progressPercent,
24+
secondsElapsed: state.secondsElapsed,
25+
});
26+
1727
export type VoiceRecordingPlayerProps = Pick<VoiceRecordingProps, 'attachment'> & {
1828
/** An array of fractional numeric values of playback speed to override the defaults (1.0, 1.5, 2.0) */
1929
playbackRates?: number[];
@@ -32,31 +42,35 @@ export const VoiceRecordingPlayer = ({
3242
waveform_data,
3343
} = attachment;
3444

35-
const {
36-
audioRef,
37-
increasePlaybackRate,
38-
isPlaying,
39-
playbackRate,
40-
progress,
41-
secondsElapsed,
42-
seek,
43-
togglePlay,
44-
} = useAudioController({
45+
/**
46+
* Introducing message context. This could be breaking change, therefore the fallback to {} is provided.
47+
* If this component is used outside the message context, then there will be no audio player namespacing
48+
* => scrolling away from the message in virtualized ML would create a new AudioPlayer instance.
49+
*
50+
* Edge case: the requester (message) has multiple attachments with the same assetURL - does not happen
51+
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
52+
* widgets will share the state.
53+
*/
54+
const { message } = useMessageContext() ?? {};
55+
56+
const audioPlayer = useAudioPlayer({
4557
durationSeconds: duration ?? 0,
4658
mimeType: mime_type,
4759
playbackRates,
60+
requester: message?.id && `${message.parent_id}${message.id}`,
61+
src: asset_url,
4862
});
4963

50-
if (!asset_url) return null;
64+
const { canPlayRecord, isPlaying, playbackRate, progress, secondsElapsed } =
65+
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
66+
67+
if (!audioPlayer) return null;
5168

5269
const displayedDuration = secondsElapsed || duration;
5370

5471
return (
5572
<div className={rootClassName} data-testid='voice-recording-widget'>
56-
<audio ref={audioRef}>
57-
<source data-testid='audio-source' src={asset_url} type={mime_type} />
58-
</audio>
59-
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
73+
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
6074
<div className='str-chat__message-attachment__voice-recording-widget__metadata'>
6175
<div
6276
className='str-chat__message-attachment__voice-recording-widget__title'
@@ -78,15 +92,18 @@ export const VoiceRecordingPlayer = ({
7892
</div>
7993
<WaveProgressBar
8094
progress={progress}
81-
seek={seek}
95+
seek={audioPlayer.seek}
8296
waveformData={waveform_data || []}
8397
/>
8498
</div>
8599
</div>
86100
<div className='str-chat__message-attachment__voice-recording-widget__right-section'>
87101
{isPlaying ? (
88-
<PlaybackRateButton disabled={!audioRef.current} onClick={increasePlaybackRate}>
89-
{playbackRate.toFixed(1)}x
102+
<PlaybackRateButton
103+
disabled={!canPlayRecord}
104+
onClick={audioPlayer.increasePlaybackRate}
105+
>
106+
{playbackRate?.toFixed(1)}x
90107
</PlaybackRateButton>
91108
) : (
92109
<FileIcon big={true} mimeType={mime_type} size={40} />

0 commit comments

Comments
 (0)