Audio Player: Support 2X speed and mute; Audio: Fix width on mobile (#1533)
This commit is contained in:
parent
dedefe87ca
commit
d6e1665e4e
@ -258,14 +258,14 @@ const Audio: FC<OwnProps> = ({
|
||||
|
||||
const isOwn = isOwnMessage(message);
|
||||
const renderedWaveform = useMemo(
|
||||
() => voice && renderWaveform(
|
||||
() => origin === AudioOrigin.Inline && voice && renderWaveform(
|
||||
voice,
|
||||
(isMediaUnread && !isOwn) ? 1 : playProgress,
|
||||
isOwn,
|
||||
theme,
|
||||
seekerRef,
|
||||
),
|
||||
[voice, isMediaUnread, isOwn, playProgress, theme],
|
||||
[origin, voice, isMediaUnread, isOwn, playProgress, theme],
|
||||
);
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
|
||||
@ -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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
ApiAudio, ApiChat, ApiMessage, ApiUser,
|
||||
} 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 {
|
||||
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle,
|
||||
@ -39,9 +40,19 @@ type StateProps = {
|
||||
sender?: ApiChat | ApiUser;
|
||||
chat?: ApiChat;
|
||||
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> = ({
|
||||
message,
|
||||
@ -51,19 +62,32 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
sender,
|
||||
chat,
|
||||
volume,
|
||||
playbackRate,
|
||||
isMuted,
|
||||
setAudioPlayerVolume,
|
||||
setAudioPlayerPlaybackRate,
|
||||
setAudioPlayerMuted,
|
||||
focusMessage,
|
||||
closeAudioPlayer,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const { audio, voice } = getMessageContent(message);
|
||||
const isVoice = Boolean(voice);
|
||||
const { audio, voice, video } = getMessageContent(message);
|
||||
const isVoice = Boolean(voice || video);
|
||||
const senderName = sender ? getSenderTitle(lang, sender) : undefined;
|
||||
const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined);
|
||||
const mediaMetadata = useMessageMediaMetadata(message, sender, chat);
|
||||
|
||||
const {
|
||||
playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume,
|
||||
playPause,
|
||||
stop,
|
||||
isPlaying,
|
||||
requestNextTrack,
|
||||
requestPreviousTrack,
|
||||
isFirst,
|
||||
isLast,
|
||||
setVolume,
|
||||
toggleMuted,
|
||||
setPlaybackRate,
|
||||
} = useAudioPlayer(
|
||||
makeTrackId(message),
|
||||
getMediaDuration(message)!,
|
||||
@ -94,15 +118,33 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
const handleVolumeChange = useCallback((value: number) => {
|
||||
setAudioPlayerVolume({ volume: value / 100 });
|
||||
setAudioPlayerMuted({ isMuted: false });
|
||||
|
||||
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(() => {
|
||||
if (volume === 0) return 'icon-muted';
|
||||
if (volume === 0 || isMuted) return 'icon-muted';
|
||||
if (volume < 0.3) return 'icon-volume-1';
|
||||
if (volume < 0.6) return 'icon-volume-2';
|
||||
return 'icon-volume-3';
|
||||
}, [volume]);
|
||||
}, [volume, isMuted]);
|
||||
|
||||
if (noUi) {
|
||||
return undefined;
|
||||
@ -152,22 +194,39 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<RippleEffect />
|
||||
</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
|
||||
round
|
||||
className="player-button volume-button"
|
||||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel="Volume"
|
||||
withClickPropagation
|
||||
ariaLabel="Playback Rate"
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
onClick={handlePlaybackClick}
|
||||
>
|
||||
<i className={volumeIcon} />
|
||||
<div className="volume-slider-spacer" />
|
||||
<div className="volume-slider">
|
||||
<RangeSlider value={volume * 100} onChange={handleVolumeChange} />
|
||||
</div>
|
||||
<span className="playback-button-inner">2Х</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
round
|
||||
className="player-close"
|
||||
@ -208,13 +267,18 @@ export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { volume } = global.audioPlayer;
|
||||
const { volume, playbackRate, isMuted } = global.audioPlayer;
|
||||
|
||||
return {
|
||||
sender,
|
||||
chat,
|
||||
volume,
|
||||
playbackRate,
|
||||
isMuted,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume']),
|
||||
(setGlobal, actions): DispatchProps => pick(
|
||||
actions,
|
||||
['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume', 'setAudioPlayerPlaybackRate', 'setAudioPlayerMuted'],
|
||||
),
|
||||
)(AudioPlayer);
|
||||
|
||||
@ -337,6 +337,10 @@
|
||||
min-width: 20rem;
|
||||
padding: .5rem .5rem .8125rem !important;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
min-width: 17rem;
|
||||
}
|
||||
|
||||
.Audio + .text-content {
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
||||
|
||||
export const DEFAULT_VOLUME = 1;
|
||||
export const DEFAULT_PLAYBACK_RATE = 1;
|
||||
|
||||
export const ANIMATION_LEVEL_MIN = 0;
|
||||
export const ANIMATION_LEVEL_MED = 1;
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
|
||||
DEFAULT_VOLUME,
|
||||
DEFAULT_PLAYBACK_RATE,
|
||||
} from '../config';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
|
||||
import { ANIMATION_END_EVENT, ANIMATION_START_EVENT } from '../hooks/useHeavyAnimationCheck';
|
||||
@ -137,6 +138,9 @@ function readCache(initialState: GlobalState): GlobalState {
|
||||
if (cached.audioPlayer.volume === undefined) {
|
||||
cached.audioPlayer.volume = DEFAULT_VOLUME;
|
||||
}
|
||||
if (cached.audioPlayer.playbackRate === undefined) {
|
||||
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
|
||||
}
|
||||
}
|
||||
|
||||
const newState = {
|
||||
@ -185,6 +189,8 @@ function updateCache() {
|
||||
]),
|
||||
audioPlayer: {
|
||||
volume: global.audioPlayer.volume,
|
||||
playbackRate: global.audioPlayer.playbackRate,
|
||||
isMuted: global.audioPlayer.isMuted,
|
||||
},
|
||||
isChatInfoShown: reduceShowChatInfo(global),
|
||||
users: reduceUsers(global),
|
||||
|
||||
@ -3,6 +3,7 @@ import { NewChatMembersProgress } from '../types';
|
||||
|
||||
import {
|
||||
ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR,
|
||||
DEFAULT_PLAYBACK_RATE,
|
||||
DEFAULT_VOLUME,
|
||||
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
|
||||
} from '../config';
|
||||
@ -113,6 +114,8 @@ export const INITIAL_STATE: GlobalState = {
|
||||
|
||||
audioPlayer: {
|
||||
volume: DEFAULT_VOLUME,
|
||||
playbackRate: DEFAULT_PLAYBACK_RATE,
|
||||
isMuted: false,
|
||||
},
|
||||
|
||||
forwardMessages: {},
|
||||
|
||||
@ -329,6 +329,8 @@ export type GlobalState = {
|
||||
threadId?: number;
|
||||
origin?: AudioOrigin;
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
topPeers: {
|
||||
@ -503,7 +505,7 @@ export type ActionTypes = (
|
||||
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' |
|
||||
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' |
|
||||
'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
|
||||
// Channel / groups creation
|
||||
// chat creation
|
||||
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
|
||||
// settings
|
||||
'setSettingOption' | 'loadPasswordInfo' | 'clearTwoFaError' |
|
||||
@ -514,7 +516,8 @@ export type ActionTypes = (
|
||||
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
|
||||
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
|
||||
'loadContentSettings' | 'updateContentSettings' |
|
||||
// Stickers & GIFs
|
||||
'loadCountryList' | 'ensureTimeFormat' |
|
||||
// stickers & GIFs
|
||||
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
||||
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
||||
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
|
||||
@ -524,12 +527,14 @@ export type ActionTypes = (
|
||||
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||
'resetInlineBot' | 'restartBot' | 'startBot' |
|
||||
// media viewer & audio player
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'closeAudioPlayer' |
|
||||
'openMediaViewer' | 'closeMediaViewer' |
|
||||
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
|
||||
'setAudioPlayerMuted' | 'closeAudioPlayer' |
|
||||
// misc
|
||||
'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' |
|
||||
'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' |
|
||||
'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
|
||||
'setDeviceToken' | 'deleteDeviceToken' |
|
||||
'checkVersionNotification' | 'createServiceNotification' |
|
||||
'loadCountryList' | 'ensureTimeFormat' |
|
||||
// payment
|
||||
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
|
||||
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
||||
|
||||
@ -49,12 +49,18 @@ export default (
|
||||
controllerRef.current = register(trackId, trackType, origin, (eventName, e) => {
|
||||
switch (eventName) {
|
||||
case 'onPlay': {
|
||||
const { setVolume, proxy } = controllerRef.current!;
|
||||
const {
|
||||
setVolume, setPlaybackRate, toggleMuted, proxy,
|
||||
} = controllerRef.current!;
|
||||
setIsPlaying(true);
|
||||
|
||||
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
|
||||
setPlaybackState('playing');
|
||||
setVolume(getGlobal().audioPlayer.volume);
|
||||
toggleMuted(!!getGlobal().audioPlayer.isMuted);
|
||||
if (trackType === 'voice') {
|
||||
setPlaybackRate(getGlobal().audioPlayer.playbackRate);
|
||||
}
|
||||
|
||||
setPositionState({
|
||||
duration: proxy.duration || 0,
|
||||
@ -63,6 +69,15 @@ export default (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'onRateChange': {
|
||||
const { proxy } = controllerRef.current!;
|
||||
setPositionState({
|
||||
duration: proxy.duration || 0,
|
||||
playbackRate: proxy.playbackRate,
|
||||
position: proxy.currentTime,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'onPause':
|
||||
setIsPlaying(false);
|
||||
setPlaybackState('paused');
|
||||
@ -111,6 +126,8 @@ export default (
|
||||
isLast,
|
||||
requestNextTrack,
|
||||
requestPreviousTrack,
|
||||
setPlaybackRate,
|
||||
toggleMuted,
|
||||
} = controllerRef.current!;
|
||||
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
|
||||
|
||||
@ -179,6 +196,8 @@ export default (
|
||||
requestPreviousTrack,
|
||||
isFirst: isFirst(),
|
||||
isLast: isLast(),
|
||||
setPlaybackRate,
|
||||
toggleMuted,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -170,7 +170,7 @@ addReducer('closeMediaViewer', (global) => {
|
||||
|
||||
addReducer('openAudioPlayer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, origin, volume,
|
||||
chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
|
||||
} = payload!;
|
||||
|
||||
return {
|
||||
@ -181,6 +181,8 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
|
||||
messageId,
|
||||
origin,
|
||||
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) => {
|
||||
return {
|
||||
...global,
|
||||
audioPlayer: {
|
||||
volume: global.audioPlayer.volume, // Preserve only volume for the next play
|
||||
volume: global.audioPlayer.volume,
|
||||
playbackRate: global.audioPlayer.playbackRate,
|
||||
isMuted: global.audioPlayer.isMuted,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -211,6 +211,21 @@ export function register(
|
||||
setVolume(volume: number) {
|
||||
if (currentTrackId === trackId) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user