Skip to content
Open
40 changes: 32 additions & 8 deletions src/components/Attachment/Audio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,57 @@ import React from 'react';
import type { Attachment } from 'stream-chat';

import { DownloadButton, FileSizeIndicator, PlayButton, ProgressBar } from './components';
import { useAudioController } from './hooks/useAudioController';
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';
import { useStateStore } from '../../store';
import { useMessageContext } from '../../context';

export type AudioProps = {
// fixme: rename og to attachment
og: Attachment;
};

const audioPlayerStateSelector = (state: AudioPlayerState) => ({
isPlaying: state.isPlaying,
progress: state.progressPercent,
});

const UnMemoizedAudio = (props: AudioProps) => {
const {
og: { asset_url, file_size, mime_type, title },
} = props;
const { audioRef, isPlaying, progress, seek, togglePlay } = useAudioController({

/**
* 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({
mimeType: mime_type,
requester:
message?.id &&
`${threadList ? (message.parent_id ?? message.id) : ''}${message.id}`,
src: asset_url,
});

if (!asset_url) return null;
const { isPlaying, progress } =
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};

if (!audioPlayer) return null;

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

return (
<div className={rootClassName} data-testid={dataTestId}>
<audio ref={audioRef}>
<source data-testid='audio-source' src={asset_url} type='audio/mp3' />
</audio>
<div className='str-chat__message-attachment-audio-widget--play-controls'>
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
</div>
<div className='str-chat__message-attachment-audio-widget--text'>
<div className='str-chat__message-attachment-audio-widget--text-first-row'>
Expand All @@ -37,7 +61,7 @@ const UnMemoizedAudio = (props: AudioProps) => {
</div>
<div className='str-chat__message-attachment-audio-widget--text-second-row'>
<FileSizeIndicator fileSize={file_size} />
<ProgressBar onClick={seek} progress={progress} />
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
</div>
</div>
</div>
Expand Down
63 changes: 45 additions & 18 deletions src/components/Attachment/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import type { AudioProps } from './Audio';
import { ImageComponent } from '../Gallery';
import { SafeAnchor } from '../SafeAnchor';
import { PlayButton, ProgressBar } from './components';
import { useAudioController } from './hooks/useAudioController';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import { useTranslationContext } from '../../context/TranslationContext';

import type { Attachment } from 'stream-chat';
import type { RenderAttachmentProps } from './utils';
import type { Dimensions } from '../../types/types';
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
import { useStateStore } from '../../store';
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';
import { useMessageContext } from '../../context';

const getHostFromURL = (url?: string | null) => {
if (url !== undefined && url !== null) {
Expand Down Expand Up @@ -126,31 +129,55 @@ const CardContent = (props: CardContentProps) => {
);
};

const audioPlayerStateSelector = (state: AudioPlayerState) => ({
isPlaying: state.isPlaying,
progress: state.progressPercent,
});

const AudioWidget = ({ mimeType, src }: { src: string; mimeType?: string }) => {
/**
* 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({
mimeType,
requester:
message?.id &&
`${threadList ? (message.parent_id ?? message.id) : ''}${message.id}`,
src,
});

const { isPlaying, progress } =
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};

if (!audioPlayer) return;

return (
<div className='str-chat__message-attachment-card-audio-widget--first-row'>
<div className='str-chat__message-attachment-audio-widget--play-controls'>
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
</div>
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
</div>
);
};

export const CardAudio = ({
og: { asset_url, author_name, mime_type, og_scrape_url, text, title, title_link },
}: AudioProps) => {
const { audioRef, isPlaying, progress, seek, togglePlay } = useAudioController({
mimeType: mime_type,
});

const url = title_link || og_scrape_url;
const dataTestId = 'card-audio-widget';
const rootClassName = 'str-chat__message-attachment-card-audio-widget';
return (
<div className={rootClassName} data-testid={dataTestId}>
{asset_url && (
<>
<audio ref={audioRef}>
<source data-testid='audio-source' src={asset_url} type='audio/mp3' />
</audio>
<div className='str-chat__message-attachment-card-audio-widget--first-row'>
<div className='str-chat__message-attachment-audio-widget--play-controls'>
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
</div>
<ProgressBar onClick={seek} progress={progress} />
</div>
</>
)}
{asset_url && <AudioWidget mimeType={mime_type} src={asset_url} />}
<div className='str-chat__message-attachment-audio-widget--second-row'>
{url && <SourceLink author_name={author_name} url={url} />}
{title && (
Expand Down
59 changes: 39 additions & 20 deletions src/components/Attachment/VoiceRecording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ import {
PlayButton,
WaveProgressBar,
} from './components';
import { useAudioController } from './hooks/useAudioController';
import { displayDuration } from './utils';
import { FileIcon } from '../ReactFileUtilities';
import { useTranslationContext } from '../../context';
import { useMessageContext, useTranslationContext } from '../../context';
import { useAudioPlayer } from '../AudioPlayer/WithAudioPlayback';
import { useStateStore } from '../../store';
import type { AudioPlayerState } from '../AudioPlayer/AudioPlayer';

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

const audioPlayerStateSelector = (state: AudioPlayerState) => ({
canPlayRecord: state.canPlayRecord,
isPlaying: state.isPlaying,
playbackRate: state.currentPlaybackRate,
progress: state.progressPercent,
secondsElapsed: state.secondsElapsed,
});

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[];
Expand All @@ -32,31 +42,37 @@ export const VoiceRecordingPlayer = ({
waveform_data,
} = attachment;

const {
audioRef,
increasePlaybackRate,
isPlaying,
playbackRate,
progress,
secondsElapsed,
seek,
togglePlay,
} = useAudioController({
/**
* 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,
mimeType: mime_type,
playbackRates,
requester:
message?.id &&
`${threadList ? (message.parent_id ?? message.id) : ''}${message.id}`,
src: asset_url,
});

if (!asset_url) return null;
const { canPlayRecord, isPlaying, playbackRate, progress, secondsElapsed } =
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};

if (!audioPlayer) return null;

const displayedDuration = secondsElapsed || duration;

return (
<div className={rootClassName} data-testid='voice-recording-widget'>
<audio ref={audioRef}>
<source data-testid='audio-source' src={asset_url} type={mime_type} />
</audio>
<PlayButton isPlaying={isPlaying} onClick={togglePlay} />
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
<div className='str-chat__message-attachment__voice-recording-widget__metadata'>
<div
className='str-chat__message-attachment__voice-recording-widget__title'
Expand All @@ -78,15 +94,18 @@ export const VoiceRecordingPlayer = ({
</div>
<WaveProgressBar
progress={progress}
seek={seek}
seek={audioPlayer.seek}
waveformData={waveform_data || []}
/>
</div>
</div>
<div className='str-chat__message-attachment__voice-recording-widget__right-section'>
{isPlaying ? (
<PlaybackRateButton disabled={!audioRef.current} onClick={increasePlaybackRate}>
{playbackRate.toFixed(1)}x
<PlaybackRateButton
disabled={!canPlayRecord}
onClick={audioPlayer.increasePlaybackRate}
>
{playbackRate?.toFixed(1)}x
</PlaybackRateButton>
) : (
<FileIcon big={true} mimeType={mime_type} size={40} />
Expand Down
Loading