290 lines
7.7 KiB
TypeScript
290 lines
7.7 KiB
TypeScript
import React, {
|
||
FC, useCallback, useEffect, useMemo,
|
||
} from '../../lib/teact/teact';
|
||
import { getActions, withGlobal } from '../../global';
|
||
|
||
import { AudioOrigin } from '../../types';
|
||
import {
|
||
ApiAudio, ApiChat, ApiMessage, ApiUser,
|
||
} from '../../api/types';
|
||
|
||
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
|
||
|
||
import * as mediaLoader from '../../util/mediaLoader';
|
||
import {
|
||
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
|
||
} from '../../global/helpers';
|
||
import { selectChat, selectSender } from '../../global/selectors';
|
||
import buildClassName from '../../util/buildClassName';
|
||
import { makeTrackId } from '../../util/audioPlayer';
|
||
import { clearMediaSession } from '../../util/mediaSession';
|
||
import windowSize from '../../util/windowSize';
|
||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||
import useLang from '../../hooks/useLang';
|
||
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
|
||
import renderText from '../common/helpers/renderText';
|
||
|
||
import RippleEffect from '../ui/RippleEffect';
|
||
import Button from '../ui/Button';
|
||
import RangeSlider from '../ui/RangeSlider';
|
||
|
||
import './AudioPlayer.scss';
|
||
|
||
type OwnProps = {
|
||
message: ApiMessage;
|
||
origin?: AudioOrigin;
|
||
className?: string;
|
||
noUi?: boolean;
|
||
};
|
||
|
||
type StateProps = {
|
||
sender?: ApiChat | ApiUser;
|
||
chat?: ApiChat;
|
||
volume: number;
|
||
playbackRate: number;
|
||
isMuted: boolean;
|
||
};
|
||
|
||
const FAST_PLAYBACK_RATE = 1.8;
|
||
|
||
const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||
message,
|
||
className,
|
||
noUi,
|
||
sender,
|
||
chat,
|
||
volume,
|
||
playbackRate,
|
||
isMuted,
|
||
}) => {
|
||
const {
|
||
setAudioPlayerVolume,
|
||
setAudioPlayerPlaybackRate,
|
||
setAudioPlayerMuted,
|
||
focusMessage,
|
||
closeAudioPlayer,
|
||
} = getActions();
|
||
|
||
const lang = useLang();
|
||
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,
|
||
toggleMuted,
|
||
setPlaybackRate,
|
||
} = useAudioPlayer(
|
||
makeTrackId(message),
|
||
getMediaDuration(message)!,
|
||
isVoice ? 'voice' : 'audio',
|
||
mediaData,
|
||
undefined,
|
||
mediaMetadata,
|
||
undefined,
|
||
true,
|
||
undefined,
|
||
undefined,
|
||
isMessageLocal(message),
|
||
true,
|
||
);
|
||
|
||
// Prevent refresh by accidentally rotating device when listening to a voice message
|
||
const isVoicePlaying = isVoice && isPlaying;
|
||
useEffect(() => {
|
||
if (!isVoicePlaying) {
|
||
return undefined;
|
||
}
|
||
|
||
windowSize.disableRefresh();
|
||
|
||
return () => {
|
||
windowSize.enableRefresh();
|
||
};
|
||
}, [isVoicePlaying]);
|
||
|
||
const handleClick = useCallback(() => {
|
||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||
}, [focusMessage, message.chatId, message.id]);
|
||
|
||
const handleClose = useCallback(() => {
|
||
if (isPlaying) {
|
||
playPause();
|
||
}
|
||
closeAudioPlayer();
|
||
clearMediaSession();
|
||
stop();
|
||
}, [closeAudioPlayer, isPlaying, playPause, stop]);
|
||
|
||
const handleVolumeChange = useCallback((value: number) => {
|
||
setAudioPlayerVolume({ volume: value / 100 });
|
||
setAudioPlayerMuted({ isMuted: false });
|
||
|
||
setVolume(value / 100);
|
||
}, [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 || 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, isMuted]);
|
||
|
||
if (noUi) {
|
||
return undefined;
|
||
}
|
||
|
||
return (
|
||
<div className={buildClassName('AudioPlayer', className)} dir={lang.isRtl ? 'rtl' : undefined}>
|
||
<Button
|
||
round
|
||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||
color="translucent"
|
||
size="smaller"
|
||
className="player-button"
|
||
disabled={isFirst()}
|
||
onClick={requestPreviousTrack}
|
||
ariaLabel="Previous track"
|
||
>
|
||
<i className="icon-skip-previous" />
|
||
</Button>
|
||
<Button
|
||
round
|
||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||
color="translucent"
|
||
size="smaller"
|
||
className={buildClassName('toggle-play', 'player-button', isPlaying ? 'pause' : 'play')}
|
||
onClick={playPause}
|
||
ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
|
||
>
|
||
<i className="icon-play" />
|
||
<i className="icon-pause" />
|
||
</Button>
|
||
<Button
|
||
round
|
||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||
color="translucent"
|
||
size="smaller"
|
||
className="player-button"
|
||
disabled={isLast()}
|
||
onClick={requestNextTrack}
|
||
ariaLabel="Next track"
|
||
>
|
||
<i className="icon-skip-next" />
|
||
</Button>
|
||
|
||
<div className="AudioPlayer-content" onClick={handleClick}>
|
||
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
|
||
<RippleEffect />
|
||
</div>
|
||
|
||
<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 bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
|
||
</div>
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
{isVoice && (
|
||
<Button
|
||
round
|
||
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
|
||
color="translucent"
|
||
size="smaller"
|
||
ariaLabel="Playback Rate"
|
||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||
onClick={handlePlaybackClick}
|
||
>
|
||
<span className="playback-button-inner">2Х</span>
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
round
|
||
className="player-close"
|
||
color="translucent"
|
||
size="smaller"
|
||
onClick={handleClose}
|
||
ariaLabel="Close player"
|
||
>
|
||
<i className="icon-close" />
|
||
</Button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
function renderAudio(audio: ApiAudio) {
|
||
const { title, performer, fileName } = audio;
|
||
|
||
return (
|
||
<>
|
||
<div className="title" dir="auto">{renderText(title || fileName)}</div>
|
||
{performer && (
|
||
<div className="subtitle" dir="auto">{renderText(performer)}</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function renderVoice(subtitle: string, senderName?: string) {
|
||
return (
|
||
<>
|
||
<div className="title" dir="auto">{senderName && renderText(senderName)}</div>
|
||
<div className="subtitle" dir="auto">{subtitle}</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default withGlobal<OwnProps>(
|
||
(global, { message }): StateProps => {
|
||
const sender = selectSender(global, message);
|
||
const chat = selectChat(global, message.chatId);
|
||
const { volume, playbackRate, isMuted } = global.audioPlayer;
|
||
|
||
return {
|
||
sender,
|
||
chat,
|
||
volume,
|
||
playbackRate,
|
||
isMuted,
|
||
};
|
||
},
|
||
)(AudioPlayer);
|