Animated voice-note controls and used image x for playback rate

This commit is contained in:
Alvaro 2022-09-19 18:28:10 -06:00 committed by GitHub
parent f9453c64dd
commit b4c9c3051b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 68 deletions

View file

@ -4,6 +4,7 @@
import React, { useRef, useEffect, useState } from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
import { animated, useSpring } from '@react-spring/web';
import type { LocalizerType } from '../../types/Util';
import type { AttachmentType } from '../../types/Attachment';
@ -35,7 +36,6 @@ export type OwnProps = Readonly<{
status?: MessageStatusType;
textPending?: boolean;
timestamp: number;
buttonRef: React.RefObject<HTMLButtonElement>;
kickOffAttachmentDownload(): void;
onCorrupted(): void;
onFirstPlayed(): void;
@ -59,11 +59,13 @@ export type Props = OwnProps & DispatchProps;
type ButtonProps = {
i18n: LocalizerType;
buttonRef: React.RefObject<HTMLButtonElement>;
mod: string;
variant: 'play' | 'playback-rate';
mod?: string;
label: string;
visible?: boolean;
onClick: () => void;
onMouseDown?: () => void;
onMouseUp?: () => void;
};
enum State {
@ -89,6 +91,15 @@ const BIG_INCREMENT = 5;
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
const SPRING_DEFAULTS = {
mass: 0.5,
tension: 350,
friction: 20,
velocity: 0.01,
};
const DOT_DIV_WIDTH = 14;
// Utils
const timeToText = (time: number): string => {
@ -107,8 +118,28 @@ const timeToText = (time: number): string => {
return hours ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
};
/**
* Handles animations, key events, and stoping event propagation
* for play button and playback rate button
*/
const Button: React.FC<ButtonProps> = props => {
const { i18n, buttonRef, mod, label, onClick } = props;
const {
i18n,
variant,
mod,
label,
children,
onClick,
visible = true,
} = props;
const [isDown, setIsDown] = useState(false);
const animProps = useSpring({
...SPRING_DEFAULTS,
from: isDown ? { scale: 1 } : { scale: 0 },
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
});
// Clicking button toggle playback
const onButtonClick = (event: React.MouseEvent) => {
event.stopPropagation();
@ -129,17 +160,59 @@ const Button: React.FC<ButtonProps> = props => {
};
return (
<button
type="button"
ref={buttonRef}
<animated.div style={animProps}>
<button
type="button"
className={classNames(
`${CSS_BASE}__${variant}-button`,
mod ? `${CSS_BASE}__${variant}-button--${mod}` : undefined
)}
onClick={onButtonClick}
onKeyDown={onButtonKeyDown}
onMouseDown={() => setIsDown(true)}
onMouseUp={() => setIsDown(false)}
onMouseLeave={() => setIsDown(false)}
tabIndex={0}
aria-label={i18n(label)}
>
{children}
</button>
</animated.div>
);
};
const PlayedDot = ({
played,
onHide,
}: {
played: boolean;
onHide: () => void;
}) => {
const start = played ? 1 : 0;
const end = played ? 0 : 1;
const [animProps] = useSpring(
{
...SPRING_DEFAULTS,
from: { scale: start, opacity: start, width: start },
to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH },
onRest: () => {
if (played) {
onHide();
}
},
},
[played]
);
return (
<animated.div
style={animProps}
aria-hidden="true"
className={classNames(
`${CSS_BASE}__button`,
`${CSS_BASE}__button--${mod}`
`${CSS_BASE}__dot`,
`${CSS_BASE}__dot--${played ? 'played' : 'unplayed'}`
)}
onClick={onButtonClick}
onKeyDown={onButtonKeyDown}
tabIndex={0}
aria-label={i18n(label)}
/>
);
};
@ -178,7 +251,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
textPending,
timestamp,
buttonRef,
kickOffAttachmentDownload,
onCorrupted,
onFirstPlayed,
@ -193,6 +265,8 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
const isPlaying = active?.playing ?? false;
const [isPlayedDotVisible, setIsPlayedDotVisible] = React.useState(!played);
// if it's playing, use the duration passed as props as it might
// change during loading/playback (?)
// NOTE: Avoid division by zero
@ -431,7 +505,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
button = (
<Button
i18n={i18n}
buttonRef={buttonRef}
variant="play"
mod="download"
label="MessageAudio--download"
onClick={kickOffAttachmentDownload}
@ -442,7 +516,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
button = (
<Button
i18n={i18n}
buttonRef={buttonRef}
variant="play"
mod={isPlaying ? 'pause' : 'play'}
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
onClick={toggleIsPlaying}
@ -460,10 +534,10 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
};
const playbackRateLabels: { [key: number]: string } = {
1: i18n('MessageAudio--playbackRate1x'),
1.5: i18n('MessageAudio--playbackRate1p5x'),
2: i18n('MessageAudio--playbackRate2x'),
0.5: i18n('MessageAudio--playbackRatep5x'),
1: i18n('MessageAudio--playbackRate1'),
1.5: i18n('MessageAudio--playbackRate1p5'),
2: i18n('MessageAudio--playbackRate2'),
0.5: i18n('MessageAudio--playbackRatep5'),
};
const metadata = (
@ -479,29 +553,26 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
</div>
<div className={`${CSS_BASE}__controls`}>
<div
aria-hidden="true"
className={classNames(
`${CSS_BASE}__dot`,
`${CSS_BASE}__dot--${played ? 'played' : 'unplayed'}`
)}
<PlayedDot
played={played}
onHide={() => setIsPlayedDotVisible(false)}
/>
{active && active.playing && (
<button
type="button"
className={classNames(`${CSS_BASE}__playback-rate-button`)}
onClick={ev => {
ev.stopPropagation();
<Button
variant="playback-rate"
i18n={i18n}
label={(active && playbackRateLabels[active.playbackRate]) ?? ''}
visible={isPlaying && (!played || (played && !isPlayedDotVisible))}
onClick={() => {
if (active) {
setPlaybackRate(
conversationId,
nextPlaybackRate(active.playbackRate)
);
}}
tabIndex={0}
>
{playbackRateLabels[active.playbackRate]}
</button>
)}
}
}}
>
{active && playbackRateLabels[active.playbackRate]}
</Button>
</div>
{!withContentBelow && !collapseMetadata && (