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",
|
||||
"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": {
|
||||
"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",
|
||||
|
|
|
@ -28,6 +28,7 @@ $audio-attachment-button-margin-small: 4px;
|
|||
@include light-theme {
|
||||
border-color: $color-black-alpha-20;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-color: $color-white-alpha-20;
|
||||
}
|
||||
|
@ -42,6 +43,25 @@ $audio-attachment-button-margin-small: 4px;
|
|||
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__spinner {
|
||||
@include button-reset;
|
||||
|
@ -91,6 +111,7 @@ $audio-attachment-button-margin-small: 4px;
|
|||
|
||||
@include all-audio-icons($color-gray-60);
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-60;
|
||||
|
||||
|
@ -115,6 +136,7 @@ $audio-attachment-button-margin-small: 4px;
|
|||
}
|
||||
|
||||
.module-message__audio-attachment__button,
|
||||
.module-message__audio-attachment__playback-rate-button,
|
||||
.module-message__audio-attachment__spinner,
|
||||
.module-message__audio-attachment__waveform {
|
||||
&:focus {
|
||||
|
@ -146,12 +168,15 @@ $audio-attachment-button-margin-small: 4px;
|
|||
.module-message__audio-attachment--incoming & {
|
||||
@include light-theme {
|
||||
background: $color-black-alpha-40;
|
||||
|
||||
&--active {
|
||||
background: $color-black-alpha-80;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-white-alpha-40;
|
||||
|
||||
&--active {
|
||||
background: $color-white-alpha-70;
|
||||
}
|
||||
|
@ -160,6 +185,7 @@ $audio-attachment-button-margin-small: 4px;
|
|||
|
||||
.module-message__audio-attachment--outgoing & {
|
||||
background: $color-white-alpha-40;
|
||||
|
||||
&--active {
|
||||
background: $color-white-alpha-80;
|
||||
}
|
||||
|
@ -205,13 +231,16 @@ $audio-attachment-button-margin-small: 4px;
|
|||
@include light-theme {
|
||||
$color: $color-black-alpha-60;
|
||||
color: $color;
|
||||
|
||||
&--unplayed:after {
|
||||
background: $color;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
$color: $color-white-alpha-80;
|
||||
color: $color;
|
||||
|
||||
&--unplayed:after {
|
||||
background: $color;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@ const REWIND_BAR_COUNT = 2;
|
|||
const SMALL_INCREMENT = 1;
|
||||
const BIG_INCREMENT = 5;
|
||||
|
||||
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
||||
|
||||
// Utils
|
||||
|
||||
const timeToText = (time: number): string => {
|
||||
|
@ -144,6 +146,7 @@ type StateType = Readonly<{
|
|||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
lastAriaTime: number;
|
||||
playbackRate: number;
|
||||
}>;
|
||||
|
||||
type ActionType = Readonly<
|
||||
|
@ -155,6 +158,10 @@ type ActionType = Readonly<
|
|||
type: 'SET_CURRENT_TIME';
|
||||
value: number;
|
||||
}
|
||||
| {
|
||||
type: 'SET_PLAYBACK_RATE';
|
||||
value: number;
|
||||
}
|
||||
>;
|
||||
|
||||
function reducer(state: StateType, action: ActionType): StateType {
|
||||
|
@ -168,6 +175,9 @@ function reducer(state: StateType, action: ActionType): StateType {
|
|||
if (action.type === 'SET_CURRENT_TIME') {
|
||||
return { ...state, currentTime: action.value };
|
||||
}
|
||||
if (action.type === 'SET_PLAYBACK_RATE') {
|
||||
return { ...state, playbackRate: action.value };
|
||||
}
|
||||
throw missingCaseError(action);
|
||||
}
|
||||
|
||||
|
@ -222,14 +232,13 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
activeAudioID === id && activeAudioContext === renderingContext;
|
||||
|
||||
const waveformRef = useRef<HTMLDivElement | null>(null);
|
||||
const [{ isPlaying, currentTime, lastAriaTime }, dispatch] = useReducer(
|
||||
reducer,
|
||||
{
|
||||
const [{ isPlaying, currentTime, lastAriaTime, playbackRate }, dispatch] =
|
||||
useReducer(reducer, {
|
||||
isPlaying: isActive && !(audio.paused || audio.ended),
|
||||
currentTime: isActive ? audio.currentTime : 0,
|
||||
lastAriaTime: isActive ? audio.currentTime : 0,
|
||||
}
|
||||
);
|
||||
playbackRate: isActive ? audio.playbackRate : 1,
|
||||
});
|
||||
|
||||
const setIsPlaying = useCallback(
|
||||
(value: boolean) => {
|
||||
|
@ -245,6 +254,13 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const setPlaybackRate = useCallback(
|
||||
(value: number) => {
|
||||
dispatch({ type: 'SET_PLAYBACK_RATE', value });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// NOTE: Avoid division by zero
|
||||
const [duration, setDuration] = useState(1e-23);
|
||||
|
||||
|
@ -397,6 +413,8 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
audio.playbackRate = playbackRate;
|
||||
|
||||
if (isPlaying) {
|
||||
if (!audio.paused) {
|
||||
return;
|
||||
|
@ -411,7 +429,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
log.info('MessageAudio: pausing playback for', id);
|
||||
audio.pause();
|
||||
}
|
||||
}, [id, audio, isActive, isPlaying, currentTime]);
|
||||
}, [id, audio, isActive, isPlaying, currentTime, playbackRate]);
|
||||
|
||||
const toggleIsPlaying = () => {
|
||||
setIsPlaying(!isPlaying);
|
||||
|
@ -590,6 +608,20 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
|
||||
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 = (
|
||||
<div className={`${CSS_BASE}__metadata`}>
|
||||
<div
|
||||
|
@ -602,21 +634,38 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
{timeToText(countDown)}
|
||||
</div>
|
||||
{!withContentBelow && !collapseMetadata && (
|
||||
<MessageMetadata
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
hasText={withContentBelow}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isShowingImage={false}
|
||||
isSticker={false}
|
||||
isTapToViewExpired={false}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
<>
|
||||
<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
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
hasText={withContentBelow}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isShowingImage={false}
|
||||
isSticker={false}
|
||||
isTapToViewExpired={false}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue