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

@ -4495,21 +4495,21 @@
"message": "Playback time of audio attachment",
"description": "Aria label for audio attachment's playback time slider"
},
"MessageAudio--playbackRate1x": {
"message": "1x",
"description": "Button in the voice note message widget that shows the current playback rate of 1x (regular speed) and allows the user to toggle to the next rate"
"MessageAudio--playbackRate1": {
"message": "1",
"description": "Button in the voice note message widget that shows the current playback rate of 1x (regular speed) and allows the user to toggle to the next rate. Don't include the 'x'."
},
"MessageAudio--playbackRate1p5x": {
"message": "1.5x",
"description": "Button in the voice note message widget that shows the current playback rate of 1.5x (%50 faster) and allows the user to toggle to the next rate"
"MessageAudio--playbackRate1p5": {
"message": "1.5",
"description": "Button in the voice note message widget that shows the current playback rate of 1.5x (%50 faster) and allows the user to toggle to the next rate. Don't include the 'x'."
},
"MessageAudio--playbackRate2x": {
"message": "2x",
"description": "Button in the voice note message widget that shows the current playback rate of 2x (double speed) and allows the user to toggle to the next rate"
"MessageAudio--playbackRate2": {
"message": "2",
"description": "Button in the voice note message widget that shows the current playback rate of 2x (double speed) and allows the user to toggle to the next rate. Don't include the 'x'."
},
"MessageAudio--playbackRatep5x": {
"message": ".5x",
"description": "Button in the voice note message widget that shows the current playback rate of .5x (half speed) and allows the user to toggle to the next rate"
"MessageAudio--playbackRatep5": {
"message": ".5",
"description": "Button in the voice note message widget that shows the current playback rate of .5x (half speed) and allows the user to toggle to the next rate. Don't include the 'x'."
},
"emptyInboxMessage": {
"message": "Click the $composeIcon$ above and search for your contacts or groups to message.",

10
images/icons/v2/x-8.svg Normal file
View file

@ -0,0 +1,10 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6_2799)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.07699 3.99999L7.99998 1.07699L6.92299 0L4 2.923L1.07701 8.86908e-06L1.51392e-05 1.077L2.923 3.99999L0 6.92299L1.07699 7.99998L4 5.07698L6.92301 7.99999L8 6.923L5.07699 3.99999Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6_2799">
<rect width="8" height="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View file

@ -54,6 +54,7 @@ $audio-attachment-button-margin-small: 4px;
align-items: center;
justify-content: flex-start;
transition: width 100ms ease-out;
width: 14px;
&:before {
content: '';
@ -61,17 +62,10 @@ $audio-attachment-button-margin-small: 4px;
width: 6px;
height: 6px;
border-radius: 100%;
transition: background 100ms ease-out;
}
&--unplayed {
width: 14px;
}
&--played {
width: 0px;
}
.module-message__audio-attachment--incoming & {
&--unplayed:before {
&:before {
@include light-theme {
background: $color-gray-60;
}
@ -85,9 +79,6 @@ $audio-attachment-button-margin-small: 4px;
background: $color-white-alpha-80;
}
}
&--played:before {
background: transparent;
}
}
.module-message__audio-attachment__playback-rate-button {
@ -120,9 +111,33 @@ $audio-attachment-button-margin-small: 4px;
color: $color-white-alpha-80;
background: $color-white-alpha-20;
}
&::after {
content: '';
display: inline-block;
width: 8px;
height: 8px;
margin-left: 2px;
@mixin x-icon($color) {
@include color-svg('../images/icons/v2/x-8.svg', $color, false);
}
.module-message__audio-attachment--incoming & {
@include light-theme {
@include x-icon($color-gray-60);
}
@include dark-theme {
@include x-icon($color-gray-25);
}
}
.module-message__audio-attachment--outgoing & {
@include x-icon($color-white-alpha-80);
}
}
}
.module-message__audio-attachment__button,
.module-message__audio-attachment__play-button,
.module-message__audio-attachment__spinner {
@include button-reset;
@ -195,7 +210,7 @@ $audio-attachment-button-margin-small: 4px;
outline: 0;
}
.module-message__audio-attachment__button,
.module-message__audio-attachment__play-button,
.module-message__audio-attachment__playback-rate-button,
.module-message__audio-attachment__spinner,
.module-message__audio-attachment__waveform {

View file

@ -163,7 +163,10 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
audio.play();
setPlaying(true);
}
audio.currentTime = audio.duration * position;
if (!Number.isNaN(audio.duration)) {
audio.currentTime = audio.duration * position;
}
if (!Number.isNaN(audio.currentTime)) {
setCurrentTime(audio.currentTime);
}

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 && (

View file

@ -37,8 +37,6 @@ export type Props = {
textPending?: boolean;
timestamp: number;
buttonRef: React.RefObject<HTMLButtonElement>;
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
kickOffAttachmentDownload(): void;
onCorrupted(): void;

View file

@ -7,7 +7,10 @@ import { GlobalAudioContext } from '../../components/GlobalAudioContext';
import type { Props as MessageAudioProps } from './MessageAudio';
import { SmartMessageAudio } from './MessageAudio';
type AudioAttachmentProps = Omit<MessageAudioProps, 'computePeaks'>;
type AudioAttachmentProps = Omit<
MessageAudioProps,
'computePeaks' | 'buttonRef'
>;
export function renderAudioAttachment(
props: AudioAttachmentProps