Voice notes mini-player
This commit is contained in:
parent
b5849f872a
commit
0e655ceeed
45 changed files with 1599 additions and 487 deletions
|
@ -34,6 +34,7 @@ export function AvatarLightbox({
|
|||
media={[]}
|
||||
saveAttachment={noop}
|
||||
toggleForwardMessageModal={noop}
|
||||
onMediaPlaybackStart={noop}
|
||||
>
|
||||
<AvatarPreview
|
||||
avatarColor={avatarColor}
|
||||
|
|
|
@ -117,6 +117,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
toggleSettings: action('toggle-settings'),
|
||||
toggleSpeakerView: action('toggle-speaker-view'),
|
||||
isConversationTooBigToRing: false,
|
||||
pauseVoiceNotePlayer: action('pause-audio-player'),
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -100,6 +100,7 @@ export type PropsType = {
|
|||
toggleSettings: () => void;
|
||||
toggleSpeakerView: () => void;
|
||||
isConversationTooBigToRing: boolean;
|
||||
pauseVoiceNotePlayer: () => void;
|
||||
};
|
||||
|
||||
type ActiveCallManagerPropsType = PropsType & {
|
||||
|
@ -138,6 +139,7 @@ function ActiveCallManager({
|
|||
toggleScreenRecordingPermissionsDialog,
|
||||
toggleSettings,
|
||||
toggleSpeakerView,
|
||||
pauseVoiceNotePlayer,
|
||||
}: ActiveCallManagerPropsType): JSX.Element {
|
||||
const {
|
||||
conversation,
|
||||
|
@ -157,6 +159,9 @@ function ActiveCallManager({
|
|||
}, [cancelCall, conversation.id]);
|
||||
|
||||
const joinActiveCall = useCallback(() => {
|
||||
// pause any voice note playback
|
||||
pauseVoiceNotePlayer();
|
||||
|
||||
startCall({
|
||||
callMode: activeCall.callMode,
|
||||
conversationId: conversation.id,
|
||||
|
@ -169,6 +174,7 @@ function ActiveCallManager({
|
|||
conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
pauseVoiceNotePlayer,
|
||||
]);
|
||||
|
||||
const getGroupCallVideoFrameSourceForActiveCall = useCallback(
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as React from 'react';
|
|||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { PropsType } from './Lightbox';
|
||||
|
@ -62,6 +63,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
saveAttachment: action('saveAttachment'),
|
||||
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
|
||||
toggleForwardMessageModal: action('toggleForwardMessageModal'),
|
||||
onMediaPlaybackStart: noop,
|
||||
});
|
||||
|
||||
export function Multimedia(): JSX.Element {
|
||||
|
|
|
@ -34,6 +34,7 @@ export type PropsType = {
|
|||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
selectedIndex?: number;
|
||||
toggleForwardMessageModal: (messageId: string) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
};
|
||||
|
||||
const ZOOM_SCALE = 3;
|
||||
|
@ -60,6 +61,7 @@ export function Lightbox({
|
|||
saveAttachment,
|
||||
selectedIndex: initialSelectedIndex = 0,
|
||||
toggleForwardMessageModal,
|
||||
onMediaPlaybackStart,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [root, setRoot] = React.useState<HTMLElement | undefined>();
|
||||
const [selectedIndex, setSelectedIndex] =
|
||||
|
@ -204,11 +206,12 @@ export function Lightbox({
|
|||
}
|
||||
|
||||
if (videoElement.paused) {
|
||||
onMediaPlaybackStart();
|
||||
void videoElement.play();
|
||||
} else {
|
||||
videoElement.pause();
|
||||
}
|
||||
}, [videoElement]);
|
||||
}, [videoElement, onMediaPlaybackStart]);
|
||||
|
||||
useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
|
|
96
ts/components/MiniPlayer.stories.tsx
Normal file
96
ts/components/MiniPlayer.stories.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MiniPlayer, PlayerState } from './MiniPlayer';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const audio = new Audio();
|
||||
audio.src = '/fixtures/incompetech-com-Agnus-Dei-X.mp3';
|
||||
|
||||
export default {
|
||||
title: 'components/MiniPlayer',
|
||||
component: MiniPlayer,
|
||||
};
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
const [playerState, setPlayerState] = useState(PlayerState.loading);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
|
||||
const activate = () => {
|
||||
setActive(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setPlayerState(PlayerState.playing);
|
||||
void audio.play();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
setActive(false);
|
||||
setPlayerState(PlayerState.loading);
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdateTime = () => {
|
||||
setCurrentTime(audio.currentTime);
|
||||
};
|
||||
const handleEnded = () => {
|
||||
deactivate();
|
||||
};
|
||||
audio.addEventListener('timeupdate', handleUpdateTime);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', handleUpdateTime);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, [setCurrentTime]);
|
||||
|
||||
const playAction = () => {
|
||||
setPlayerState(PlayerState.playing);
|
||||
void audio.play();
|
||||
};
|
||||
const pauseAction = () => {
|
||||
setPlayerState(PlayerState.paused);
|
||||
audio.pause();
|
||||
};
|
||||
|
||||
const setPlaybackRateAction = (rate: number) => {
|
||||
setPlaybackRate(rate);
|
||||
audio.playbackRate = rate;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{active && (
|
||||
<MiniPlayer
|
||||
title="Paige Hall 😉"
|
||||
i18n={i18n}
|
||||
onPlay={playAction}
|
||||
onPause={pauseAction}
|
||||
onPlaybackRate={setPlaybackRateAction}
|
||||
state={playerState}
|
||||
currentTime={currentTime}
|
||||
duration={Number.isFinite(audio.duration) ? audio.duration : 0}
|
||||
playbackRate={playbackRate}
|
||||
onClose={deactivate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!active && (
|
||||
<button type="button" onClick={activate}>
|
||||
Activate
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
117
ts/components/MiniPlayer.tsx
Normal file
117
ts/components/MiniPlayer.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { durationToPlaybackText } from '../util/durationToPlaybackText';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import { PlaybackRateButton } from './PlaybackRateButton';
|
||||
|
||||
export enum PlayerState {
|
||||
loading = 'loading',
|
||||
playing = 'playing',
|
||||
paused = 'paused',
|
||||
}
|
||||
|
||||
export type Props = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
title: string;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
playbackRate: number;
|
||||
state: PlayerState;
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
onPlaybackRate: (rate: number) => void;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export function MiniPlayer({
|
||||
i18n,
|
||||
title,
|
||||
state,
|
||||
currentTime,
|
||||
duration,
|
||||
playbackRate,
|
||||
onPlay,
|
||||
onPause,
|
||||
onPlaybackRate,
|
||||
onClose,
|
||||
}: Props): JSX.Element {
|
||||
const updatePlaybackRate = useCallback(() => {
|
||||
onPlaybackRate(PlaybackRateButton.nextPlaybackRate(playbackRate));
|
||||
}, [playbackRate, onPlaybackRate]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
switch (state) {
|
||||
case PlayerState.playing:
|
||||
onPause();
|
||||
break;
|
||||
case PlayerState.paused:
|
||||
onPlay();
|
||||
break;
|
||||
case PlayerState.loading:
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Missing case: ${state}`);
|
||||
}
|
||||
}, [state, onPause, onPlay]);
|
||||
|
||||
let label: string | undefined;
|
||||
switch (state) {
|
||||
case PlayerState.playing:
|
||||
label = i18n('MessageAudio--pause');
|
||||
break;
|
||||
case PlayerState.paused:
|
||||
label = i18n('MessageAudio--play');
|
||||
break;
|
||||
case PlayerState.loading:
|
||||
label = i18n('MessageAudio--pending');
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Missing case ${state}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="MiniPlayer">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'MiniPlayer__playback-button',
|
||||
state === 'playing' && 'MiniPlayer__playback-button--pause',
|
||||
state === 'paused' && 'MiniPlayer__playback-button--play',
|
||||
state === 'loading' && 'MiniPlayer__playback-button--pending'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
aria-label={label}
|
||||
disabled={state === PlayerState.loading}
|
||||
/>
|
||||
|
||||
<div className="MiniPlayer__state">
|
||||
<Emojify text={title} />
|
||||
<span className="MiniPlayer__middot">·</span>
|
||||
<span>
|
||||
{durationToPlaybackText(
|
||||
state === PlayerState.loading ? duration : currentTime
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<PlaybackRateButton
|
||||
i18n={i18n}
|
||||
variant="mini-player"
|
||||
playbackRate={playbackRate}
|
||||
onClick={updatePlaybackRate}
|
||||
visible={state === 'playing'}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="MiniPlayer__close-button"
|
||||
onClick={onClose}
|
||||
aria-label={i18n('close')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -26,6 +26,7 @@ export type PropsType = {
|
|||
onDelete: (story: StoryViewType) => unknown;
|
||||
onForward: (storyId: string) => unknown;
|
||||
onSave: (story: StoryViewType) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
retryMessageSend: (messageId: string) => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
|
@ -43,6 +44,7 @@ export function MyStories({
|
|||
retryMessageSend,
|
||||
viewStory,
|
||||
hasViewReceiptSetting,
|
||||
onMediaPlaybackStart,
|
||||
}: PropsType): JSX.Element {
|
||||
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
||||
StoryViewType | undefined
|
||||
|
@ -94,6 +96,7 @@ export function MyStories({
|
|||
key={story.messageId}
|
||||
onForward={onForward}
|
||||
onSave={onSave}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
retryMessageSend={retryMessageSend}
|
||||
setConfirmDeleteStory={setConfirmDeleteStory}
|
||||
|
@ -122,6 +125,7 @@ type StorySentPropsType = Pick<
|
|||
| 'queueStoryDownload'
|
||||
| 'retryMessageSend'
|
||||
| 'viewStory'
|
||||
| 'onMediaPlaybackStart'
|
||||
> & {
|
||||
setConfirmDeleteStory: (_: StoryViewType | undefined) => unknown;
|
||||
story: StoryViewType;
|
||||
|
@ -132,6 +136,7 @@ function StorySent({
|
|||
i18n,
|
||||
onForward,
|
||||
onSave,
|
||||
onMediaPlaybackStart,
|
||||
queueStoryDownload,
|
||||
retryMessageSend,
|
||||
setConfirmDeleteStory,
|
||||
|
@ -177,6 +182,7 @@ function StorySent({
|
|||
moduleClassName="StoryListItem__previews--image"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={story.messageId}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
/>
|
||||
</div>
|
||||
<div className="MyStories__story__details">
|
||||
|
|
|
@ -21,6 +21,7 @@ export type PropsType = {
|
|||
myStories: Array<MyStoryType>;
|
||||
onAddStory: () => unknown;
|
||||
onClick: () => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
showToast: ShowToastActionCreatorType;
|
||||
};
|
||||
|
@ -35,6 +36,7 @@ export function MyStoryButton({
|
|||
myStories,
|
||||
onAddStory,
|
||||
onClick,
|
||||
onMediaPlaybackStart,
|
||||
queueStoryDownload,
|
||||
showToast,
|
||||
}: PropsType): JSX.Element {
|
||||
|
@ -190,6 +192,7 @@ export function MyStoryButton({
|
|||
moduleClassName="StoryListItem__previews--image"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={newestStory.messageId}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
107
ts/components/PlaybackRateButton.tsx
Normal file
107
ts/components/PlaybackRateButton.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
const SPRING_CONFIG = {
|
||||
mass: 0.5,
|
||||
tension: 350,
|
||||
friction: 20,
|
||||
velocity: 0.01,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
// undefined if not playing
|
||||
playbackRate: number | undefined;
|
||||
variant: 'message-outgoing' | 'message-incoming' | 'mini-player';
|
||||
onClick: () => void;
|
||||
visible?: boolean;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export function PlaybackRateButton({
|
||||
playbackRate,
|
||||
variant,
|
||||
visible = true,
|
||||
i18n,
|
||||
onClick,
|
||||
}: Props): JSX.Element {
|
||||
const [isDown, setIsDown] = useState(false);
|
||||
|
||||
const [animProps] = useSpring(
|
||||
{
|
||||
config: SPRING_CONFIG,
|
||||
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
||||
},
|
||||
[visible, isDown]
|
||||
);
|
||||
|
||||
// Clicking button toggle playback
|
||||
const onButtonClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
// Keyboard playback toggle
|
||||
const onButtonKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
const playbackRateLabels: { [key: number]: string } = {
|
||||
1: i18n('MessageAudio--playbackRate1'),
|
||||
1.5: i18n('MessageAudio--playbackRate1p5'),
|
||||
2: i18n('MessageAudio--playbackRate2'),
|
||||
0.5: i18n('MessageAudio--playbackRatep5'),
|
||||
};
|
||||
|
||||
const label = playbackRate
|
||||
? playbackRateLabels[playbackRate].toString()
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<animated.div style={animProps}>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'PlaybackRateButton',
|
||||
`PlaybackRateButton--${variant}`
|
||||
)}
|
||||
onClick={onButtonClick}
|
||||
onKeyDown={onButtonKeyDown}
|
||||
onMouseDown={() => setIsDown(true)}
|
||||
onMouseUp={() => setIsDown(false)}
|
||||
onMouseLeave={() => setIsDown(false)}
|
||||
aria-label={label}
|
||||
tabIndex={0}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</animated.div>
|
||||
);
|
||||
}
|
||||
|
||||
const playbackRates = [1, 1.5, 2, 0.5];
|
||||
|
||||
PlaybackRateButton.nextPlaybackRate = (currentRate: number): number => {
|
||||
// cycle through the rates
|
||||
return playbackRates[
|
||||
(playbackRates.indexOf(currentRate) + 1) % playbackRates.length
|
||||
];
|
||||
};
|
|
@ -72,6 +72,7 @@ export type PropsType = {
|
|||
string,
|
||||
number
|
||||
>;
|
||||
onMediaPlaybackStart: () => void;
|
||||
} & Pick<
|
||||
StoriesSettingsModalPropsType,
|
||||
| 'onHideMyStoriesFrom'
|
||||
|
@ -138,6 +139,7 @@ export function SendStoryModal({
|
|||
toggleGroupsForStorySend,
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||
toggleSignalConnectionsModal,
|
||||
onMediaPlaybackStart,
|
||||
}: PropsType): JSX.Element {
|
||||
const [page, setPage] = useState<PageType>(Page.SendStory);
|
||||
|
||||
|
@ -875,6 +877,7 @@ export function SendStoryModal({
|
|||
label="label"
|
||||
moduleClassName="SendStoryModal__story"
|
||||
attachment={attachment}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
/>
|
||||
</div>
|
||||
<div className="SendStoryModal__top-bar">
|
||||
|
|
|
@ -39,6 +39,7 @@ export type PropsType = {
|
|||
myStories: Array<MyStoryType>;
|
||||
onForwardStory: (storyId: string) => unknown;
|
||||
onSaveStory: (story: StoryViewType) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
preferredWidthFromStorage: number;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
renderStoryCreator: () => JSX.Element;
|
||||
|
@ -67,6 +68,7 @@ export function Stories({
|
|||
myStories,
|
||||
onForwardStory,
|
||||
onSaveStory,
|
||||
onMediaPlaybackStart,
|
||||
preferredWidthFromStorage,
|
||||
queueStoryDownload,
|
||||
renderStoryCreator,
|
||||
|
@ -110,6 +112,7 @@ export function Stories({
|
|||
onDelete={deleteStoryForEveryone}
|
||||
onForward={onForwardStory}
|
||||
onSave={onSaveStory}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
retryMessageSend={retryMessageSend}
|
||||
viewStory={viewStory}
|
||||
|
@ -134,6 +137,7 @@ export function Stories({
|
|||
}
|
||||
}}
|
||||
onStoriesSettings={showStoriesSettings}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
showConversation={showConversation}
|
||||
showToast={showToast}
|
||||
|
|
|
@ -64,6 +64,7 @@ export type PropsType = {
|
|||
onAddStory: (file?: File) => unknown;
|
||||
onMyStoriesClicked: () => unknown;
|
||||
onStoriesSettings: () => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
showConversation: ShowConversationType;
|
||||
showToast: ShowToastActionCreatorType;
|
||||
|
@ -82,6 +83,7 @@ export function StoriesPane({
|
|||
onAddStory,
|
||||
onMyStoriesClicked,
|
||||
onStoriesSettings,
|
||||
onMediaPlaybackStart,
|
||||
queueStoryDownload,
|
||||
showConversation,
|
||||
showToast,
|
||||
|
@ -159,6 +161,7 @@ export function StoriesPane({
|
|||
onClick={onMyStoriesClicked}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
showToast={showToast}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
/>
|
||||
{renderedStories.map(story => (
|
||||
<StoryListItem
|
||||
|
@ -174,6 +177,7 @@ export function StoriesPane({
|
|||
toggleStoriesView();
|
||||
}}
|
||||
onHideStory={toggleHideStories}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
story={story.storyView}
|
||||
viewUserStories={viewUserStories}
|
||||
|
@ -204,6 +208,7 @@ export function StoriesPane({
|
|||
toggleStoriesView();
|
||||
}}
|
||||
onHideStory={toggleHideStories}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
story={story.storyView}
|
||||
viewUserStories={viewUserStories}
|
||||
|
|
|
@ -71,6 +71,7 @@ export type PropsType = {
|
|||
| 'toggleGroupsForStorySend'
|
||||
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
|
||||
| 'toggleSignalConnectionsModal'
|
||||
| 'onMediaPlaybackStart'
|
||||
> &
|
||||
Pick<
|
||||
TextStoryCreatorPropsType,
|
||||
|
@ -104,6 +105,7 @@ export function StoryCreator({
|
|||
onSetSkinTone,
|
||||
onUseEmoji,
|
||||
onViewersUpdated,
|
||||
onMediaPlaybackStart,
|
||||
ourConversationId,
|
||||
processAttachment,
|
||||
recentEmojis,
|
||||
|
@ -194,6 +196,7 @@ export function StoryCreator({
|
|||
setDraftAttachment(undefined);
|
||||
}}
|
||||
onViewersUpdated={onViewersUpdated}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
setMyStoriesToAllSignalConnections={
|
||||
setMyStoriesToAllSignalConnections
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { PropsType } from './StoryImage';
|
||||
import { StoryImage } from './StoryImage';
|
||||
|
@ -32,6 +33,7 @@ function getDefaultProps(): PropsType {
|
|||
label: 'A story',
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
storyId: uuid(),
|
||||
onMediaPlaybackStart: noop,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ export type PropsType = {
|
|||
readonly moduleClassName?: string;
|
||||
readonly queueStoryDownload: (storyId: string) => unknown;
|
||||
readonly storyId: string;
|
||||
readonly onMediaPlaybackStart: () => void;
|
||||
};
|
||||
|
||||
export function StoryImage({
|
||||
|
@ -50,6 +51,7 @@ export function StoryImage({
|
|||
moduleClassName,
|
||||
queueStoryDownload,
|
||||
storyId,
|
||||
onMediaPlaybackStart,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const shouldDownloadAttachment =
|
||||
(!isDownloaded(attachment) && !isDownloading(attachment)) ||
|
||||
|
@ -72,9 +74,10 @@ export function StoryImage({
|
|||
if (isPaused) {
|
||||
videoRef.current.pause();
|
||||
} else {
|
||||
onMediaPlaybackStart();
|
||||
void videoRef.current.play();
|
||||
}
|
||||
}, [isPaused]);
|
||||
}, [isPaused, onMediaPlaybackStart]);
|
||||
|
||||
useEffect(() => {
|
||||
setHasImgError(false);
|
||||
|
|
|
@ -28,6 +28,7 @@ export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
|
|||
onGoToConversation: (conversationId: string) => unknown;
|
||||
onHideStory: (conversationId: string) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
story: StoryViewType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
};
|
||||
|
@ -88,6 +89,7 @@ export function StoryListItem({
|
|||
isHidden,
|
||||
onGoToConversation,
|
||||
onHideStory,
|
||||
onMediaPlaybackStart,
|
||||
queueStoryDownload,
|
||||
story,
|
||||
viewUserStories,
|
||||
|
@ -195,6 +197,7 @@ export function StoryListItem({
|
|||
moduleClassName="StoryListItem__previews--image"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={story.messageId}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
|
|
|
@ -96,6 +96,7 @@ export type PropsType = {
|
|||
story: StoryViewType
|
||||
) => unknown;
|
||||
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
recentEmojis?: ReadonlyArray<string>;
|
||||
|
@ -148,6 +149,7 @@ export function StoryViewer({
|
|||
onSetSkinTone,
|
||||
onTextTooLong,
|
||||
onUseEmoji,
|
||||
onMediaPlaybackStart,
|
||||
preferredReactionEmoji,
|
||||
queueStoryDownload,
|
||||
recentEmojis,
|
||||
|
@ -618,6 +620,7 @@ export function StoryViewer({
|
|||
moduleClassName="StoryViewer__story"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={messageId}
|
||||
onMediaPlaybackStart={onMediaPlaybackStart}
|
||||
>
|
||||
{reactionEmoji && (
|
||||
<div className="StoryViewer__animated-emojis">
|
||||
|
|
|
@ -14,7 +14,7 @@ const MAX_AUDIO_DURATION = 15 * 60; // 15 minutes
|
|||
|
||||
export type ComputePeaksResult = {
|
||||
duration: number;
|
||||
peaks: ReadonlyArray<number>;
|
||||
peaks: ReadonlyArray<number>; // 0 < peak < 1
|
||||
};
|
||||
|
||||
export type Contents = {
|
||||
|
@ -174,9 +174,10 @@ const globalContents: Contents = {
|
|||
computePeaks,
|
||||
};
|
||||
|
||||
export const GlobalAudioContext = React.createContext<Contents>(globalContents);
|
||||
export const VoiceNotesPlaybackContext =
|
||||
React.createContext<Contents>(globalContents);
|
||||
|
||||
export type GlobalAudioProps = {
|
||||
export type VoiceNotesPlaybackProps = {
|
||||
conversationId: string | undefined;
|
||||
isPaused: boolean;
|
||||
children?: React.ReactNode | React.ReactChildren;
|
||||
|
@ -187,21 +188,12 @@ export type GlobalAudioProps = {
|
|||
* A global context that holds Audio, AudioContext, LRU instances that are used
|
||||
* inside the conversation by ts/components/conversation/MessageAudio.tsx
|
||||
*/
|
||||
export function GlobalAudioProvider({
|
||||
conversationId,
|
||||
export function VoiceNotesPlaybackProvider({
|
||||
children,
|
||||
unloadMessageAudio,
|
||||
}: GlobalAudioProps): JSX.Element {
|
||||
// When moving between conversations - stop audio
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
unloadMessageAudio();
|
||||
};
|
||||
}, [conversationId, unloadMessageAudio]);
|
||||
|
||||
}: VoiceNotesPlaybackProps): JSX.Element {
|
||||
return (
|
||||
<GlobalAudioContext.Provider value={globalContents}>
|
||||
<VoiceNotesPlaybackContext.Provider value={globalContents}>
|
||||
{children}
|
||||
</GlobalAudioContext.Provider>
|
||||
</VoiceNotesPlaybackContext.Provider>
|
||||
);
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { SmartMiniPlayer } from '../../state/smart/MiniPlayer';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
|
@ -86,6 +87,7 @@ export function ConversationView({
|
|||
{renderConversationHeader()}
|
||||
</div>
|
||||
<div className="ConversationView__pane main panel">
|
||||
<SmartMiniPlayer />
|
||||
<div className="ConversationView__timeline--container">
|
||||
<div aria-live="polite" className="ConversationView__timeline">
|
||||
{renderTimeline()}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useRef, useEffect, useState } from 'react';
|
||||
import type { RefObject, ReactNode } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
|
@ -13,15 +13,21 @@ import type { PushPanelForConversationActionType } from '../../state/ducks/conve
|
|||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
|
||||
import type { ComputePeaksResult } from '../GlobalAudioContext';
|
||||
import type { ComputePeaksResult } from '../VoiceNotesPlaybackContext';
|
||||
import { MessageMetadata } from './MessageMetadata';
|
||||
import * as log from '../../logging/log';
|
||||
import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer';
|
||||
import { PlaybackRateButton } from '../PlaybackRateButton';
|
||||
import { durationToPlaybackText } from '../../util/durationToPlaybackText';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
active: ActiveAudioPlayerStateType | undefined;
|
||||
active:
|
||||
| Pick<
|
||||
ActiveAudioPlayerStateType,
|
||||
'currentTime' | 'duration' | 'playing' | 'playbackRate'
|
||||
>
|
||||
| undefined;
|
||||
buttonRef: RefObject<HTMLButtonElement>;
|
||||
renderingContext: string;
|
||||
i18n: LocalizerType;
|
||||
attachment: AttachmentType;
|
||||
collapseMetadata: boolean;
|
||||
|
@ -33,7 +39,6 @@ export type OwnProps = Readonly<{
|
|||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
conversationId: string;
|
||||
played: boolean;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
|
@ -41,34 +46,25 @@ export type OwnProps = Readonly<{
|
|||
kickOffAttachmentDownload(): void;
|
||||
onCorrupted(): void;
|
||||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||
onPlayMessage: (id: string, position: number) => void;
|
||||
}>;
|
||||
|
||||
export type DispatchProps = Readonly<{
|
||||
loadAndPlayMessageAudio: (
|
||||
id: string,
|
||||
url: string,
|
||||
context: string,
|
||||
position: number,
|
||||
isConsecutive: boolean
|
||||
) => void;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
setCurrentTime: (currentTime: number) => void;
|
||||
setPlaybackRate: (conversationId: string, rate: number) => void;
|
||||
setPlaybackRate: (rate: number) => void;
|
||||
setIsPlaying: (value: boolean) => void;
|
||||
}>;
|
||||
|
||||
export type Props = OwnProps & DispatchProps;
|
||||
|
||||
type ButtonProps = {
|
||||
variant: 'play' | 'playback-rate';
|
||||
mod?: string;
|
||||
label: string;
|
||||
visible?: boolean;
|
||||
animateClick?: boolean;
|
||||
onClick: () => void;
|
||||
onMouseDown?: () => void;
|
||||
onMouseUp?: () => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
enum State {
|
||||
|
@ -92,8 +88,6 @@ const REWIND_BAR_COUNT = 2;
|
|||
const SMALL_INCREMENT = 1;
|
||||
const BIG_INCREMENT = 5;
|
||||
|
||||
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
||||
|
||||
const SPRING_CONFIG = {
|
||||
mass: 0.5,
|
||||
tension: 350,
|
||||
|
@ -103,48 +97,16 @@ const SPRING_CONFIG = {
|
|||
|
||||
const DOT_DIV_WIDTH = 14;
|
||||
|
||||
// Utils
|
||||
|
||||
const timeToText = (time: number): string => {
|
||||
const hours = Math.floor(time / 3600);
|
||||
let minutes = Math.floor((time % 3600) / 60).toString();
|
||||
let seconds = Math.floor(time % 60).toString();
|
||||
|
||||
if (hours !== 0 && minutes.length < 2) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
|
||||
if (seconds.length < 2) {
|
||||
seconds = `0${seconds}`;
|
||||
}
|
||||
|
||||
return hours ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles animations, key events, and stopping event propagation
|
||||
* for play button and playback rate button
|
||||
*/
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
/** Handles animations, key events, and stopping event propagation */
|
||||
const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function ButtonInner(props, ref) {
|
||||
const {
|
||||
variant,
|
||||
mod,
|
||||
label,
|
||||
children,
|
||||
onClick,
|
||||
visible = true,
|
||||
animateClick = true,
|
||||
} = props;
|
||||
const [isDown, setIsDown] = useState(false);
|
||||
|
||||
const { mod, label, onClick, visible = true } = props;
|
||||
const [animProps] = useSpring(
|
||||
{
|
||||
config: SPRING_CONFIG,
|
||||
to:
|
||||
isDown && animateClick ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
||||
to: { scale: visible ? 1 : 0 },
|
||||
},
|
||||
[visible, isDown, animateClick]
|
||||
[visible]
|
||||
);
|
||||
|
||||
// Clicking button toggle playback
|
||||
|
@ -178,19 +140,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
type="button"
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
`${CSS_BASE}__${variant}-button`,
|
||||
mod ? `${CSS_BASE}__${variant}-button--${mod}` : undefined
|
||||
`${CSS_BASE}__play-button`,
|
||||
mod ? `${CSS_BASE}__play-button--${mod}` : undefined
|
||||
)}
|
||||
onClick={onButtonClick}
|
||||
onKeyDown={onButtonKeyDown}
|
||||
onMouseDown={() => setIsDown(true)}
|
||||
onMouseUp={() => setIsDown(false)}
|
||||
onMouseLeave={() => setIsDown(false)}
|
||||
tabIndex={0}
|
||||
aria-label={label}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
/>
|
||||
</animated.div>
|
||||
);
|
||||
}
|
||||
|
@ -237,10 +194,9 @@ function PlayedDot({
|
|||
* toggle Play/Pause button.
|
||||
*
|
||||
* A global audio player is used for playback and access is managed by the
|
||||
* `activeAudioID` and `activeAudioContext` properties. Whenever both
|
||||
* `activeAudioID` and `activeAudioContext` are equal to `id` and `context`
|
||||
* respectively the instance of the `MessageAudio` assumes the ownership of the
|
||||
* `Audio` instance and fully manages it.
|
||||
* `active.content.current.id` and the `active.content.context` properties. Whenever both
|
||||
* are equal to `id` and `context` respectively the instance of the `MessageAudio`
|
||||
* assumes the ownership of the `Audio` instance and fully manages it.
|
||||
*
|
||||
* `context` is required for displaying separate MessageAudio instances in
|
||||
* MessageDetails and Message React components.
|
||||
|
@ -250,10 +206,8 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
active,
|
||||
buttonRef,
|
||||
i18n,
|
||||
renderingContext,
|
||||
attachment,
|
||||
collapseMetadata,
|
||||
conversationId,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
|
||||
|
@ -270,7 +224,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
onCorrupted,
|
||||
computePeaks,
|
||||
setPlaybackRate,
|
||||
loadAndPlayMessageAudio,
|
||||
onPlayMessage,
|
||||
pushPanelForConversation,
|
||||
setCurrentTime,
|
||||
setIsPlaying,
|
||||
|
@ -373,10 +327,9 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
if (active) {
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
loadAndPlayMessageAudio(id, attachment.url, renderingContext, 0, false);
|
||||
onPlayMessage(id, 0);
|
||||
}
|
||||
} else {
|
||||
// stop
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
@ -401,13 +354,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
}
|
||||
|
||||
if (attachment.url) {
|
||||
loadAndPlayMessageAudio(
|
||||
id,
|
||||
attachment.url,
|
||||
renderingContext,
|
||||
progress,
|
||||
false
|
||||
);
|
||||
onPlayMessage(id, progress);
|
||||
} else {
|
||||
log.warn('Waveform clicked on attachment with no url');
|
||||
}
|
||||
|
@ -467,7 +414,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
aria-valuenow={currentTimeOrZero}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={duration}
|
||||
aria-valuetext={timeToText(currentTimeOrZero)}
|
||||
aria-valuetext={durationToPlaybackText(currentTimeOrZero)}
|
||||
>
|
||||
{peaks.map((peak, i) => {
|
||||
let height = Math.max(BAR_MIN_HEIGHT, BAR_MAX_HEIGHT * peak);
|
||||
|
@ -512,26 +459,22 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
);
|
||||
} else if (state === State.NotDownloaded) {
|
||||
button = (
|
||||
<Button
|
||||
<PlaybackButton
|
||||
ref={buttonRef}
|
||||
variant="play"
|
||||
mod="download"
|
||||
label="MessageAudio--download"
|
||||
animateClick={false}
|
||||
onClick={kickOffAttachmentDownload}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// State.Normal
|
||||
button = (
|
||||
<Button
|
||||
<PlaybackButton
|
||||
ref={buttonRef}
|
||||
variant="play"
|
||||
mod={isPlaying ? 'pause' : 'play'}
|
||||
label={
|
||||
isPlaying ? i18n('MessageAudio--pause') : i18n('MessageAudio--play')
|
||||
}
|
||||
animateClick={false}
|
||||
onClick={toggleIsPlaying}
|
||||
/>
|
||||
);
|
||||
|
@ -539,20 +482,6 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
|
||||
const countDown = Math.max(0, duration - (active?.currentTime ?? 0));
|
||||
|
||||
const nextPlaybackRate = (currentRate: number): number => {
|
||||
// cycle through the rates
|
||||
return PLAYBACK_RATES[
|
||||
(PLAYBACK_RATES.indexOf(currentRate) + 1) % PLAYBACK_RATES.length
|
||||
];
|
||||
};
|
||||
|
||||
const playbackRateLabels: { [key: number]: string } = {
|
||||
1: i18n('MessageAudio--playbackRate1'),
|
||||
1.5: i18n('MessageAudio--playbackRate1p5'),
|
||||
2: i18n('MessageAudio--playbackRate2'),
|
||||
0.5: i18n('MessageAudio--playbackRatep5'),
|
||||
};
|
||||
|
||||
const metadata = (
|
||||
<div className={`${CSS_BASE}__metadata`}>
|
||||
<div
|
||||
|
@ -562,7 +491,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
`${CSS_BASE}__countdown--${played ? 'played' : 'unplayed'}`
|
||||
)}
|
||||
>
|
||||
{timeToText(countDown)}
|
||||
{durationToPlaybackText(countDown)}
|
||||
</div>
|
||||
|
||||
<div className={`${CSS_BASE}__controls`}>
|
||||
|
@ -570,21 +499,20 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||
played={played}
|
||||
onHide={() => setIsPlayedDotVisible(false)}
|
||||
/>
|
||||
<Button
|
||||
variant="playback-rate"
|
||||
label={playbackRateLabels[active?.playbackRate ?? 1]}
|
||||
|
||||
<PlaybackRateButton
|
||||
i18n={i18n}
|
||||
variant={`message-${direction}`}
|
||||
playbackRate={active?.playbackRate}
|
||||
visible={isPlaying && (!played || !isPlayedDotVisible)}
|
||||
onClick={() => {
|
||||
if (active) {
|
||||
setPlaybackRate(
|
||||
conversationId,
|
||||
nextPlaybackRate(active.playbackRate)
|
||||
PlaybackRateButton.nextPlaybackRate(active.playbackRate)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{playbackRateLabels[active?.playbackRate ?? 1]}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!withContentBelow && !collapseMetadata && (
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../../types/MIME';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { MessageAudio } from './MessageAudio';
|
||||
import { computePeaks } from '../GlobalAudioContext';
|
||||
import { computePeaks } from '../VoiceNotesPlaybackContext';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { pngUrl } from '../../storybook/Fixtures';
|
||||
|
@ -89,6 +89,10 @@ const Template: Story<Partial<Props>> = args => {
|
|||
});
|
||||
};
|
||||
|
||||
const messageIdToAudioUrl = {
|
||||
'incompetech-com-Agnus-Dei-X': '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
|
||||
};
|
||||
|
||||
function getJoyReaction() {
|
||||
return {
|
||||
emoji: '😂',
|
||||
|
@ -152,14 +156,9 @@ function MessageAudioContainer({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const loadAndPlayMessageAudio = (
|
||||
_id: string,
|
||||
url: string,
|
||||
_context: string,
|
||||
position: number
|
||||
) => {
|
||||
const handlePlayMessage = (id: string, position: number) => {
|
||||
if (!active) {
|
||||
audio.src = url;
|
||||
audio.src = messageIdToAudioUrl[id as keyof typeof messageIdToAudioUrl];
|
||||
setIsActive(true);
|
||||
}
|
||||
if (!playing) {
|
||||
|
@ -176,7 +175,7 @@ function MessageAudioContainer({
|
|||
}
|
||||
};
|
||||
|
||||
const setPlaybackRateAction = (_conversationId: string, rate: number) => {
|
||||
const setPlaybackRateAction = (rate: number) => {
|
||||
audio.playbackRate = rate;
|
||||
setPlaybackRate(rate);
|
||||
};
|
||||
|
@ -202,14 +201,12 @@ function MessageAudioContainer({
|
|||
return (
|
||||
<MessageAudio
|
||||
{...props}
|
||||
conversationId="some-conversation-id"
|
||||
active={active}
|
||||
computePeaks={computePeaks}
|
||||
id="storybook"
|
||||
loadAndPlayMessageAudio={loadAndPlayMessageAudio}
|
||||
onPlayMessage={handlePlayMessage}
|
||||
played={_played}
|
||||
pushPanelForConversation={action('pushPanelForConversation')}
|
||||
renderingContext="storybook"
|
||||
setCurrentTime={setCurrentTimeAction}
|
||||
setIsPlaying={setIsPlayingAction}
|
||||
setPlaybackRate={setPlaybackRateAction}
|
||||
|
@ -427,11 +424,12 @@ export function EmojiMessages(): JSX.Element {
|
|||
<br />
|
||||
<TimelineMessage
|
||||
{...createProps({
|
||||
id: 'incompetech-com-Agnus-Dei-X',
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: AUDIO_MP3,
|
||||
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
|
||||
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
|
||||
url: messageIdToAudioUrl['incompetech-com-Agnus-Dei-X'],
|
||||
}),
|
||||
],
|
||||
text: '😀',
|
||||
|
@ -1353,11 +1351,12 @@ export const _Audio = (): JSX.Element => {
|
|||
const [isPlayed, setIsPlayed] = React.useState(false);
|
||||
|
||||
const messageProps = createProps({
|
||||
id: 'incompetech-com-Agnus-Dei-X',
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
contentType: AUDIO_MP3,
|
||||
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
|
||||
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
|
||||
url: messageIdToAudioUrl['incompetech-com-Agnus-Dei-X'],
|
||||
path: 'somepath',
|
||||
}),
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue