Audio Player: Support 2X speed and mute; Audio: Fix width on mobile (#1533)

This commit is contained in:
Alexander Zinchuk 2021-11-05 21:57:51 +03:00
parent dedefe87ca
commit d6e1665e4e
11 changed files with 195 additions and 30 deletions

View File

@ -258,14 +258,14 @@ const Audio: FC<OwnProps> = ({
const isOwn = isOwnMessage(message); const isOwn = isOwnMessage(message);
const renderedWaveform = useMemo( const renderedWaveform = useMemo(
() => voice && renderWaveform( () => origin === AudioOrigin.Inline && voice && renderWaveform(
voice, voice,
(isMediaUnread && !isOwn) ? 1 : playProgress, (isMediaUnread && !isOwn) ? 1 : playProgress,
isOwn, isOwn,
theme, theme,
seekerRef, seekerRef,
), ),
[voice, isMediaUnread, isOwn, playProgress, theme], [origin, voice, isMediaUnread, isOwn, playProgress, theme],
); );
const fullClassName = buildClassName( const fullClassName = buildClassName(

View File

@ -105,6 +105,22 @@
} }
} }
.playback-button {
&.applied {
--color-text-secondary: var(--color-primary);
}
.playback-button-inner {
transition: 0.2s color ease-in-out;
font-size: 0.75rem;
font-weight: bold;
border: 2px solid;
border-radius: 0.375rem;
padding: 0.15rem 0.25rem;
font-variant-numeric: tabular-nums;
}
}
&-content { &-content {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -7,7 +7,8 @@ import {
ApiAudio, ApiChat, ApiMessage, ApiUser, ApiAudio, ApiChat, ApiMessage, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import * as mediaLoader from '../../util/mediaLoader'; import * as mediaLoader from '../../util/mediaLoader';
import { import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle,
@ -39,9 +40,19 @@ type StateProps = {
sender?: ApiChat | ApiUser; sender?: ApiChat | ApiUser;
chat?: ApiChat; chat?: ApiChat;
volume: number; volume: number;
playbackRate: number;
isMuted: boolean;
}; };
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer' | 'setAudioPlayerVolume'>; type DispatchProps = Pick<GlobalActions, (
'focusMessage' |
'closeAudioPlayer' |
'setAudioPlayerVolume' |
'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted'
)>;
const FAST_PLAYBACK_RATE = 1.8;
const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
message, message,
@ -51,19 +62,32 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
sender, sender,
chat, chat,
volume, volume,
playbackRate,
isMuted,
setAudioPlayerVolume, setAudioPlayerVolume,
setAudioPlayerPlaybackRate,
setAudioPlayerMuted,
focusMessage, focusMessage,
closeAudioPlayer, closeAudioPlayer,
}) => { }) => {
const lang = useLang(); const lang = useLang();
const { audio, voice } = getMessageContent(message); const { audio, voice, video } = getMessageContent(message);
const isVoice = Boolean(voice); const isVoice = Boolean(voice || video);
const senderName = sender ? getSenderTitle(lang, sender) : undefined; const senderName = sender ? getSenderTitle(lang, sender) : undefined;
const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined); const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined);
const mediaMetadata = useMessageMediaMetadata(message, sender, chat); const mediaMetadata = useMessageMediaMetadata(message, sender, chat);
const { const {
playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume, playPause,
stop,
isPlaying,
requestNextTrack,
requestPreviousTrack,
isFirst,
isLast,
setVolume,
toggleMuted,
setPlaybackRate,
} = useAudioPlayer( } = useAudioPlayer(
makeTrackId(message), makeTrackId(message),
getMediaDuration(message)!, getMediaDuration(message)!,
@ -94,15 +118,33 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
const handleVolumeChange = useCallback((value: number) => { const handleVolumeChange = useCallback((value: number) => {
setAudioPlayerVolume({ volume: value / 100 }); setAudioPlayerVolume({ volume: value / 100 });
setAudioPlayerMuted({ isMuted: false });
setVolume(value / 100); setVolume(value / 100);
}, [setAudioPlayerVolume, setVolume]); }, [setAudioPlayerMuted, setAudioPlayerVolume, setVolume]);
const handleVolumeClick = useCallback(() => {
if (IS_TOUCH_ENV && !IS_IOS) return;
toggleMuted();
setAudioPlayerMuted({ isMuted: !isMuted });
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
const handlePlaybackClick = useCallback(() => {
if (playbackRate === 1) {
setPlaybackRate(FAST_PLAYBACK_RATE);
setAudioPlayerPlaybackRate({ playbackRate: FAST_PLAYBACK_RATE });
} else {
setPlaybackRate(1);
setAudioPlayerPlaybackRate({ playbackRate: 1 });
}
}, [playbackRate, setAudioPlayerPlaybackRate, setPlaybackRate]);
const volumeIcon = useMemo(() => { const volumeIcon = useMemo(() => {
if (volume === 0) return 'icon-muted'; if (volume === 0 || isMuted) return 'icon-muted';
if (volume < 0.3) return 'icon-volume-1'; if (volume < 0.3) return 'icon-volume-1';
if (volume < 0.6) return 'icon-volume-2'; if (volume < 0.6) return 'icon-volume-2';
return 'icon-volume-3'; return 'icon-volume-3';
}, [volume]); }, [volume, isMuted]);
if (noUi) { if (noUi) {
return undefined; return undefined;
@ -152,22 +194,39 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
<RippleEffect /> <RippleEffect />
</div> </div>
{!IS_IOS && ( <Button
round
className="player-button volume-button"
color="translucent"
size="smaller"
ariaLabel="Volume"
withClickPropagation
>
<i className={volumeIcon} onClick={handleVolumeClick} />
{!IS_IOS && (
<>
<div className="volume-slider-spacer" />
<div className="volume-slider">
<RangeSlider value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
</div>
</>
)}
</Button>
{isVoice && (
<Button <Button
round round
className="player-button volume-button" className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
color="translucent" color="translucent"
size="smaller" size="smaller"
ariaLabel="Volume" ariaLabel="Playback Rate"
withClickPropagation ripple={!IS_SINGLE_COLUMN_LAYOUT}
onClick={handlePlaybackClick}
> >
<i className={volumeIcon} /> <span className="playback-button-inner">2Х</span>
<div className="volume-slider-spacer" />
<div className="volume-slider">
<RangeSlider value={volume * 100} onChange={handleVolumeChange} />
</div>
</Button> </Button>
)} )}
<Button <Button
round round
className="player-close" className="player-close"
@ -208,13 +267,18 @@ export default withGlobal<OwnProps>(
(global, { message }): StateProps => { (global, { message }): StateProps => {
const sender = selectSender(global, message); const sender = selectSender(global, message);
const chat = selectChat(global, message.chatId); const chat = selectChat(global, message.chatId);
const { volume } = global.audioPlayer; const { volume, playbackRate, isMuted } = global.audioPlayer;
return { return {
sender, sender,
chat, chat,
volume, volume,
playbackRate,
isMuted,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume']), (setGlobal, actions): DispatchProps => pick(
actions,
['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume', 'setAudioPlayerPlaybackRate', 'setAudioPlayerMuted'],
),
)(AudioPlayer); )(AudioPlayer);

View File

@ -337,6 +337,10 @@
min-width: 20rem; min-width: 20rem;
padding: .5rem .5rem .8125rem !important; padding: .5rem .5rem .8125rem !important;
@media (max-width: 600px) {
min-width: 17rem;
}
.Audio + .text-content { .Audio + .text-content {
margin-top: .25rem; margin-top: .25rem;
} }

View File

@ -61,6 +61,7 @@ export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
export const ALL_CHATS_PRELOAD_DISABLED = false; export const ALL_CHATS_PRELOAD_DISABLED = false;
export const DEFAULT_VOLUME = 1; export const DEFAULT_VOLUME = 1;
export const DEFAULT_PLAYBACK_RATE = 1;
export const ANIMATION_LEVEL_MIN = 0; export const ANIMATION_LEVEL_MIN = 0;
export const ANIMATION_LEVEL_MED = 1; export const ANIMATION_LEVEL_MED = 1;

View File

@ -14,6 +14,7 @@ import {
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
GLOBAL_STATE_CACHE_USER_LIST_LIMIT, GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
DEFAULT_VOLUME, DEFAULT_VOLUME,
DEFAULT_PLAYBACK_RATE,
} from '../config'; } from '../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
import { ANIMATION_END_EVENT, ANIMATION_START_EVENT } from '../hooks/useHeavyAnimationCheck'; import { ANIMATION_END_EVENT, ANIMATION_START_EVENT } from '../hooks/useHeavyAnimationCheck';
@ -137,6 +138,9 @@ function readCache(initialState: GlobalState): GlobalState {
if (cached.audioPlayer.volume === undefined) { if (cached.audioPlayer.volume === undefined) {
cached.audioPlayer.volume = DEFAULT_VOLUME; cached.audioPlayer.volume = DEFAULT_VOLUME;
} }
if (cached.audioPlayer.playbackRate === undefined) {
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
}
} }
const newState = { const newState = {
@ -185,6 +189,8 @@ function updateCache() {
]), ]),
audioPlayer: { audioPlayer: {
volume: global.audioPlayer.volume, volume: global.audioPlayer.volume,
playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted,
}, },
isChatInfoShown: reduceShowChatInfo(global), isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global), users: reduceUsers(global),

View File

@ -3,6 +3,7 @@ import { NewChatMembersProgress } from '../types';
import { import {
ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR, ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR,
DEFAULT_PLAYBACK_RATE,
DEFAULT_VOLUME, DEFAULT_VOLUME,
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
} from '../config'; } from '../config';
@ -113,6 +114,8 @@ export const INITIAL_STATE: GlobalState = {
audioPlayer: { audioPlayer: {
volume: DEFAULT_VOLUME, volume: DEFAULT_VOLUME,
playbackRate: DEFAULT_PLAYBACK_RATE,
isMuted: false,
}, },
forwardMessages: {}, forwardMessages: {},

View File

@ -329,6 +329,8 @@ export type GlobalState = {
threadId?: number; threadId?: number;
origin?: AudioOrigin; origin?: AudioOrigin;
volume: number; volume: number;
playbackRate: number;
isMuted: boolean;
}; };
topPeers: { topPeers: {
@ -503,7 +505,7 @@ export type ActionTypes = (
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' | 'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' | 'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
// Channel / groups creation // chat creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' | 'createChannel' | 'createGroupChat' | 'resetChatCreation' |
// settings // settings
'setSettingOption' | 'loadPasswordInfo' | 'clearTwoFaError' | 'setSettingOption' | 'loadPasswordInfo' | 'clearTwoFaError' |
@ -514,7 +516,8 @@ export type ActionTypes = (
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | 'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' | 'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
'loadContentSettings' | 'updateContentSettings' | 'loadContentSettings' | 'updateContentSettings' |
// Stickers & GIFs 'loadCountryList' | 'ensureTimeFormat' |
// stickers & GIFs
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' | 'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' | 'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' | 'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
@ -524,12 +527,14 @@ export type ActionTypes = (
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' | 'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' | 'startBot' | 'resetInlineBot' | 'restartBot' | 'startBot' |
// media viewer & audio player // media viewer & audio player
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'closeAudioPlayer' | 'openMediaViewer' | 'closeMediaViewer' |
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted' | 'closeAudioPlayer' |
// misc // misc
'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'openPollModal' | 'closePollModal' |
'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
'setDeviceToken' | 'deleteDeviceToken' |
'checkVersionNotification' | 'createServiceNotification' | 'checkVersionNotification' | 'createServiceNotification' |
'loadCountryList' | 'ensureTimeFormat' |
// payment // payment
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' | 'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' | 'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |

View File

@ -49,12 +49,18 @@ export default (
controllerRef.current = register(trackId, trackType, origin, (eventName, e) => { controllerRef.current = register(trackId, trackType, origin, (eventName, e) => {
switch (eventName) { switch (eventName) {
case 'onPlay': { case 'onPlay': {
const { setVolume, proxy } = controllerRef.current!; const {
setVolume, setPlaybackRate, toggleMuted, proxy,
} = controllerRef.current!;
setIsPlaying(true); setIsPlaying(true);
registerMediaSession(metadata, makeMediaHandlers(controllerRef)); registerMediaSession(metadata, makeMediaHandlers(controllerRef));
setPlaybackState('playing'); setPlaybackState('playing');
setVolume(getGlobal().audioPlayer.volume); setVolume(getGlobal().audioPlayer.volume);
toggleMuted(!!getGlobal().audioPlayer.isMuted);
if (trackType === 'voice') {
setPlaybackRate(getGlobal().audioPlayer.playbackRate);
}
setPositionState({ setPositionState({
duration: proxy.duration || 0, duration: proxy.duration || 0,
@ -63,6 +69,15 @@ export default (
}); });
break; break;
} }
case 'onRateChange': {
const { proxy } = controllerRef.current!;
setPositionState({
duration: proxy.duration || 0,
playbackRate: proxy.playbackRate,
position: proxy.currentTime,
});
break;
}
case 'onPause': case 'onPause':
setIsPlaying(false); setIsPlaying(false);
setPlaybackState('paused'); setPlaybackState('paused');
@ -111,6 +126,8 @@ export default (
isLast, isLast,
requestNextTrack, requestNextTrack,
requestPreviousTrack, requestPreviousTrack,
setPlaybackRate,
toggleMuted,
} = controllerRef.current!; } = controllerRef.current!;
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration; const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
@ -179,6 +196,8 @@ export default (
requestPreviousTrack, requestPreviousTrack,
isFirst: isFirst(), isFirst: isFirst(),
isLast: isLast(), isLast: isLast(),
setPlaybackRate,
toggleMuted,
}; };
}; };

View File

@ -170,7 +170,7 @@ addReducer('closeMediaViewer', (global) => {
addReducer('openAudioPlayer', (global, actions, payload) => { addReducer('openAudioPlayer', (global, actions, payload) => {
const { const {
chatId, threadId, messageId, origin, volume, chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
} = payload!; } = payload!;
return { return {
@ -181,6 +181,8 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
messageId, messageId,
origin, origin,
volume: volume || global.audioPlayer.volume, volume: volume || global.audioPlayer.volume,
playbackRate: playbackRate || global.audioPlayer.playbackRate,
isMuted: isMuted || global.audioPlayer.isMuted,
}, },
}; };
}); });
@ -199,11 +201,41 @@ addReducer('setAudioPlayerVolume', (global, actions, payload) => {
}; };
}); });
addReducer('setAudioPlayerPlaybackRate', (global, actions, payload) => {
const {
playbackRate,
} = payload!;
return {
...global,
audioPlayer: {
...global.audioPlayer,
playbackRate,
},
};
});
addReducer('setAudioPlayerMuted', (global, actions, payload) => {
const {
isMuted,
} = payload!;
return {
...global,
audioPlayer: {
...global.audioPlayer,
isMuted,
},
};
});
addReducer('closeAudioPlayer', (global) => { addReducer('closeAudioPlayer', (global) => {
return { return {
...global, ...global,
audioPlayer: { audioPlayer: {
volume: global.audioPlayer.volume, // Preserve only volume for the next play volume: global.audioPlayer.volume,
playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted,
}, },
}; };
}); });

View File

@ -211,6 +211,21 @@ export function register(
setVolume(volume: number) { setVolume(volume: number) {
if (currentTrackId === trackId) { if (currentTrackId === trackId) {
audio.volume = volume; audio.volume = volume;
audio.muted = false;
}
},
setPlaybackRate(rate: number) {
if (currentTrackId === trackId) {
audio.playbackRate = rate;
}
},
toggleMuted(muted?: boolean) {
if (muted === undefined) {
audio.muted = !audio.muted;
} else {
audio.muted = muted;
} }
}, },