Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/Attachment/AttachmentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isVoiceRecordingAttachment,
} from 'stream-chat';

import { Audio as DefaultAudioAttachment } from './Audio';
import { AttachmentActions as DefaultAttachmentActions } from './AttachmentActions';
import { VoiceRecording as DefaultVoiceRecording } from './VoiceRecording';
import { type GalleryItem, toGalleryItemDescriptors } from '../Gallery';
Expand Down Expand Up @@ -275,7 +276,7 @@ export const OtherFilesContainer = ({

export const AudioContainer = ({
attachment,
Audio = DefaultFile,
Audio = DefaultAudioAttachment,
}: RenderAttachmentProps) => (
<AttachmentWithinContainer attachment={attachment} componentType='audio'>
<div className='str-chat__attachment'>
Expand Down
47 changes: 34 additions & 13 deletions src/components/Attachment/Audio.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import type { Attachment } from 'stream-chat';

import { DownloadButton, FileSizeIndicator, ProgressBar } from './components';
import { FileSizeIndicator } from './components';
import type { AudioPlayerState } from '../AudioPlayback/AudioPlayer';
import { useAudioPlayer } from '../AudioPlayback/WithAudioPlayback';
import { useStateStore } from '../../store';
import { useMessageContext } from '../../context';
import type { AudioPlayer } from '../AudioPlayback/AudioPlayer';
import { PlayButton } from '../Button/PlayButton';
import { FileIcon } from '../FileIcon';
import { DurationDisplay, ProgressBar } from '../AudioPlayback';

type AudioAttachmentUIProps = {
audioPlayer: AudioPlayer;
Expand All @@ -18,24 +20,42 @@ const AudioAttachmentUI = ({ audioPlayer }: AudioAttachmentUIProps) => {
const dataTestId = 'audio-widget';
const rootClassName = 'str-chat__message-attachment-audio-widget';

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

return (
<div className={rootClassName} data-testid={dataTestId}>
<div className='str-chat__message-attachment-audio-widget--play-controls'>
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
</div>
<div className='str-chat__message-attachment-audio-widget--text'>
<div className='str-chat__message-attachment-audio-widget--data'>
<div className='str-chat__message-attachment-audio-widget--text-first-row'>
<div className='str-chat__message-attachment-audio-widget--title'>
{audioPlayer.title}
</div>
<DownloadButton assetUrl={audioPlayer.src} />
<FileIcon
className='str-chat__file-icon'
mimeType={audioPlayer.mimeType}
size='sm'
/>
{/*<DownloadButton assetUrl={audioPlayer.src} />*/}
</div>
<div className='str-chat__message-attachment-audio-widget--text-second-row'>
<FileSizeIndicator fileSize={audioPlayer.fileSize} />
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
{durationSeconds ? (
<>
<DurationDisplay
duration={durationSeconds}
isPlaying={!!isPlaying}
secondsElapsed={secondsElapsed}
/>
<ProgressBar progress={progress ?? 0} seek={audioPlayer.seek} />
</>
) : (
<>
<FileSizeIndicator fileSize={audioPlayer.fileSize} />
<ProgressBar progress={progress ?? 0} seek={audioPlayer.seek} />
</>
)}
</div>
</div>
</div>
Expand All @@ -47,13 +67,18 @@ export type AudioProps = {
};

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

const UnMemoizedAudio = (props: AudioProps) => {
/**
* Audio attachment with play/pause button and progress bar
*/
export const Audio = (props: AudioProps) => {
const {
attachment: { asset_url, file_size, mime_type, title },
attachment: { asset_url, duration, file_size, mime_type, title },
} = props;

/**
Expand All @@ -68,6 +93,7 @@ const UnMemoizedAudio = (props: AudioProps) => {
const { message, threadList } = useMessageContext() ?? {};

const audioPlayer = useAudioPlayer({
durationSeconds: duration,
fileSize: file_size,
mimeType: mime_type,
requester:
Expand All @@ -80,8 +106,3 @@ const UnMemoizedAudio = (props: AudioProps) => {

return audioPlayer ? <AudioAttachmentUI audioPlayer={audioPlayer} /> : null;
};

/**
* Audio attachment with play/pause button and progress bar
*/
export const Audio = React.memo(UnMemoizedAudio) as typeof UnMemoizedAudio;
48 changes: 28 additions & 20 deletions src/components/Attachment/FileAttachment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { useComponentContext } from '../../context/ComponentContext';
import { FileIcon } from '../FileIcon';
import type { Attachment } from 'stream-chat';

Expand All @@ -8,26 +9,33 @@ export type FileAttachmentProps = {
attachment: Attachment;
};

const UnMemoizedFileAttachment = ({ attachment }: FileAttachmentProps) => (
<div className='str-chat__message-attachment-file--item' data-testid='attachment-file'>
<FileIcon className='str-chat__file-icon' mimeType={attachment.mime_type} />
<div className='str-chat__message-attachment-file--item__info'>
<div className='str-chat__message-attachment-file--item__first-row'>
<div
className='str-chat__message-attachment-file--item__name'
data-testid='file-title'
>
{attachment.title}
export const FileAttachment = ({ attachment }: FileAttachmentProps) => {
const { AttachmentFileIcon } = useComponentContext();
const FileIconComponent = AttachmentFileIcon ?? FileIcon;
return (
<div
className='str-chat__message-attachment-file--item'
data-testid='attachment-file'
>
<FileIconComponent
className='str-chat__file-icon'
fileName={attachment.title}
mimeType={attachment.mime_type}
/>
<div className='str-chat__message-attachment-file--item__info'>
<div className='str-chat__message-attachment-file--item__first-row'>
<div
className='str-chat__message-attachment-file--item__name'
data-testid='file-title'
>
{attachment.title}
</div>
{/*<DownloadButton assetUrl={attachment.asset_url} />*/}
</div>
<div className='str-chat__message-attachment-file--item__data'>
<FileSizeIndicator fileSize={attachment.file_size} />
</div>
{/*<DownloadButton assetUrl={attachment.asset_url} />*/}
</div>
<div className='str-chat__message-attachment-file--item__data'>
<FileSizeIndicator fileSize={attachment.file_size} />
</div>
</div>
</div>
);

export const FileAttachment = React.memo(
UnMemoizedFileAttachment,
) as typeof UnMemoizedFileAttachment;
);
};
5 changes: 2 additions & 3 deletions src/components/Attachment/LinkPreview/CardAudio.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { type AudioPlayerState, useAudioPlayer } from '../../AudioPlayback';
import { type AudioPlayerState, ProgressBar, useAudioPlayer } from '../../AudioPlayback';
import { useMessageContext } from '../../../context';
import { useStateStore } from '../../../store';
import { PlayButton } from '../../Button';
import { ProgressBar } from '../components';
import type { AudioProps } from '../Audio';
import React from 'react';
import { IconChainLink } from '../../Icons';
Expand Down Expand Up @@ -74,7 +73,7 @@ const AudioWidget = ({ mimeType, src }: { src: string; mimeType?: string }) => {
<div className='str-chat__message-attachment-audio-widget--play-controls'>
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
</div>
<ProgressBar onClick={audioPlayer.seek} progress={progress ?? 0} />
<ProgressBar progress={progress ?? 0} seek={audioPlayer.seek} />
</div>
);
};
Expand Down
29 changes: 20 additions & 9 deletions src/components/Attachment/VoiceRecording.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import React from 'react';
import type { Attachment } from 'stream-chat';

import { FileSizeIndicator, PlaybackRateButton, WaveProgressBar } from './components';
import { FileSizeIndicator } from './components';
import { FileIcon } from '../FileIcon';
import { useMessageContext, useTranslationContext } from '../../context';
import { DurationDisplay } from '../AudioPlayback';
import type { AudioPlayerState } from '../AudioPlayback/AudioPlayer';
import { useAudioPlayer } from '../AudioPlayback/WithAudioPlayback';
import {
type AudioPlayer,
type AudioPlayerState,
DurationDisplay,
PlaybackRateButton,
useAudioPlayer,
WaveProgressBar,
} from '../AudioPlayback';
import { useStateStore } from '../../store';
import type { AudioPlayer } from '../AudioPlayback/AudioPlayer';
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,
Expand All @@ -27,18 +32,24 @@ type VoiceRecordingPlayerUIProps = {

// todo: finish creating a BaseAudioPlayer derived from VoiceRecordingPlayerUI and AudioAttachmentUI
const VoiceRecordingPlayerUI = ({ audioPlayer }: VoiceRecordingPlayerUIProps) => {
const { canPlayRecord, isPlaying, playbackRate, progress, secondsElapsed } =
useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
const {
canPlayRecord,
durationSeconds,
isPlaying,
playbackRate,
progress,
secondsElapsed,
} = useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};

return (
<div className={rootClassName} data-testid='voice-recording-widget'>
<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__audio-state'>
<div className='str-chat__message-attachment__voice-recording-widget__timer'>
{audioPlayer.durationSeconds ? (
{durationSeconds ? (
<DurationDisplay
duration={audioPlayer.durationSeconds}
duration={durationSeconds}
isPlaying={!!isPlaying}
secondsElapsed={secondsElapsed}
showRemaining
Expand Down
9 changes: 8 additions & 1 deletion src/components/Attachment/__tests__/Audio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,21 @@ describe('Audio', () => {

it('renders title and file size', () => {
const { container, getByText } = renderComponent({
og: audioAttachment,
og: { ...audioAttachment, duration: undefined },
});

expect(getByText(audioAttachment.title)).toBeInTheDocument();
expect(getByText(prettifyFileSize(audioAttachment.file_size))).toBeInTheDocument();
expect(container.querySelector('img')).not.toBeInTheDocument();
});

it('renders duration instead of file size when available', () => {
renderComponent({ og: { ...audioAttachment, duration: 43.007999420166016 } });

expect(screen.getByText('00:44')).toBeInTheDocument();
expect(screen.queryByTestId('file-size-indicator')).not.toBeInTheDocument();
});

it('creates a playback Audio() with the right src only after clicked to play', async () => {
renderComponent({ og: audioAttachment });
await clickToPlay();
Expand Down
17 changes: 10 additions & 7 deletions src/components/Attachment/__tests__/WaveProgressBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ window.ResizeObserver = ResizeObserverMock;

const getBoundingClientRect = jest
.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect')
.mockReturnValue({ width: 120 });
.mockReturnValue({ width: 120, x: 0 });

describe('WaveProgressBar', () => {
beforeEach(() => {
ResizeObserverMock.observers = [];
getBoundingClientRect.mockReturnValue({ width: 120, x: 0 });
});

it('is not rendered if waveform data is missing', () => {
Expand All @@ -27,15 +28,16 @@ describe('WaveProgressBar', () => {
});

it('is not rendered if no space available', () => {
getBoundingClientRect.mockReturnValueOnce({ width: 0 });
getBoundingClientRect.mockReturnValue({ width: 0, x: 0 });
render(
<WaveProgressBar
amplitudesCount={5}
seek={jest.fn()}
waveformData={originalSample}
/>,
);
expect(screen.queryByTestId(BAR_ROOT_TEST_ID)).not.toBeInTheDocument();
expect(screen.getByTestId(BAR_ROOT_TEST_ID)).toBeInTheDocument();
expect(screen.queryAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(0);
});

it('renders with default number of bars', () => {
Expand Down Expand Up @@ -110,15 +112,16 @@ describe('WaveProgressBar', () => {
});

it('is rendered with zero progress by default if waveform data is available', () => {
const { container } = render(
render(
<WaveProgressBar
amplitudesCount={5}
seek={jest.fn()}
waveformData={originalSample}
/>,
);
expect(container).toMatchSnapshot();
expect(screen.getAllByTestId(AMPLITUDE_BAR_TEST_ID)).toHaveLength(40);
expect(screen.getByTestId(PROGRESS_INDICATOR_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(PROGRESS_INDICATOR_TEST_ID)).toHaveStyle('left: 0px');
});

it('is rendered with highlighted bars with non-zero progress', () => {
Expand All @@ -132,8 +135,8 @@ describe('WaveProgressBar', () => {
);
expect(
container.querySelectorAll('.str-chat__wave-progress-bar__amplitude-bar--active'),
).toHaveLength(1);
).toHaveLength(8);
expect(screen.queryByTestId(PROGRESS_INDICATOR_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(PROGRESS_INDICATOR_TEST_ID)).toHaveStyle('left: 20%');
expect(screen.queryByTestId(PROGRESS_INDICATOR_TEST_ID)).not.toHaveStyle('left: 0px');
});
});

This file was deleted.

Loading
Loading