-
Notifications
You must be signed in to change notification settings - Fork 294
Expand file tree
/
Copy pathVoiceRecording.tsx
More file actions
177 lines (162 loc) Β· 5.98 KB
/
VoiceRecording.tsx
File metadata and controls
177 lines (162 loc) Β· 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import React from 'react';
import type { Attachment } from 'stream-chat';
import { FileSizeIndicator as DefaultFileSizeIndicator } from './components';
import { FileIcon } from '../FileIcon';
import {
useComponentContext,
useMessageContext,
useTranslationContext,
} from '../../context';
import {
type AudioPlayer,
type AudioPlayerState,
DurationDisplay,
PlaybackRateButton,
WaveProgressBar,
} from '../AudioPlayback';
import { useAudioPlayer } from '../AudioPlayback/WithAudioPlayback';
import { useStateStore } from '../../store';
import { PlayButton } from '../Button';
const rootClassName = 'str-chat__message-attachment__voice-recording-widget';
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
canPlayRecord: state.canPlayRecord,
durationSeconds: state.durationSeconds,
isPlaying: state.isPlaying,
playbackRate: state.currentPlaybackRate,
progress: state.progressPercent,
secondsElapsed: state.secondsElapsed,
});
type VoiceRecordingPlayerUIProps = {
audioPlayer: AudioPlayer;
};
// todo: finish creating a BaseAudioPlayer derived from VoiceRecordingPlayerUI and AudioAttachmentUI
const VoiceRecordingPlayerUI = ({ audioPlayer }: VoiceRecordingPlayerUIProps) => {
const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext();
const {
canPlayRecord,
durationSeconds,
isPlaying,
playbackRate,
progress,
secondsElapsed,
} = useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
return (
<div className={rootClassName} data-testid='voice-recording-widget'>
<div className='str-chat__message-attachment__voice-recording-widget__play-button-container'>
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
</div>
<div className='str-chat__message-attachment__voice-recording-widget__metadata'>
<div className='str-chat__message-attachment__voice-recording-widget__audio-state'>
<div className='str-chat__message-attachment__voice-recording-widget__timer'>
{durationSeconds ? (
<DurationDisplay
duration={durationSeconds}
isPlaying={!!isPlaying}
secondsElapsed={secondsElapsed}
showRemaining
/>
) : (
<FileSizeIndicator
fileSize={audioPlayer.fileSize}
maximumFractionDigits={0}
/>
)}
</div>
<WaveProgressBar
progress={progress}
seek={audioPlayer.seek}
waveformData={audioPlayer.waveformData || []}
/>
</div>
</div>
<div className='str-chat__message-attachment__voice-recording-widget__right-section'>
<PlaybackRateButton
disabled={!canPlayRecord}
onClick={audioPlayer.increasePlaybackRate}
>
x{playbackRate?.toString()}
</PlaybackRateButton>
</div>
</div>
);
};
export type VoiceRecordingPlayerProps = Pick<VoiceRecordingProps, 'attachment'> & {
/** An array of fractional numeric values of playback speed to override the defaults (1.0, 1.5, 2.0) */
playbackRates?: number[];
};
export const VoiceRecordingPlayer = ({
attachment,
playbackRates,
}: VoiceRecordingPlayerProps) => {
const { t } = useTranslationContext();
const {
asset_url,
duration = 0,
file_size,
mime_type,
title = t('Voice message'),
waveform_data,
} = attachment;
/**
* Introducing message context. This could be breaking change, therefore the fallback to {} is provided.
* If this component is used outside the message context, then there will be no audio player namespacing
* => scrolling away from the message in virtualized ML would create a new AudioPlayer instance.
*
* Edge case: the requester (message) has multiple attachments with the same assetURL - does not happen
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
* widgets will share the state.
*/
const { message, threadList } = useMessageContext() ?? {};
const audioPlayer = useAudioPlayer({
durationSeconds: duration ?? 0,
fileSize: file_size,
mimeType: mime_type,
playbackRates,
requester:
message?.id &&
`${threadList ? (message.parent_id ?? message.id) : ''}${message.id}`,
src: asset_url,
title,
waveformData: waveform_data,
});
return audioPlayer ? <VoiceRecordingPlayerUI audioPlayer={audioPlayer} /> : null;
};
export type QuotedVoiceRecordingProps = Pick<VoiceRecordingProps, 'attachment'>;
export const QuotedVoiceRecording = ({ attachment }: QuotedVoiceRecordingProps) => {
const { FileSizeIndicator = DefaultFileSizeIndicator } = useComponentContext();
return (
<div className={rootClassName} data-testid='quoted-voice-recording-widget'>
<div className='str-chat__message-attachment__voice-recording-widget__metadata'>
<div className='str-chat__message-attachment__voice-recording-widget__audio-state'>
<div className='str-chat__message-attachment__voice-recording-widget__timer'>
{attachment.duration ? (
<DurationDisplay
duration={attachment.duration}
isPlaying={false}
secondsElapsed={undefined}
/>
) : (
<FileSizeIndicator
fileSize={attachment.file_size}
maximumFractionDigits={0}
/>
)}
</div>
</div>
</div>
<FileIcon mimeType={attachment.mime_type} />
</div>
);
};
export type VoiceRecordingProps = {
/** The attachment object from the message's attachment list. */
attachment: Attachment;
/** A boolean flag to signal whether the attachment will be rendered inside the quoted reply. */
isQuoted?: boolean;
};
export const VoiceRecording = ({ attachment, isQuoted }: VoiceRecordingProps) =>
isQuoted ? (
<QuotedVoiceRecording attachment={attachment} />
) : (
<VoiceRecordingPlayer attachment={attachment} />
);