Audio Player: Add track navigation and volume controls (#1526)
This commit is contained in:
parent
c604e6156d
commit
3d8bb54398
BIN
src/assets/fonts/icomoon.woff
Executable file → Normal file
BIN
src/assets/fonts/icomoon.woff
Executable file → Normal file
Binary file not shown.
Binary file not shown.
@ -58,7 +58,7 @@
|
|||||||
--color-pinned: var(--color-white);
|
--color-pinned: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-muted-chat {
|
.icon-muted {
|
||||||
color: var(--color-white) !important;
|
color: var(--color-white) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +89,7 @@
|
|||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-muted-chat {
|
.icon-muted {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
margin-top: -.0625rem;
|
margin-top: -.0625rem;
|
||||||
@ -194,7 +194,7 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-muted-chat {
|
.icon-muted {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -290,7 +290,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
<div className="title">
|
<div className="title">
|
||||||
<h3>{renderText(getChatTitle(lang, chat, privateChatUser))}</h3>
|
<h3>{renderText(getChatTitle(lang, chat, privateChatUser))}</h3>
|
||||||
{chat.isVerified && <VerifiedIcon />}
|
{chat.isVerified && <VerifiedIcon />}
|
||||||
{isMuted && <i className="icon-muted-chat" />}
|
{isMuted && <i className="icon-muted" />}
|
||||||
{chat.lastMessage && (
|
{chat.lastMessage && (
|
||||||
<LastMessageMeta
|
<LastMessageMeta
|
||||||
message={chat.lastMessage}
|
message={chat.lastMessage}
|
||||||
|
|||||||
@ -156,8 +156,8 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
<RangeSlider
|
<RangeSlider
|
||||||
label={lang('TextSize')}
|
label={lang('TextSize')}
|
||||||
// TODO Remove memo-killer
|
min={12}
|
||||||
range={{ min: 12, max: 20 }}
|
max={20}
|
||||||
value={messageTextSize}
|
value={messageTextSize}
|
||||||
onChange={handleMessageTextSizeChange}
|
onChange={handleMessageTextSizeChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -124,10 +124,8 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
<RangeSlider
|
<RangeSlider
|
||||||
label="Sound"
|
label="Sound"
|
||||||
disabled={!hasWebNotifications}
|
disabled={!hasWebNotifications}
|
||||||
range={{
|
min={0}
|
||||||
min: 0,
|
max={10}
|
||||||
max: 10,
|
|
||||||
}}
|
|
||||||
value={notificationSoundVolume}
|
value={notificationSoundVolume}
|
||||||
onChange={(volume) => {
|
onChange={(volume) => {
|
||||||
updateWebNotificationSettings({ notificationSoundVolume: volume });
|
updateWebNotificationSettings({ notificationSoundVolume: volume });
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
margin: .125rem;
|
margin: .125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .toggle-play {
|
> .player-button {
|
||||||
--color-text-secondary: var(--color-primary);
|
--color-text-secondary: var(--color-primary);
|
||||||
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
||||||
--color-primary-shade: var(--color-green);
|
--color-primary-shade: var(--color-green);
|
||||||
@ -24,7 +24,7 @@
|
|||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-play {
|
.player-button {
|
||||||
&.smaller {
|
&.smaller {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
@ -60,6 +60,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.volume-button {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.volume-slider-spacer {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(100%);
|
||||||
|
bottom: 0;
|
||||||
|
height: 1rem;
|
||||||
|
width: 8rem;
|
||||||
|
cursor: default;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .volume-slider-spacer, .volume-slider-spacer:hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-background);
|
||||||
|
bottom: -1rem;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||||
|
width: 8rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: 0 1px 2px var(--color-default-shadow);
|
||||||
|
|
||||||
|
.RangeSlider {
|
||||||
|
margin-bottom: 0;
|
||||||
|
input[type=range] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .volume-slider,
|
||||||
|
.volume-slider:hover,
|
||||||
|
.volume-slider-spacer:hover + .volume-slider {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -69,7 +118,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 15rem;
|
|
||||||
border-radius: var(--border-radius-messages-small);
|
border-radius: var(--border-radius-messages-small);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useCallback } from '../../lib/teact/teact';
|
import React, { FC, useCallback, useMemo } from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../lib/teact/teactn';
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { AudioOrigin } from '../../types';
|
import { AudioOrigin } from '../../types';
|
||||||
@ -7,7 +7,7 @@ import {
|
|||||||
ApiAudio, ApiChat, ApiMessage, ApiUser,
|
ApiAudio, ApiChat, ApiMessage, ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } 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,
|
||||||
@ -24,6 +24,7 @@ import { clearMediaSession } from '../../util/mediaSession';
|
|||||||
|
|
||||||
import RippleEffect from '../ui/RippleEffect';
|
import RippleEffect from '../ui/RippleEffect';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import RangeSlider from '../ui/RangeSlider';
|
||||||
|
|
||||||
import './AudioPlayer.scss';
|
import './AudioPlayer.scss';
|
||||||
|
|
||||||
@ -37,12 +38,22 @@ type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
sender?: ApiChat | ApiUser;
|
sender?: ApiChat | ApiUser;
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
|
volume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer'>;
|
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer' | 'setAudioPlayerVolume'>;
|
||||||
|
|
||||||
const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
message, origin = AudioOrigin.Inline, className, noUi, sender, focusMessage, closeAudioPlayer, chat,
|
message,
|
||||||
|
origin = AudioOrigin.Inline,
|
||||||
|
className,
|
||||||
|
noUi,
|
||||||
|
sender,
|
||||||
|
chat,
|
||||||
|
volume,
|
||||||
|
setAudioPlayerVolume,
|
||||||
|
focusMessage,
|
||||||
|
closeAudioPlayer,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { audio, voice } = getMessageContent(message);
|
const { audio, voice } = getMessageContent(message);
|
||||||
@ -50,7 +61,10 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
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 { playPause, stop, isPlaying } = useAudioPlayer(
|
|
||||||
|
const {
|
||||||
|
playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume,
|
||||||
|
} = useAudioPlayer(
|
||||||
makeTrackId(message),
|
makeTrackId(message),
|
||||||
getMediaDuration(message)!,
|
getMediaDuration(message)!,
|
||||||
isVoice ? 'voice' : 'audio',
|
isVoice ? 'voice' : 'audio',
|
||||||
@ -78,6 +92,18 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
stop();
|
stop();
|
||||||
}, [closeAudioPlayer, isPlaying, playPause, stop]);
|
}, [closeAudioPlayer, isPlaying, playPause, stop]);
|
||||||
|
|
||||||
|
const handleVolumeChange = useCallback((value: number) => {
|
||||||
|
setAudioPlayerVolume({ volume: value / 100 });
|
||||||
|
setVolume(value / 100);
|
||||||
|
}, [setAudioPlayerVolume, setVolume]);
|
||||||
|
|
||||||
|
const volumeIcon = useMemo(() => {
|
||||||
|
if (volume === 0) return 'icon-muted';
|
||||||
|
if (volume < 0.3) return 'icon-volume-1';
|
||||||
|
if (volume < 0.6) return 'icon-volume-2';
|
||||||
|
return 'icon-volume-3';
|
||||||
|
}, [volume]);
|
||||||
|
|
||||||
if (noUi) {
|
if (noUi) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -89,19 +115,59 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="smaller"
|
size="smaller"
|
||||||
className={buildClassName('toggle-play', isPlaying ? 'pause' : 'play')}
|
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}
|
onClick={playPause}
|
||||||
ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
|
ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
|
||||||
>
|
>
|
||||||
<i className="icon-play" />
|
<i className="icon-play" />
|
||||||
<i className="icon-pause" />
|
<i className="icon-pause" />
|
||||||
</Button>
|
</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}>
|
<div className="AudioPlayer-content" onClick={handleClick}>
|
||||||
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
|
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
|
||||||
<RippleEffect />
|
<RippleEffect />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!IS_IOS && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
className="player-button volume-button"
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
ariaLabel="Volume"
|
||||||
|
withClickPropagation
|
||||||
|
>
|
||||||
|
<i className={volumeIcon} />
|
||||||
|
<div className="volume-slider-spacer" />
|
||||||
|
<div className="volume-slider">
|
||||||
|
<RangeSlider value={volume * 100} onChange={handleVolumeChange} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
className="player-close"
|
className="player-close"
|
||||||
@ -142,11 +208,13 @@ 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;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sender,
|
sender,
|
||||||
chat,
|
chat,
|
||||||
|
volume,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer']),
|
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume']),
|
||||||
)(AudioPlayer);
|
)(AudioPlayer);
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2.875rem;
|
height: 2.875rem;
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 2px var(--color-light-shadow);
|
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -248,7 +248,7 @@ const RoundVideo: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
<div className="message-media-duration">
|
<div className="message-media-duration">
|
||||||
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)}
|
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)}
|
||||||
{(!isActivated || playerRef.current!.paused) && <i className="icon-muted-chat" />}
|
{(!isActivated || playerRef.current!.paused) && <i className="icon-muted" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -516,7 +516,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-media-duration .icon-muted-chat {
|
.message-media-duration .icon-muted {
|
||||||
vertical-align: -.1875rem;
|
vertical-align: -.1875rem;
|
||||||
margin-left: .375rem;
|
margin-left: .375rem;
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export type OwnProps = {
|
|||||||
faded?: boolean;
|
faded?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
isRtl?: boolean;
|
isRtl?: boolean;
|
||||||
|
withClickPropagation?: boolean;
|
||||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||||
@ -70,6 +71,7 @@ const Button: FC<OwnProps> = ({
|
|||||||
faded,
|
faded,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
isRtl,
|
isRtl,
|
||||||
|
withClickPropagation,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
|
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
|
||||||
@ -108,11 +110,11 @@ const Button: FC<OwnProps> = ({
|
|||||||
}, [disabled, onClick]);
|
}, [disabled, onClick]);
|
||||||
|
|
||||||
const handleMouseDown = useCallback((e: ReactMouseEvent<HTMLButtonElement>) => {
|
const handleMouseDown = useCallback((e: ReactMouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
if (!withClickPropagation) e.preventDefault();
|
||||||
if (!disabled && onMouseDown) {
|
if (!disabled && onMouseDown) {
|
||||||
onMouseDown(e);
|
onMouseDown(e);
|
||||||
}
|
}
|
||||||
}, [onMouseDown, disabled]);
|
}, [onMouseDown, disabled, withClickPropagation]);
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -100,6 +100,14 @@
|
|||||||
&::-moz-slider-thumb {
|
&::-moz-slider-thumb {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track, &::-moz-range-progress {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply custom styles
|
// Apply custom styles
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import './RangeSlider.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
options?: string[];
|
options?: string[];
|
||||||
range?: { min: number; max: number; step?: number };
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
label?: string;
|
label?: string;
|
||||||
value: number;
|
value: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -19,7 +21,9 @@ type OwnProps = {
|
|||||||
|
|
||||||
const RangeSlider: FC<OwnProps> = ({
|
const RangeSlider: FC<OwnProps> = ({
|
||||||
options,
|
options,
|
||||||
range,
|
min = 0,
|
||||||
|
max = options ? options.length - 1 : 100,
|
||||||
|
step = 1,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
disabled,
|
disabled,
|
||||||
@ -38,29 +42,19 @@ const RangeSlider: FC<OwnProps> = ({
|
|||||||
const trackWidth = useMemo(() => {
|
const trackWidth = useMemo(() => {
|
||||||
if (options) {
|
if (options) {
|
||||||
return (value / (options.length - 1)) * 100;
|
return (value / (options.length - 1)) * 100;
|
||||||
} else if (range) {
|
} else {
|
||||||
const possibleValuesLength = (range.max - range.min) / (range.step || 1);
|
const possibleValuesLength = (max - min) / step;
|
||||||
return ((value - range.min) / possibleValuesLength) * 100;
|
return ((value - min) / possibleValuesLength) * 100;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}, [value, options, range]);
|
}, [options, value, max, min, step]);
|
||||||
|
|
||||||
const [min, max, step] = useMemo(() => {
|
|
||||||
if (options) {
|
|
||||||
return [0, options.length - 1, 1];
|
|
||||||
} else if (range) {
|
|
||||||
return [range.min, range.max, range.step || 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [0, 0, 0];
|
|
||||||
}, [range, options]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{label && (
|
{label && (
|
||||||
<div className="slider-top-row" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="slider-top-row" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<span className="label" dir="auto">{label}</span>
|
<span className="label" dir="auto">{label}</span>
|
||||||
{range && (
|
{!options && (
|
||||||
<span className="value" dir="auto">{value}</span>
|
<span className="value" dir="auto">{value}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
|||||||
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
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 ANIMATION_LEVEL_MIN = 0;
|
export const ANIMATION_LEVEL_MIN = 0;
|
||||||
export const ANIMATION_LEVEL_MED = 1;
|
export const ANIMATION_LEVEL_MED = 1;
|
||||||
export const ANIMATION_LEVEL_MAX = 2;
|
export const ANIMATION_LEVEL_MAX = 2;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
|
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
|
||||||
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,
|
||||||
} 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';
|
||||||
@ -132,6 +133,10 @@ function readCache(initialState: GlobalState): GlobalState {
|
|||||||
if (!cached.serviceNotifications) {
|
if (!cached.serviceNotifications) {
|
||||||
cached.serviceNotifications = [];
|
cached.serviceNotifications = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cached.audioPlayer.volume === undefined) {
|
||||||
|
cached.audioPlayer.volume = DEFAULT_VOLUME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
@ -178,6 +183,9 @@ function updateCache() {
|
|||||||
'leftColumnWidth',
|
'leftColumnWidth',
|
||||||
'serviceNotifications',
|
'serviceNotifications',
|
||||||
]),
|
]),
|
||||||
|
audioPlayer: {
|
||||||
|
volume: global.audioPlayer.volume,
|
||||||
|
},
|
||||||
isChatInfoShown: reduceShowChatInfo(global),
|
isChatInfoShown: reduceShowChatInfo(global),
|
||||||
users: reduceUsers(global),
|
users: reduceUsers(global),
|
||||||
chats: reduceChats(global),
|
chats: reduceChats(global),
|
||||||
|
|||||||
@ -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_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';
|
||||||
import { IS_IOS, IS_MAC_OS } from '../util/environment';
|
import { IS_IOS, IS_MAC_OS } from '../util/environment';
|
||||||
@ -110,7 +111,9 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
mediaViewer: {},
|
mediaViewer: {},
|
||||||
|
|
||||||
audioPlayer: {},
|
audioPlayer: {
|
||||||
|
volume: DEFAULT_VOLUME,
|
||||||
|
},
|
||||||
|
|
||||||
forwardMessages: {},
|
forwardMessages: {},
|
||||||
|
|
||||||
|
|||||||
@ -328,6 +328,7 @@ export type GlobalState = {
|
|||||||
messageId?: number;
|
messageId?: number;
|
||||||
threadId?: number;
|
threadId?: number;
|
||||||
origin?: AudioOrigin;
|
origin?: AudioOrigin;
|
||||||
|
volume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
topPeers: {
|
topPeers: {
|
||||||
@ -523,9 +524,10 @@ export type ActionTypes = (
|
|||||||
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||||
'resetInlineBot' | 'restartBot' | 'startBot' |
|
'resetInlineBot' | 'restartBot' | 'startBot' |
|
||||||
// misc
|
// misc
|
||||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'closeAudioPlayer' |
|
||||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' |
|
||||||
'deleteDeviceToken' | 'checkVersionNotification' | 'createServiceNotification' |
|
'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' |
|
||||||
|
'checkVersionNotification' | 'createServiceNotification' |
|
||||||
// payment
|
// payment
|
||||||
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
|
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
|
||||||
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
useCallback, useEffect, useRef, useState,
|
useCallback, useEffect, useRef, useState,
|
||||||
} from '../lib/teact/teact';
|
} from '../lib/teact/teact';
|
||||||
import { getDispatch } from '../lib/teact/teactn';
|
import { getDispatch, getGlobal } from '../lib/teact/teactn';
|
||||||
|
|
||||||
import { AudioOrigin } from '../types';
|
import { AudioOrigin } from '../types';
|
||||||
|
|
||||||
@ -48,12 +48,21 @@ export default (
|
|||||||
useOnChange(() => {
|
useOnChange(() => {
|
||||||
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!;
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|
||||||
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
|
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
|
||||||
setPlaybackState('playing');
|
setPlaybackState('playing');
|
||||||
|
setVolume(getGlobal().audioPlayer.volume);
|
||||||
|
|
||||||
|
setPositionState({
|
||||||
|
duration: proxy.duration || 0,
|
||||||
|
playbackRate: proxy.playbackRate,
|
||||||
|
position: proxy.currentTime,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'onPause':
|
case 'onPause':
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setPlaybackState('paused');
|
setPlaybackState('paused');
|
||||||
@ -62,11 +71,6 @@ export default (
|
|||||||
const { proxy } = controllerRef.current!;
|
const { proxy } = controllerRef.current!;
|
||||||
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
|
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
|
||||||
if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration);
|
if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration);
|
||||||
setPositionState({
|
|
||||||
duration: proxy.duration,
|
|
||||||
playbackRate: proxy.playbackRate,
|
|
||||||
position: proxy.currentTime,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'onEnded': {
|
case 'onEnded': {
|
||||||
@ -95,7 +99,18 @@ export default (
|
|||||||
}, [metadata, isPlaying]);
|
}, [metadata, isPlaying]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
play, pause, setCurrentTime, proxy, destroy, setVolume, setCurrentOrigin, stop,
|
play,
|
||||||
|
pause,
|
||||||
|
setCurrentTime,
|
||||||
|
proxy,
|
||||||
|
destroy,
|
||||||
|
setVolume,
|
||||||
|
setCurrentOrigin,
|
||||||
|
stop,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
requestNextTrack,
|
||||||
|
requestPreviousTrack,
|
||||||
} = 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;
|
||||||
|
|
||||||
@ -160,6 +175,10 @@ export default (
|
|||||||
setVolume,
|
setVolume,
|
||||||
audioProxy: proxy,
|
audioProxy: proxy,
|
||||||
duration,
|
duration,
|
||||||
|
requestNextTrack,
|
||||||
|
requestPreviousTrack,
|
||||||
|
isFirst: isFirst(),
|
||||||
|
isLast: isLast(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,9 @@ export default (message: ApiMessage, sender?: ApiUser | ApiChat, chat?: ApiChat)
|
|||||||
const hash = (audio && audioCoverHash) || (voice && avatarHash);
|
const hash = (audio && audioCoverHash) || (voice && avatarHash);
|
||||||
const media = useMedia(hash);
|
const media = useMedia(hash);
|
||||||
|
|
||||||
const size = getCoverSize(audio, voice, media);
|
const size = useMemo(() => {
|
||||||
|
return getCoverSize(audio, voice, media);
|
||||||
|
}, [audio, media, voice]);
|
||||||
const { result: url } = useAsync(() => makeGoodArtwork(media, size), [media, size], telegramLogoPath);
|
const { result: url } = useAsync(() => makeGoodArtwork(media, size), [media, size], telegramLogoPath);
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return buildMediaMetadata({
|
return buildMediaMetadata({
|
||||||
|
|||||||
@ -170,7 +170,7 @@ addReducer('closeMediaViewer', (global) => {
|
|||||||
|
|
||||||
addReducer('openAudioPlayer', (global, actions, payload) => {
|
addReducer('openAudioPlayer', (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
chatId, threadId, messageId, origin,
|
chatId, threadId, messageId, origin, volume,
|
||||||
} = payload!;
|
} = payload!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -180,6 +180,21 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
|
|||||||
threadId,
|
threadId,
|
||||||
messageId,
|
messageId,
|
||||||
origin,
|
origin,
|
||||||
|
volume: volume || global.audioPlayer.volume,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('setAudioPlayerVolume', (global, actions, payload) => {
|
||||||
|
const {
|
||||||
|
volume,
|
||||||
|
} = payload!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
audioPlayer: {
|
||||||
|
...global.audioPlayer,
|
||||||
|
volume,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -187,7 +202,9 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
|
|||||||
addReducer('closeAudioPlayer', (global) => {
|
addReducer('closeAudioPlayer', (global) => {
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
audioPlayer: {},
|
audioPlayer: {
|
||||||
|
volume: global.audioPlayer.volume, // Preserve only volume for the next play
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-loop:before {
|
||||||
|
content: "\e981";
|
||||||
|
}
|
||||||
|
.icon-skip-next:before {
|
||||||
|
content: "\e982";
|
||||||
|
}
|
||||||
|
.icon-skip-previous:before {
|
||||||
|
content: "\e983";
|
||||||
|
}
|
||||||
|
.icon-volume-1:before {
|
||||||
|
content: "\e984";
|
||||||
|
}
|
||||||
|
.icon-volume-2:before {
|
||||||
|
content: "\e985";
|
||||||
|
}
|
||||||
|
.icon-volume-3:before {
|
||||||
|
content: "\e986";
|
||||||
|
}
|
||||||
.icon-bot-commands-filled:before {
|
.icon-bot-commands-filled:before {
|
||||||
content: "\e97f";
|
content: "\e97f";
|
||||||
}
|
}
|
||||||
@ -404,7 +422,7 @@
|
|||||||
.icon-eye:before {
|
.icon-eye:before {
|
||||||
content: "\e924";
|
content: "\e924";
|
||||||
}
|
}
|
||||||
.icon-muted-chat:before {
|
.icon-muted:before {
|
||||||
content: "\e95d";
|
content: "\e95d";
|
||||||
}
|
}
|
||||||
.icon-avatar-archived-chats:before {
|
.icon-avatar-archived-chats:before {
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export function updateMetadata(metadata?: MediaMetadata) {
|
|||||||
const { mediaSession } = window.navigator;
|
const { mediaSession } = window.navigator;
|
||||||
if (mediaSession) {
|
if (mediaSession) {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
mediaSession.metadata = metadata !== undefined ? metadata : null;
|
mediaSession.metadata = metadata ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ export function clearMediaSession() {
|
|||||||
mediaSession.metadata = null;
|
mediaSession.metadata = null;
|
||||||
setMediaSessionHandlers(DEFAULT_HANDLERS);
|
setMediaSessionHandlers(DEFAULT_HANDLERS);
|
||||||
if (mediaSession.playbackState) mediaSession.playbackState = 'none';
|
if (mediaSession.playbackState) mediaSession.playbackState = 'none';
|
||||||
if (mediaSession.setPositionState) mediaSession.setPositionState(undefined);
|
mediaSession.setPositionState?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,29 +85,23 @@ export function setPlaybackState(state: 'none' | 'paused' | 'playing' = 'none')
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setPositionState(state?: MediaPositionState) {
|
export function setPositionState(state?: MediaPositionState) {
|
||||||
if (!state || !state.position || !state.duration) return;
|
if (!state || state.position === undefined || state.duration === undefined) return;
|
||||||
|
state.position = Math.min(state.position, state.duration);
|
||||||
|
|
||||||
const { mediaSession } = window.navigator;
|
const { mediaSession } = window.navigator;
|
||||||
if (mediaSession && mediaSession.setPositionState) {
|
mediaSession?.setPositionState?.(state);
|
||||||
mediaSession.setPositionState(state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setMicrophoneActive(active: boolean) {
|
export function setMicrophoneActive(active: boolean) {
|
||||||
const { mediaSession } = window.navigator;
|
const { mediaSession } = window.navigator;
|
||||||
// @ts-ignore typings not updated yet
|
// @ts-ignore typings not updated yet
|
||||||
if (mediaSession && mediaSession.setMicrophoneActive) {
|
mediaSession?.setMicrophoneActive?.(active);
|
||||||
// @ts-ignore
|
|
||||||
mediaSession.setMicrophoneActive(active);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCameraActive(active: boolean) {
|
export function setCameraActive(active: boolean) {
|
||||||
const { mediaSession } = window.navigator;
|
const { mediaSession } = window.navigator;
|
||||||
// @ts-ignore typings not updated yet
|
// @ts-ignore typings not updated yet
|
||||||
if (mediaSession && mediaSession.setCameraActive) {
|
mediaSession?.setCameraActive?.(active);
|
||||||
// @ts-ignore
|
|
||||||
mediaSession.setCameraActive(active);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMediaMetadata({
|
export function buildMediaMetadata({
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default (mediaEl: HTMLMediaElement) => {
|
|||||||
mediaEl.play().catch((err) => {
|
mediaEl.play().catch((err) => {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(err);
|
console.warn(err, mediaEl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user