Animated voice-note controls and used image x for playback rate
This commit is contained in:
parent
f9453c64dd
commit
b4c9c3051b
7 changed files with 168 additions and 68 deletions
|
@ -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
10
images/icons/v2/x-8.svg
Normal 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 |
|
@ -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 {
|
||||
|
|
|
@ -163,7 +163,10 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
|||
audio.play();
|
||||
setPlaying(true);
|
||||
}
|
||||
|
||||
if (!Number.isNaN(audio.duration)) {
|
||||
audio.currentTime = audio.duration * position;
|
||||
}
|
||||
if (!Number.isNaN(audio.currentTime)) {
|
||||
setCurrentTime(audio.currentTime);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<animated.div style={animProps}>
|
||||
<button
|
||||
type="button"
|
||||
ref={buttonRef}
|
||||
className={classNames(
|
||||
`${CSS_BASE}__button`,
|
||||
`${CSS_BASE}__button--${mod}`
|
||||
`${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}__dot`,
|
||||
`${CSS_BASE}__dot--${played ? 'played' : 'unplayed'}`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue