Added a playback speed button on voice notes
This commit is contained in:
parent
bb9a7113f1
commit
13046dc020
3 changed files with 115 additions and 21 deletions
|
@ -6223,6 +6223,22 @@
|
||||||
"message": "Playback time of audio attachment",
|
"message": "Playback time of audio attachment",
|
||||||
"description": "Aria label for audio attachment's playback time slider"
|
"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--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--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--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"
|
||||||
|
},
|
||||||
"emptyInboxMessage": {
|
"emptyInboxMessage": {
|
||||||
"message": "Click the $composeIcon$ above and search for your contacts or groups to message.",
|
"message": "Click the $composeIcon$ above and search for your contacts or groups to message.",
|
||||||
"description": "Shown in the left-pane when the inbox is empty",
|
"description": "Shown in the left-pane when the inbox is empty",
|
||||||
|
|
|
@ -28,6 +28,7 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
border-color: $color-black-alpha-20;
|
border-color: $color-black-alpha-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
border-color: $color-white-alpha-20;
|
border-color: $color-white-alpha-20;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +43,25 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__audio-attachment__controls {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: right;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__audio-attachment__playback-rate-button {
|
||||||
|
@include button-reset;
|
||||||
|
@include font-body-2-bold;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px; /* tiny override */
|
||||||
|
padding: 1px 7px;
|
||||||
|
margin: -1px 4px;
|
||||||
|
background: $color-white-alpha-20;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__audio-attachment__button,
|
.module-message__audio-attachment__button,
|
||||||
.module-message__audio-attachment__spinner {
|
.module-message__audio-attachment__spinner {
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
@ -91,6 +111,7 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
|
|
||||||
@include all-audio-icons($color-gray-60);
|
@include all-audio-icons($color-gray-60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
background: $color-gray-60;
|
background: $color-gray-60;
|
||||||
|
|
||||||
|
@ -115,6 +136,7 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__audio-attachment__button,
|
.module-message__audio-attachment__button,
|
||||||
|
.module-message__audio-attachment__playback-rate-button,
|
||||||
.module-message__audio-attachment__spinner,
|
.module-message__audio-attachment__spinner,
|
||||||
.module-message__audio-attachment__waveform {
|
.module-message__audio-attachment__waveform {
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -146,12 +168,15 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
.module-message__audio-attachment--incoming & {
|
.module-message__audio-attachment--incoming & {
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
background: $color-black-alpha-40;
|
background: $color-black-alpha-40;
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
background: $color-black-alpha-80;
|
background: $color-black-alpha-80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
background: $color-white-alpha-40;
|
background: $color-white-alpha-40;
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
background: $color-white-alpha-70;
|
background: $color-white-alpha-70;
|
||||||
}
|
}
|
||||||
|
@ -160,6 +185,7 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
|
|
||||||
.module-message__audio-attachment--outgoing & {
|
.module-message__audio-attachment--outgoing & {
|
||||||
background: $color-white-alpha-40;
|
background: $color-white-alpha-40;
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
background: $color-white-alpha-80;
|
background: $color-white-alpha-80;
|
||||||
}
|
}
|
||||||
|
@ -205,13 +231,16 @@ $audio-attachment-button-margin-small: 4px;
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
$color: $color-black-alpha-60;
|
$color: $color-black-alpha-60;
|
||||||
color: $color;
|
color: $color;
|
||||||
|
|
||||||
&--unplayed:after {
|
&--unplayed:after {
|
||||||
background: $color;
|
background: $color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
$color: $color-white-alpha-80;
|
$color: $color-white-alpha-80;
|
||||||
color: $color;
|
color: $color;
|
||||||
|
|
||||||
&--unplayed:after {
|
&--unplayed:after {
|
||||||
background: $color;
|
background: $color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,8 @@ const REWIND_BAR_COUNT = 2;
|
||||||
const SMALL_INCREMENT = 1;
|
const SMALL_INCREMENT = 1;
|
||||||
const BIG_INCREMENT = 5;
|
const BIG_INCREMENT = 5;
|
||||||
|
|
||||||
|
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
const timeToText = (time: number): string => {
|
const timeToText = (time: number): string => {
|
||||||
|
@ -144,6 +146,7 @@ type StateType = Readonly<{
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
lastAriaTime: number;
|
lastAriaTime: number;
|
||||||
|
playbackRate: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ActionType = Readonly<
|
type ActionType = Readonly<
|
||||||
|
@ -155,6 +158,10 @@ type ActionType = Readonly<
|
||||||
type: 'SET_CURRENT_TIME';
|
type: 'SET_CURRENT_TIME';
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_PLAYBACK_RATE';
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function reducer(state: StateType, action: ActionType): StateType {
|
function reducer(state: StateType, action: ActionType): StateType {
|
||||||
|
@ -168,6 +175,9 @@ function reducer(state: StateType, action: ActionType): StateType {
|
||||||
if (action.type === 'SET_CURRENT_TIME') {
|
if (action.type === 'SET_CURRENT_TIME') {
|
||||||
return { ...state, currentTime: action.value };
|
return { ...state, currentTime: action.value };
|
||||||
}
|
}
|
||||||
|
if (action.type === 'SET_PLAYBACK_RATE') {
|
||||||
|
return { ...state, playbackRate: action.value };
|
||||||
|
}
|
||||||
throw missingCaseError(action);
|
throw missingCaseError(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,14 +232,13 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
activeAudioID === id && activeAudioContext === renderingContext;
|
activeAudioID === id && activeAudioContext === renderingContext;
|
||||||
|
|
||||||
const waveformRef = useRef<HTMLDivElement | null>(null);
|
const waveformRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [{ isPlaying, currentTime, lastAriaTime }, dispatch] = useReducer(
|
const [{ isPlaying, currentTime, lastAriaTime, playbackRate }, dispatch] =
|
||||||
reducer,
|
useReducer(reducer, {
|
||||||
{
|
|
||||||
isPlaying: isActive && !(audio.paused || audio.ended),
|
isPlaying: isActive && !(audio.paused || audio.ended),
|
||||||
currentTime: isActive ? audio.currentTime : 0,
|
currentTime: isActive ? audio.currentTime : 0,
|
||||||
lastAriaTime: isActive ? audio.currentTime : 0,
|
lastAriaTime: isActive ? audio.currentTime : 0,
|
||||||
}
|
playbackRate: isActive ? audio.playbackRate : 1,
|
||||||
);
|
});
|
||||||
|
|
||||||
const setIsPlaying = useCallback(
|
const setIsPlaying = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
|
@ -245,6 +254,13 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setPlaybackRate = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
dispatch({ type: 'SET_PLAYBACK_RATE', value });
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
// NOTE: Avoid division by zero
|
// NOTE: Avoid division by zero
|
||||||
const [duration, setDuration] = useState(1e-23);
|
const [duration, setDuration] = useState(1e-23);
|
||||||
|
|
||||||
|
@ -397,6 +413,8 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio.playbackRate = playbackRate;
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
if (!audio.paused) {
|
if (!audio.paused) {
|
||||||
return;
|
return;
|
||||||
|
@ -411,7 +429,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
log.info('MessageAudio: pausing playback for', id);
|
log.info('MessageAudio: pausing playback for', id);
|
||||||
audio.pause();
|
audio.pause();
|
||||||
}
|
}
|
||||||
}, [id, audio, isActive, isPlaying, currentTime]);
|
}, [id, audio, isActive, isPlaying, currentTime, playbackRate]);
|
||||||
|
|
||||||
const toggleIsPlaying = () => {
|
const toggleIsPlaying = () => {
|
||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
|
@ -590,6 +608,20 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
const countDown = Math.max(0, duration - currentTime);
|
const countDown = Math.max(0, duration - currentTime);
|
||||||
|
|
||||||
|
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--playbackRate1x'),
|
||||||
|
1.5: i18n('MessageAudio--playbackRate1p5x'),
|
||||||
|
2: i18n('MessageAudio--playbackRate2x'),
|
||||||
|
0.5: i18n('MessageAudio--playbackRatep5x'),
|
||||||
|
};
|
||||||
|
|
||||||
const metadata = (
|
const metadata = (
|
||||||
<div className={`${CSS_BASE}__metadata`}>
|
<div className={`${CSS_BASE}__metadata`}>
|
||||||
<div
|
<div
|
||||||
|
@ -602,6 +634,22 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
{timeToText(countDown)}
|
{timeToText(countDown)}
|
||||||
</div>
|
</div>
|
||||||
{!withContentBelow && !collapseMetadata && (
|
{!withContentBelow && !collapseMetadata && (
|
||||||
|
<>
|
||||||
|
<div className={`${CSS_BASE}__controls`}>
|
||||||
|
{isPlaying && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(`${CSS_BASE}__playback-rate-button`)}
|
||||||
|
onClick={ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setPlaybackRate(nextPlaybackRate(playbackRate));
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{playbackRateLabels[playbackRate]}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<MessageMetadata
|
<MessageMetadata
|
||||||
direction={direction}
|
direction={direction}
|
||||||
expirationLength={expirationLength}
|
expirationLength={expirationLength}
|
||||||
|
@ -617,6 +665,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
textPending={textPending}
|
textPending={textPending}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue