diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d1686f71f5f..d1874b72633 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/stylesheets/components/MessageAudio.scss b/stylesheets/components/MessageAudio.scss index e166f91701a..b5bd5b11805 100644 --- a/stylesheets/components/MessageAudio.scss +++ b/stylesheets/components/MessageAudio.scss @@ -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; } diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 5dc0bb15b69..4716fda1ca5 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -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) => { activeAudioID === id && activeAudioContext === renderingContext; const waveformRef = useRef(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) => { [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) => { return; } + audio.playbackRate = playbackRate; + if (isPlaying) { if (!audio.paused) { return; @@ -411,7 +429,7 @@ export const MessageAudio: React.FC = (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) => { 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 = (
= (props: Props) => { {timeToText(countDown)}
{!withContentBelow && !collapseMetadata && ( - + <> +
+ {isPlaying && ( + + )} +
+ + )}
);