import type { FC } from '../../../lib/teact/teact'; import React, { useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiAudio, ApiChat, ApiMessage, ApiPeer, MediaContent, } from '../../../api/types'; import type { IconName } from '../../../types/icons'; import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config'; import { getMediaDuration, getMessageContent, getMessageMediaHash, getPeerTitle, isMessageLocal, } from '../../../global/helpers'; import { selectChat, selectChatMessage, selectSender, selectTabState, } from '../../../global/selectors'; import { makeTrackId } from '../../../util/audioPlayer'; import buildClassName from '../../../util/buildClassName'; import * as mediaLoader from '../../../util/mediaLoader'; import { clearMediaSession } from '../../../util/mediaSession'; import { IS_IOS, IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import renderText from '../../common/helpers/renderText'; import useAppLayout from '../../../hooks/useAppLayout'; import useAudioPlayer from '../../../hooks/useAudioPlayer'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useLastCallback from '../../../hooks/useLastCallback'; import useMessageMediaMetadata from '../../../hooks/useMessageMediaMetadata'; import useOldLang from '../../../hooks/useOldLang'; import useShowTransition from '../../../hooks/useShowTransition'; import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane'; import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import DropdownMenu from '../../ui/DropdownMenu'; import MenuItem from '../../ui/MenuItem'; import RangeSlider from '../../ui/RangeSlider'; import RippleEffect from '../../ui/RippleEffect'; import './AudioPlayer.scss'; type OwnProps = { className?: string; noUi?: boolean; isFullWidth?: boolean; isHidden?: boolean; onPaneStateChange?: (state: PaneState) => void; }; type StateProps = { message?: ApiMessage; sender?: ApiPeer; chat?: ApiChat; volume: number; playbackRate: number; isPlaybackRateActive?: boolean; isMuted: boolean; }; const PLAYBACK_RATES: Record = { 0.5: 0.66, 0.75: 0.8, 1: 1, 1.5: 1.4, 2: 1.8, }; const PLAYBACK_RATE_VALUES = Object.keys(PLAYBACK_RATES).sort().map(Number); const REGULAR_PLAYBACK_RATE = 1; const DEFAULT_FAST_PLAYBACK_RATE = 2; const AudioPlayer: FC = ({ message, className, noUi, sender, chat, volume, playbackRate, isPlaybackRateActive, isMuted, isFullWidth, onPaneStateChange, }) => { const { setAudioPlayerVolume, setAudioPlayerPlaybackRate, setAudioPlayerMuted, focusMessage, closeAudioPlayer, } = getActions(); const lang = useOldLang(); const { isMobile } = useAppLayout(); const renderingMessage = useCurrentOrPrev(message); const { audio, voice, video } = renderingMessage ? getMessageContent(renderingMessage) : {} satisfies MediaContent; const isVoice = Boolean(voice || video); const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION; const senderName = sender ? getPeerTitle(lang, sender) : undefined; const mediaHash = renderingMessage && getMessageMediaHash(renderingMessage, 'inline'); const mediaData = mediaHash && mediaLoader.getFromMemory(mediaHash); const mediaMetadata = useMessageMediaMetadata(renderingMessage, sender, chat); const { playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume, toggleMuted, setPlaybackRate, } = useAudioPlayer( message && makeTrackId(message), message ? getMediaDuration(message)! : 0, isVoice ? 'voice' : 'audio', mediaData, undefined, mediaMetadata, undefined, true, undefined, undefined, message && isMessageLocal(message), true, ); const isOpen = Boolean(message); const { ref: transitionRef, } = useShowTransition({ isOpen, shouldForceOpen: isFullWidth, // Use pane animation instead }); const { ref, shouldRender } = useHeaderPane({ isOpen, isDisabled: !isFullWidth, ref: transitionRef, onStateChange: onPaneStateChange, }); const { isContextMenuOpen, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(transitionRef, !shouldRender); const handleClick = useLastCallback(() => { const { chatId, id } = renderingMessage!; focusMessage({ chatId, messageId: id }); }); const handleClose = useLastCallback(() => { if (!stop) { return; } if (isPlaying) { playPause(); } closeAudioPlayer(); clearMediaSession(); stop(); }); const handleVolumeChange = useLastCallback((value: number) => { if (!setVolume) { return; } setAudioPlayerVolume({ volume: value / 100 }); setVolume(value / 100); }); const handleVolumeClick = useLastCallback(() => { if (IS_TOUCH_ENV && !IS_IOS) return; if (!toggleMuted) { return; } toggleMuted(); setAudioPlayerMuted({ isMuted: !isMuted }); }); const updatePlaybackRate = useLastCallback((newRate: number, isActive = true) => { if (!setPlaybackRate) { return; } const rate = PLAYBACK_RATES[newRate]; const shouldBeActive = newRate !== REGULAR_PLAYBACK_RATE && isActive; setAudioPlayerPlaybackRate({ playbackRate: rate, isPlaybackRateActive: shouldBeActive }); setPlaybackRate(shouldBeActive ? rate : REGULAR_PLAYBACK_RATE); }); const handlePlaybackClick = useLastCallback(() => { handleContextMenuClose(); const oldRate = Number(Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0]) || REGULAR_PLAYBACK_RATE; const newIsActive = !isPlaybackRateActive; updatePlaybackRate( newIsActive && oldRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : oldRate, newIsActive, ); }); const PlaybackRateButton = useLastCallback(() => { const displayRate = Object.entries(PLAYBACK_RATES).find(([, rate]) => rate === playbackRate)?.[0] || REGULAR_PLAYBACK_RATE; const text = `${playbackRate === REGULAR_PLAYBACK_RATE ? DEFAULT_FAST_PLAYBACK_RATE : displayRate}Х`; return (
{isContextMenuOpen &&
}
); }); const volumeIcon: IconName = useMemo(() => { if (volume === 0 || isMuted) return 'muted'; if (volume < 0.3) return 'volume-1'; if (volume < 0.6) return 'volume-2'; return 'volume-3'; }, [volume, isMuted]); if (noUi || !shouldRender) { return undefined; } return (
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
{!IS_IOS && (
)}
{shouldRenderPlaybackButton && ( {PLAYBACK_RATE_VALUES.map((rate) => { return renderPlaybackRateMenuItem(rate, playbackRate, updatePlaybackRate, isPlaybackRateActive); })} )}
); }; function renderAudio(audio: ApiAudio) { const { title, performer, fileName } = audio; return ( <>
{renderText(title || fileName)}
{performer && (
{renderText(performer)}
)} ); } function renderVoice(subtitle: string, senderName?: string) { return ( <>
{senderName && renderText(senderName)}
{subtitle}
); } function renderPlaybackRateMenuItem( rate: number, currentRate: number, onClick: (rate: number) => void, isPlaybackRateActive?: boolean, ) { const isSelected = (currentRate === PLAYBACK_RATES[rate] && isPlaybackRateActive) || (rate === REGULAR_PLAYBACK_RATE && !isPlaybackRateActive); return ( onClick(rate)} icon={isSelected ? 'check' : undefined} customIcon={!isSelected ? : undefined} > {rate}X ); } export default withGlobal( (global, { isHidden }): StateProps => { const { audioPlayer } = selectTabState(global); const { chatId, messageId } = audioPlayer; const message = !isHidden && chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined; const sender = message && selectSender(global, message); const chat = message && selectChat(global, message.chatId); const { volume, playbackRate, isMuted, isPlaybackRateActive, } = selectTabState(global).audioPlayer; return { message, sender, chat, volume, playbackRate, isPlaybackRateActive, isMuted, }; }, )(AudioPlayer);