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);
|
||||
}
|
||||
|
||||
.icon-muted-chat {
|
||||
.icon-muted {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.icon-muted-chat {
|
||||
.icon-muted {
|
||||
font-size: 1.25rem;
|
||||
margin-left: 0.25rem;
|
||||
margin-top: -.0625rem;
|
||||
@ -194,7 +194,7 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.icon-muted-chat {
|
||||
.icon-muted {
|
||||
margin-left: 0;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@ -290,7 +290,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<div className="title">
|
||||
<h3>{renderText(getChatTitle(lang, chat, privateChatUser))}</h3>
|
||||
{chat.isVerified && <VerifiedIcon />}
|
||||
{isMuted && <i className="icon-muted-chat" />}
|
||||
{isMuted && <i className="icon-muted" />}
|
||||
{chat.lastMessage && (
|
||||
<LastMessageMeta
|
||||
message={chat.lastMessage}
|
||||
|
||||
@ -156,8 +156,8 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
<RangeSlider
|
||||
label={lang('TextSize')}
|
||||
// TODO Remove memo-killer
|
||||
range={{ min: 12, max: 20 }}
|
||||
min={12}
|
||||
max={20}
|
||||
value={messageTextSize}
|
||||
onChange={handleMessageTextSizeChange}
|
||||
/>
|
||||
|
||||
@ -124,10 +124,8 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<RangeSlider
|
||||
label="Sound"
|
||||
disabled={!hasWebNotifications}
|
||||
range={{
|
||||
min: 0,
|
||||
max: 10,
|
||||
}}
|
||||
min={0}
|
||||
max={10}
|
||||
value={notificationSoundVolume}
|
||||
onChange={(volume) => {
|
||||
updateWebNotificationSettings({ notificationSoundVolume: volume });
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
margin: .125rem;
|
||||
}
|
||||
|
||||
> .toggle-play {
|
||||
> .player-button {
|
||||
--color-text-secondary: var(--color-primary);
|
||||
--color-text-secondary-rgb: var(--color-primary-shade-rgb);
|
||||
--color-primary-shade: var(--color-green);
|
||||
@ -24,7 +24,7 @@
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-play {
|
||||
.player-button {
|
||||
&.smaller {
|
||||
width: 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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -69,7 +118,6 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
max-width: 15rem;
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
|
||||
&: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 { AudioOrigin } from '../../types';
|
||||
@ -7,7 +7,7 @@ import {
|
||||
ApiAudio, ApiChat, ApiMessage, ApiUser,
|
||||
} 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 {
|
||||
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle,
|
||||
@ -24,6 +24,7 @@ import { clearMediaSession } from '../../util/mediaSession';
|
||||
|
||||
import RippleEffect from '../ui/RippleEffect';
|
||||
import Button from '../ui/Button';
|
||||
import RangeSlider from '../ui/RangeSlider';
|
||||
|
||||
import './AudioPlayer.scss';
|
||||
|
||||
@ -37,12 +38,22 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
sender?: ApiChat | ApiUser;
|
||||
chat?: ApiChat;
|
||||
volume: number;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer'>;
|
||||
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer' | 'setAudioPlayerVolume'>;
|
||||
|
||||
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 { audio, voice } = getMessageContent(message);
|
||||
@ -50,7 +61,10 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
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 } = useAudioPlayer(
|
||||
|
||||
const {
|
||||
playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume,
|
||||
} = useAudioPlayer(
|
||||
makeTrackId(message),
|
||||
getMediaDuration(message)!,
|
||||
isVoice ? 'voice' : 'audio',
|
||||
@ -78,6 +92,18 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
@ -89,19 +115,59 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||
color="translucent"
|
||||
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}
|
||||
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>
|
||||
|
||||
{!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
|
||||
round
|
||||
className="player-close"
|
||||
@ -142,11 +208,13 @@ export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { volume } = global.audioPlayer;
|
||||
|
||||
return {
|
||||
sender,
|
||||
chat,
|
||||
volume,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer']),
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume']),
|
||||
)(AudioPlayer);
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2.875rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||
|
||||
display: flex;
|
||||
|
||||
@ -248,7 +248,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
)}
|
||||
<div className="message-media-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>
|
||||
);
|
||||
|
||||
@ -516,7 +516,7 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.message-media-duration .icon-muted-chat {
|
||||
.message-media-duration .icon-muted {
|
||||
vertical-align: -.1875rem;
|
||||
margin-left: .375rem;
|
||||
font-size: 1.0625rem;
|
||||
|
||||
@ -32,6 +32,7 @@ export type OwnProps = {
|
||||
faded?: boolean;
|
||||
tabIndex?: number;
|
||||
isRtl?: boolean;
|
||||
withClickPropagation?: boolean;
|
||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||
@ -70,6 +71,7 @@ const Button: FC<OwnProps> = ({
|
||||
faded,
|
||||
tabIndex,
|
||||
isRtl,
|
||||
withClickPropagation,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
|
||||
@ -108,11 +110,11 @@ const Button: FC<OwnProps> = ({
|
||||
}, [disabled, onClick]);
|
||||
|
||||
const handleMouseDown = useCallback((e: ReactMouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!withClickPropagation) e.preventDefault();
|
||||
if (!disabled && onMouseDown) {
|
||||
onMouseDown(e);
|
||||
}
|
||||
}, [onMouseDown, disabled]);
|
||||
}, [onMouseDown, disabled, withClickPropagation]);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
|
||||
@ -100,6 +100,14 @@
|
||||
&::-moz-slider-thumb {
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-track, &::-moz-range-progress {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom styles
|
||||
|
||||
@ -10,7 +10,9 @@ import './RangeSlider.scss';
|
||||
|
||||
type OwnProps = {
|
||||
options?: string[];
|
||||
range?: { min: number; max: number; step?: number };
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
@ -19,7 +21,9 @@ type OwnProps = {
|
||||
|
||||
const RangeSlider: FC<OwnProps> = ({
|
||||
options,
|
||||
range,
|
||||
min = 0,
|
||||
max = options ? options.length - 1 : 100,
|
||||
step = 1,
|
||||
label,
|
||||
value,
|
||||
disabled,
|
||||
@ -38,29 +42,19 @@ const RangeSlider: FC<OwnProps> = ({
|
||||
const trackWidth = useMemo(() => {
|
||||
if (options) {
|
||||
return (value / (options.length - 1)) * 100;
|
||||
} else if (range) {
|
||||
const possibleValuesLength = (range.max - range.min) / (range.step || 1);
|
||||
return ((value - range.min) / possibleValuesLength) * 100;
|
||||
} else {
|
||||
const possibleValuesLength = (max - min) / step;
|
||||
return ((value - min) / possibleValuesLength) * 100;
|
||||
}
|
||||
return 0;
|
||||
}, [value, options, range]);
|
||||
|
||||
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]);
|
||||
}, [options, value, max, min, step]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<div className="slider-top-row" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<span className="label" dir="auto">{label}</span>
|
||||
{range && (
|
||||
{!options && (
|
||||
<span className="value" dir="auto">{value}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -60,6 +60,8 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
||||
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
||||
|
||||
export const DEFAULT_VOLUME = 1;
|
||||
|
||||
export const ANIMATION_LEVEL_MIN = 0;
|
||||
export const ANIMATION_LEVEL_MED = 1;
|
||||
export const ANIMATION_LEVEL_MAX = 2;
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
|
||||
DEFAULT_VOLUME,
|
||||
} from '../config';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
|
||||
import { ANIMATION_END_EVENT, ANIMATION_START_EVENT } from '../hooks/useHeavyAnimationCheck';
|
||||
@ -132,6 +133,10 @@ function readCache(initialState: GlobalState): GlobalState {
|
||||
if (!cached.serviceNotifications) {
|
||||
cached.serviceNotifications = [];
|
||||
}
|
||||
|
||||
if (cached.audioPlayer.volume === undefined) {
|
||||
cached.audioPlayer.volume = DEFAULT_VOLUME;
|
||||
}
|
||||
}
|
||||
|
||||
const newState = {
|
||||
@ -178,6 +183,9 @@ function updateCache() {
|
||||
'leftColumnWidth',
|
||||
'serviceNotifications',
|
||||
]),
|
||||
audioPlayer: {
|
||||
volume: global.audioPlayer.volume,
|
||||
},
|
||||
isChatInfoShown: reduceShowChatInfo(global),
|
||||
users: reduceUsers(global),
|
||||
chats: reduceChats(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_VOLUME,
|
||||
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
|
||||
} from '../config';
|
||||
import { IS_IOS, IS_MAC_OS } from '../util/environment';
|
||||
@ -110,7 +111,9 @@ export const INITIAL_STATE: GlobalState = {
|
||||
|
||||
mediaViewer: {},
|
||||
|
||||
audioPlayer: {},
|
||||
audioPlayer: {
|
||||
volume: DEFAULT_VOLUME,
|
||||
},
|
||||
|
||||
forwardMessages: {},
|
||||
|
||||
|
||||
@ -328,6 +328,7 @@ export type GlobalState = {
|
||||
messageId?: number;
|
||||
threadId?: number;
|
||||
origin?: AudioOrigin;
|
||||
volume: number;
|
||||
};
|
||||
|
||||
topPeers: {
|
||||
@ -523,9 +524,10 @@ export type ActionTypes = (
|
||||
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||
'resetInlineBot' | 'restartBot' | 'startBot' |
|
||||
// misc
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
||||
'deleteDeviceToken' | 'checkVersionNotification' | 'createServiceNotification' |
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'closeAudioPlayer' |
|
||||
'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' |
|
||||
'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' |
|
||||
'checkVersionNotification' | 'createServiceNotification' |
|
||||
// payment
|
||||
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
|
||||
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { getDispatch } from '../lib/teact/teactn';
|
||||
import { getDispatch, getGlobal } from '../lib/teact/teactn';
|
||||
|
||||
import { AudioOrigin } from '../types';
|
||||
|
||||
@ -48,12 +48,21 @@ export default (
|
||||
useOnChange(() => {
|
||||
controllerRef.current = register(trackId, trackType, origin, (eventName, e) => {
|
||||
switch (eventName) {
|
||||
case 'onPlay':
|
||||
case 'onPlay': {
|
||||
const { setVolume, proxy } = controllerRef.current!;
|
||||
setIsPlaying(true);
|
||||
|
||||
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
|
||||
setPlaybackState('playing');
|
||||
setVolume(getGlobal().audioPlayer.volume);
|
||||
|
||||
setPositionState({
|
||||
duration: proxy.duration || 0,
|
||||
playbackRate: proxy.playbackRate,
|
||||
position: proxy.currentTime,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'onPause':
|
||||
setIsPlaying(false);
|
||||
setPlaybackState('paused');
|
||||
@ -62,11 +71,6 @@ export default (
|
||||
const { proxy } = controllerRef.current!;
|
||||
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
|
||||
if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration);
|
||||
setPositionState({
|
||||
duration: proxy.duration,
|
||||
playbackRate: proxy.playbackRate,
|
||||
position: proxy.currentTime,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'onEnded': {
|
||||
@ -95,7 +99,18 @@ export default (
|
||||
}, [metadata, isPlaying]);
|
||||
|
||||
const {
|
||||
play, pause, setCurrentTime, proxy, destroy, setVolume, setCurrentOrigin, stop,
|
||||
play,
|
||||
pause,
|
||||
setCurrentTime,
|
||||
proxy,
|
||||
destroy,
|
||||
setVolume,
|
||||
setCurrentOrigin,
|
||||
stop,
|
||||
isFirst,
|
||||
isLast,
|
||||
requestNextTrack,
|
||||
requestPreviousTrack,
|
||||
} = controllerRef.current!;
|
||||
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
|
||||
|
||||
@ -160,6 +175,10 @@ export default (
|
||||
setVolume,
|
||||
audioProxy: proxy,
|
||||
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 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);
|
||||
return useMemo(() => {
|
||||
return buildMediaMetadata({
|
||||
|
||||
@ -170,7 +170,7 @@ addReducer('closeMediaViewer', (global) => {
|
||||
|
||||
addReducer('openAudioPlayer', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, threadId, messageId, origin,
|
||||
chatId, threadId, messageId, origin, volume,
|
||||
} = payload!;
|
||||
|
||||
return {
|
||||
@ -180,6 +180,21 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
|
||||
threadId,
|
||||
messageId,
|
||||
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) => {
|
||||
return {
|
||||
...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 {
|
||||
content: "\e97f";
|
||||
}
|
||||
@ -404,7 +422,7 @@
|
||||
.icon-eye:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon-muted-chat:before {
|
||||
.icon-muted:before {
|
||||
content: "\e95d";
|
||||
}
|
||||
.icon-avatar-archived-chats:before {
|
||||
|
||||
@ -48,7 +48,7 @@ export function updateMetadata(metadata?: MediaMetadata) {
|
||||
const { mediaSession } = window.navigator;
|
||||
if (mediaSession) {
|
||||
// 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;
|
||||
setMediaSessionHandlers(DEFAULT_HANDLERS);
|
||||
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) {
|
||||
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;
|
||||
if (mediaSession && mediaSession.setPositionState) {
|
||||
mediaSession.setPositionState(state);
|
||||
}
|
||||
mediaSession?.setPositionState?.(state);
|
||||
}
|
||||
|
||||
export function setMicrophoneActive(active: boolean) {
|
||||
const { mediaSession } = window.navigator;
|
||||
// @ts-ignore typings not updated yet
|
||||
if (mediaSession && mediaSession.setMicrophoneActive) {
|
||||
// @ts-ignore
|
||||
mediaSession.setMicrophoneActive(active);
|
||||
}
|
||||
mediaSession?.setMicrophoneActive?.(active);
|
||||
}
|
||||
|
||||
export function setCameraActive(active: boolean) {
|
||||
const { mediaSession } = window.navigator;
|
||||
// @ts-ignore typings not updated yet
|
||||
if (mediaSession && mediaSession.setCameraActive) {
|
||||
// @ts-ignore
|
||||
mediaSession.setCameraActive(active);
|
||||
}
|
||||
mediaSession?.setCameraActive?.(active);
|
||||
}
|
||||
|
||||
export function buildMediaMetadata({
|
||||
|
||||
@ -4,7 +4,7 @@ export default (mediaEl: HTMLMediaElement) => {
|
||||
mediaEl.play().catch((err) => {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
console.warn(err, mediaEl);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user