Media Viewer: Add volume and playback rate controls (#1768)

This commit is contained in:
Alexander Zinchuk 2022-03-19 21:19:51 +01:00
parent 36bdd5dd97
commit 12f5acb7e0
15 changed files with 413 additions and 138 deletions

View File

@ -75,19 +75,16 @@
.theme-dark body.initial & { .theme-dark body.initial & {
background-color: #0f0f0f; background-color: #0f0f0f;
background-image: url('../../assets/chat-bg-dark.png');
background-position: top left;
background-size: 650px;
background-repeat: repeat;
} }
.theme-light body.initial &, .theme-light body.initial &,
body:not(.initial) & { body:not(.initial) & {
background-image: url('../../assets/chat-bg.jpg'); background-image: url('../../assets/chat-bg.jpg');
} }
.theme-dark body.initial & {
background-image: url('../../assets/chat-bg-dark.png');
background-position: top left;
background-size: 650px;
background-repeat: repeat;
}
} }
html.theme-light body.animation-level-2 &.with-right-column::before { html.theme-light body.animation-level-2 &.with-right-column::before {

View File

@ -64,6 +64,9 @@ type StateProps = {
message?: ApiMessage; message?: ApiMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isProtected?: boolean; isProtected?: boolean;
volume: number;
isMuted: boolean;
playbackRate: number;
}; };
const ANIMATION_DURATION = 350; const ANIMATION_DURATION = 350;
@ -78,10 +81,13 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
profilePhotoIndex, profilePhotoIndex,
origin, origin,
animationLevel, animationLevel,
onClose,
onFooterClick,
isFooterHidden, isFooterHidden,
isProtected, isProtected,
volume,
playbackRate,
isMuted,
onClose,
onFooterClick,
setIsFooterHidden, setIsFooterHidden,
} = props; } = props;
/* Content */ /* Content */
@ -211,6 +217,9 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
toggleControls={toggleControls} toggleControls={toggleControls}
noPlay={!isActive} noPlay={!isActive}
onClose={onClose} onClose={onClose}
isMuted={isMuted}
volume={volume}
playbackRate={playbackRate}
/> />
))} ))}
{textParts && ( {textParts && (
@ -236,14 +245,20 @@ export default memo(withGlobal<OwnProps>(
origin, origin,
} = ownProps; } = ownProps;
const {
volume,
isMuted,
playbackRate,
} = global.mediaViewer;
if (origin === MediaViewerOrigin.SearchResult) { if (origin === MediaViewerOrigin.SearchResult) {
if (!(chatId && messageId)) { if (!(chatId && messageId)) {
return {}; return { volume, isMuted, playbackRate };
} }
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chatId, messageId);
if (!message) { if (!message) {
return {}; return { volume, isMuted, playbackRate };
} }
return { return {
@ -253,6 +268,9 @@ export default memo(withGlobal<OwnProps>(
origin, origin,
message, message,
isProtected: selectIsMessageProtected(global, message), isProtected: selectIsMessageProtected(global, message),
volume,
isMuted,
playbackRate,
}; };
} }
@ -265,11 +283,14 @@ export default memo(withGlobal<OwnProps>(
avatarOwner: sender, avatarOwner: sender,
profilePhotoIndex: profilePhotoIndex || 0, profilePhotoIndex: profilePhotoIndex || 0,
origin, origin,
volume,
isMuted,
playbackRate,
}; };
} }
if (!(chatId && threadId && messageId)) { if (!(chatId && threadId && messageId)) {
return {}; return { volume, isMuted, playbackRate };
} }
let message: ApiMessage | undefined; let message: ApiMessage | undefined;
@ -280,7 +301,7 @@ export default memo(withGlobal<OwnProps>(
} }
if (!message) { if (!message) {
return {}; return { volume, isMuted, playbackRate };
} }
return { return {
@ -291,6 +312,9 @@ export default memo(withGlobal<OwnProps>(
origin, origin,
message, message,
isProtected: selectIsMessageProtected(global, message), isProtected: selectIsMessageProtected(global, message),
volume,
isMuted,
playbackRate,
}; };
}, },
)(MediaViewerContent)); )(MediaViewerContent));

View File

@ -567,7 +567,10 @@ function checkIfInsideSelector(element: HTMLElement, selector: string) {
function checkIfControlTarget(e: TouchEvent | MouseEvent) { function checkIfControlTarget(e: TouchEvent | MouseEvent) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (checkIfInsideSelector(target, '.VideoPlayerControls')) { if (checkIfInsideSelector(target, '.VideoPlayerControls')) {
if (checkIfInsideSelector(target, '.play, .fullscreen')) { if (checkIfInsideSelector(
target,
'.play, .fullscreen, .volume, .volume-slider, .playback-rate, .playback-rate-menu',
)) {
return true; return true;
} }
e.preventDefault(); e.preventDefault();

View File

@ -1,6 +1,7 @@
import React, { import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions } from '../../global';
import { ApiDimensions } from '../../api/types'; import { ApiDimensions } from '../../api/types';
@ -28,6 +29,9 @@ type OwnProps = {
isMediaViewerOpen?: boolean; isMediaViewerOpen?: boolean;
noPlay?: boolean; noPlay?: boolean;
areControlsVisible: boolean; areControlsVisible: boolean;
volume: number;
isMuted: boolean;
playbackRate: number;
toggleControls: (isVisible: boolean) => void; toggleControls: (isVisible: boolean) => void;
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void; onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}; };
@ -43,10 +47,18 @@ const VideoPlayer: FC<OwnProps> = ({
fileSize, fileSize,
isMediaViewerOpen, isMediaViewerOpen,
noPlay, noPlay,
volume,
isMuted,
playbackRate,
onClose, onClose,
toggleControls, toggleControls,
areControlsVisible, areControlsVisible,
}) => { }) => {
const {
setMediaViewerVolume,
setMediaViewerMuted,
setMediaViewerPlaybackRate,
} = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS); const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS);
@ -84,6 +96,14 @@ const VideoPlayer: FC<OwnProps> = ({
} }
}, [currentTime]); }, [currentTime]);
useEffect(() => {
videoRef.current!.volume = volume;
}, [volume]);
useEffect(() => {
videoRef.current!.playbackRate = playbackRate;
}, [playbackRate]);
const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => { const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => {
e.stopPropagation(); e.stopPropagation();
if (isPlayed) { if (isPlayed) {
@ -129,6 +149,18 @@ const VideoPlayer: FC<OwnProps> = ({
videoRef.current!.currentTime = position; videoRef.current!.currentTime = position;
}, []); }, []);
const handleVolumeChange = useCallback((newVolume: number) => {
setMediaViewerVolume({ volume: newVolume / 100 });
}, [setMediaViewerVolume]);
const handleVolumeMuted = useCallback(() => {
setMediaViewerMuted({ isMuted: !isMuted });
}, [isMuted, setMediaViewerMuted]);
const handlePlaybackRateChange = useCallback((newPlaybackRate: number) => {
setMediaViewerPlaybackRate({ playbackRate: newPlaybackRate });
}, [setMediaViewerPlaybackRate]);
useEffect(() => { useEffect(() => {
if (!isMediaViewerOpen) return undefined; if (!isMediaViewerOpen) return undefined;
const togglePayingStateBySpace = (e: KeyboardEvent) => { const togglePayingStateBySpace = (e: KeyboardEvent) => {
@ -164,7 +196,7 @@ const VideoPlayer: FC<OwnProps> = ({
playsInline playsInline
loop={isGif} loop={isGif}
// This is to force auto playing on mobiles // This is to force auto playing on mobiles
muted={isGif} muted={isGif || isMuted}
id="media-viewer-video" id="media-viewer-video"
style={videoStyle} style={videoStyle}
onPlay={IS_IOS ? () => setIsPlayed(true) : undefined} onPlay={IS_IOS ? () => setIsPlayed(true) : undefined}
@ -198,6 +230,7 @@ const VideoPlayer: FC<OwnProps> = ({
<VideoPlayerControls <VideoPlayerControls
isPlayed={isPlayed} isPlayed={isPlayed}
bufferedProgress={bufferedProgress} bufferedProgress={bufferedProgress}
isBuffered={isBuffered}
currentTime={currentTime} currentTime={currentTime}
isFullscreenSupported={Boolean(setFullscreen)} isFullscreenSupported={Boolean(setFullscreen)}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
@ -209,6 +242,12 @@ const VideoPlayer: FC<OwnProps> = ({
onSeek={handleSeek} onSeek={handleSeek}
onChangeFullscreen={handleFullscreenChange} onChangeFullscreen={handleFullscreenChange}
onPlayPause={togglePlayState} onPlayPause={togglePlayState}
volume={volume}
playbackRate={playbackRate}
isMuted={isMuted}
onVolumeClick={handleVolumeMuted}
onVolumeChange={handleVolumeChange}
onPlaybackRateChange={handlePlaybackRateChange}
/> />
)} )}
</div> </div>

View File

@ -28,77 +28,67 @@
pointer-events: all; pointer-events: all;
} }
&.mobile { .buttons {
.player-file-size { display: flex;
position: static; align-items: center;
transform: none; width: 100%;
margin-left: auto;
}
.fullscreen {
margin-left: 1rem;
}
.player-time + .fullscreen {
margin-left: auto;
}
} }
.Button { .spacer {
width: 2.25rem; flex-grow: 1;
}
.Button.round {
width: 2rem;
padding: 0; padding: 0;
margin: 0.25rem; margin: 0.25rem;
height: 1.75rem; height: 2rem;
@media (max-width: 600px) { }
height: 2.25rem;
.volume-slider {
margin-bottom: 0;
margin-left: -0.75rem;
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
width: 0;
--volume-slider-width: 4rem;
--slider-color: #fff;
--color-borders: rgba(255, 255, 255, 0.5);
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: width 0.2s ease-in-out;
&:hover{
overflow: hidden;
width: var(--volume-slider-width);
.RangeSlider__input {
opacity: 1;
}
}
.RangeSlider__input {
margin-bottom: 0;
opacity: 0;
transition: opacity 0.15s ease-in-out;
} }
} }
.play i { .volume:hover + .volume-slider {
line-height: 1.5rem; overflow: hidden;
width: var(--volume-slider-width);
.RangeSlider__input {
opacity: 1;
}
} }
.player-time { .player-time {
margin: 0 1rem;
white-space: nowrap; white-space: nowrap;
} }
.fullscreen {
margin-left: auto;
@media (max-width: 600px) {
margin-left: 1rem;
}
i {
line-height: 1.75rem;
@media (max-width: 600px) {
font-size: 1.5rem;
}
}
}
@media (max-width: 600px) {
.player-time + .fullscreen {
margin-left: auto;
}
}
.player-file-size { .player-file-size {
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: 0.5rem;
@media (max-width: 600px) {
position: static;
transform: none;
margin-left: auto;
margin-right: 1rem;
& + .fullscreen {
margin-left: 0;
}
}
} }
.player-seekline { .player-seekline {
@ -155,4 +145,9 @@
} }
} }
} }
.playback-rate-menu .bubble {
min-width: 4rem;
margin-right: 4rem;
}
} }

View File

@ -1,19 +1,23 @@
import React, { import React, {
FC, useEffect, useRef, useCallback, FC, useEffect, useRef, useCallback, useMemo,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import useFlag from '../../hooks/useFlag';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { formatMediaDuration } from '../../util/dateFormat'; import { formatMediaDuration } from '../../util/dateFormat';
import formatFileSize from './helpers/formatFileSize'; import formatFileSize from './helpers/formatFileSize';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { captureEvents } from '../../util/captureEvents'; import { captureEvents } from '../../util/captureEvents';
import Button from '../ui/Button'; import Button from '../ui/Button';
import RangeSlider from '../ui/RangeSlider';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import './VideoPlayerControls.scss'; import './VideoPlayerControls.scss';
type IProps = { type OwnProps = {
bufferedProgress: number; bufferedProgress: number;
currentTime: number; currentTime: number;
duration: number; duration: number;
@ -22,9 +26,16 @@ type IProps = {
isPlayed: boolean; isPlayed: boolean;
isFullscreenSupported: boolean; isFullscreenSupported: boolean;
isFullscreen: boolean; isFullscreen: boolean;
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
isVisible: boolean; isVisible: boolean;
isBuffered: boolean;
volume: number;
isMuted: boolean;
playbackRate: number;
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onVolumeClick: () => void;
onVolumeChange: (volume: number) => void;
onPlaybackRateChange: (playbackRate: number) => void;
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
setVisibility: (isVisible: boolean) => void; setVisibility: (isVisible: boolean) => void;
onSeek: (position: number) => void; onSeek: (position: number) => void;
}; };
@ -33,9 +44,16 @@ const stopEvent = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation(); e.stopPropagation();
}; };
const PLAYBACK_RATES = [
0.5,
1,
1.5,
2,
];
const HIDE_CONTROLS_TIMEOUT_MS = 1500; const HIDE_CONTROLS_TIMEOUT_MS = 1500;
const VideoPlayerControls: FC<IProps> = ({ const VideoPlayerControls: FC<OwnProps> = ({
bufferedProgress, bufferedProgress,
currentTime, currentTime,
duration, duration,
@ -44,12 +62,20 @@ const VideoPlayerControls: FC<IProps> = ({
isPlayed, isPlayed,
isFullscreenSupported, isFullscreenSupported,
isFullscreen, isFullscreen,
onChangeFullscreen,
onPlayPause,
isVisible, isVisible,
isBuffered,
volume,
isMuted,
playbackRate,
onChangeFullscreen,
onVolumeClick,
onVolumeChange,
onPlaybackRateChange,
onPlayPause,
setVisibility, setVisibility,
onSeek, onSeek,
}) => { }) => {
const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const seekerRef = useRef<HTMLDivElement>(null); const seekerRef = useRef<HTMLDivElement>(null);
const isSeekingRef = useRef<boolean>(false); const isSeekingRef = useRef<boolean>(false);
@ -57,7 +83,7 @@ const VideoPlayerControls: FC<IProps> = ({
useEffect(() => { useEffect(() => {
let timeout: number | undefined; let timeout: number | undefined;
if (!isVisible || !isPlayed || isSeeking) { if (!isVisible || !isPlayed || isSeeking || isPlaybackMenuOpen) {
if (timeout) window.clearTimeout(timeout); if (timeout) window.clearTimeout(timeout);
return undefined; return undefined;
} }
@ -67,7 +93,7 @@ const VideoPlayerControls: FC<IProps> = ({
return () => { return () => {
if (timeout) window.clearTimeout(timeout); if (timeout) window.clearTimeout(timeout);
}; };
}, [isPlayed, isVisible, isSeeking, setVisibility]); }, [isPlayed, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]);
useEffect(() => { useEffect(() => {
if (isVisible) { if (isVisible) {
@ -80,6 +106,12 @@ const VideoPlayerControls: FC<IProps> = ({
}; };
}, [isVisible]); }, [isVisible]);
useEffect(() => {
if (!isVisible) {
closePlaybackMenu();
}
}, [closePlaybackMenu, isVisible]);
const lang = useLang(); const lang = useLang();
const handleSeek = useCallback((e: MouseEvent | TouchEvent) => { const handleSeek = useCallback((e: MouseEvent | TouchEvent) => {
@ -112,35 +144,84 @@ const VideoPlayerControls: FC<IProps> = ({
}); });
}, [isVisible, handleStartSeek, handleSeek, handleStopSeek]); }, [isVisible, handleStartSeek, handleSeek, handleStopSeek]);
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]);
return ( return (
<div <div
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')} className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')}
onClick={stopEvent} onClick={stopEvent}
> >
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)} {renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
<Button <div className="buttons">
ariaLabel={lang('AccActionPlay')}
size="tiny"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
color="translucent-white"
className="play"
onClick={onPlayPause}
>
<i className={isPlayed ? 'icon-pause' : 'icon-play'} />
</Button>
{renderTime(currentTime, duration)}
{bufferedProgress < 1 && renderFileSize(bufferedProgress, fileSize)}
{isFullscreenSupported && (
<Button <Button
ariaLabel="Fullscreen" ariaLabel={lang('AccActionPlay')}
size="tiny"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
color="translucent-white"
className="play"
round
onClick={onPlayPause}
>
<i className={isPlayed ? 'icon-pause' : 'icon-play'} />
</Button>
<Button
ariaLabel="Volume"
size="tiny" size="tiny"
color="translucent-white" color="translucent-white"
className="fullscreen" className="volume"
onClick={onChangeFullscreen} round
onClick={onVolumeClick}
> >
<i className={`${isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'}`} /> <i className={volumeIcon} />
</Button> </Button>
)} {!IS_IOS && (
<RangeSlider bold className="volume-slider" value={isMuted ? 0 : volume * 100} onChange={onVolumeChange} />
)}
{renderTime(currentTime, duration)}
{!isBuffered && renderFileSize(bufferedProgress, fileSize)}
<div className="spacer" />
<Button
ariaLabel="Playback rate"
size="tiny"
color="translucent-white"
className="playback-rate"
round
onClick={openPlaybackMenu}
>
{`${playbackRate}x`}
</Button>
{isFullscreenSupported && (
<Button
ariaLabel="Fullscreen"
size="tiny"
color="translucent-white"
className="fullscreen"
round
onClick={onChangeFullscreen}
>
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} />
</Button>
)}
</div>
<Menu
isOpen={isPlaybackMenuOpen}
className="playback-rate-menu"
positionX="right"
positionY="bottom"
autoClose
onClose={closePlaybackMenu}
>
{PLAYBACK_RATES.map((rate) => (
<MenuItem disabled={playbackRate === rate} onClick={() => onPlaybackRateChange(rate)}>
{`${rate}x`}
</MenuItem>
))}
</Menu>
</div> </div>
); );
}; };

View File

@ -216,7 +216,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
<> <>
<div className="volume-slider-spacer" /> <div className="volume-slider-spacer" />
<div className="volume-slider"> <div className="volume-slider">
<RangeSlider value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} /> <RangeSlider bold value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
</div> </div>
</> </>
)} )}

View File

@ -73,6 +73,18 @@
pointer-events: none; pointer-events: none;
} }
&.bold {
.slider-main::before {
top: 0.25rem;
height: 0.25rem;
}
.slider-fill-track {
top: 0.25rem;
height: 0.25rem;
}
}
// Reset range input browser styles // Reset range input browser styles
@include reset-range(); @include reset-range();

View File

@ -15,8 +15,10 @@ type OwnProps = {
step?: number; step?: number;
label?: string; label?: string;
value: number; value: number;
renderValue?: (value: number) => string;
disabled?: boolean; disabled?: boolean;
bold?: boolean;
className?: string;
renderValue?: (value: number) => string;
onChange: (value: number) => void; onChange: (value: number) => void;
}; };
@ -27,8 +29,10 @@ const RangeSlider: FC<OwnProps> = ({
step = 1, step = 1,
label, label,
value, value,
renderValue,
disabled, disabled,
bold,
className,
renderValue,
onChange, onChange,
}) => { }) => {
const lang = useLang(); const lang = useLang();
@ -36,9 +40,11 @@ const RangeSlider: FC<OwnProps> = ({
onChange(Number(event.currentTarget.value)); onChange(Number(event.currentTarget.value));
}, [onChange]); }, [onChange]);
const className = buildClassName( const mainClassName = buildClassName(
className,
'RangeSlider', 'RangeSlider',
disabled && 'disabled', disabled && 'disabled',
bold && 'bold',
); );
const trackWidth = useMemo(() => { const trackWidth = useMemo(() => {
@ -51,7 +57,7 @@ const RangeSlider: FC<OwnProps> = ({
}, [options, value, max, min, step]); }, [options, value, max, min, step]);
return ( return (
<div className={className}> <div className={mainClassName}>
{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>
@ -71,6 +77,7 @@ const RangeSlider: FC<OwnProps> = ({
value={value} value={value}
step={step} step={step}
type="range" type="range"
className="RangeSlider__input"
onChange={handleChange} onChange={handleChange}
/> />
{options && ( {options && (

View File

@ -9,6 +9,7 @@ import './ui/settings';
import './ui/misc'; import './ui/misc';
import './ui/payments'; import './ui/payments';
import './ui/calls'; import './ui/calls';
import './ui/mediaViewer';
import './api/initial'; import './api/initial';
import './api/chats'; import './api/chats';

View File

@ -0,0 +1,78 @@
import { addActionHandler } from '../../index';
addActionHandler('openMediaViewer', (global, actions, payload) => {
const {
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin, volume, playbackRate, isMuted,
} = payload;
return {
...global,
mediaViewer: {
...global.mediaViewer,
chatId,
threadId,
messageId,
avatarOwnerId,
profilePhotoIndex,
origin,
volume: volume ?? global.mediaViewer.volume,
playbackRate: playbackRate || global.mediaViewer.playbackRate,
isMuted: isMuted || global.mediaViewer.isMuted,
},
forwardMessages: {},
};
});
addActionHandler('closeMediaViewer', (global) => {
const { volume, isMuted, playbackRate } = global.mediaViewer;
return {
...global,
mediaViewer: {
volume,
isMuted,
playbackRate,
},
};
});
addActionHandler('setMediaViewerVolume', (global, actions, payload) => {
const {
volume,
} = payload;
return {
...global,
mediaViewer: {
...global.mediaViewer,
volume,
},
};
});
addActionHandler('setMediaViewerPlaybackRate', (global, actions, payload) => {
const {
playbackRate,
} = payload;
return {
...global,
mediaViewer: {
...global.mediaViewer,
playbackRate,
},
};
});
addActionHandler('setMediaViewerMuted', (global, actions, payload) => {
const {
isMuted,
} = payload;
return {
...global,
mediaViewer: {
...global.mediaViewer,
isMuted,
},
};
});

View File

@ -156,36 +156,10 @@ addActionHandler('replyToNextMessage', (global, actions, payload) => {
}); });
}); });
addActionHandler('openMediaViewer', (global, actions, payload) => {
const {
chatId, threadId, messageId, avatarOwnerId, profilePhotoIndex, origin,
} = payload!;
return {
...global,
mediaViewer: {
chatId,
threadId,
messageId,
avatarOwnerId,
profilePhotoIndex,
origin,
},
forwardMessages: {},
};
});
addActionHandler('closeMediaViewer', (global) => {
return {
...global,
mediaViewer: {},
};
});
addActionHandler('openAudioPlayer', (global, actions, payload) => { addActionHandler('openAudioPlayer', (global, actions, payload) => {
const { const {
chatId, threadId, messageId, origin, volume, playbackRate, isMuted, chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
} = payload!; } = payload;
return { return {
...global, ...global,
@ -204,7 +178,7 @@ addActionHandler('openAudioPlayer', (global, actions, payload) => {
addActionHandler('setAudioPlayerVolume', (global, actions, payload) => { addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
const { const {
volume, volume,
} = payload!; } = payload;
return { return {
...global, ...global,
@ -218,7 +192,7 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => { addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
const { const {
playbackRate, playbackRate,
} = payload!; } = payload;
return { return {
...global, ...global,
@ -232,7 +206,7 @@ addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
addActionHandler('setAudioPlayerMuted', (global, actions, payload) => { addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
const { const {
isMuted, isMuted,
} = payload!; } = payload;
return { return {
...global, ...global,
@ -246,7 +220,7 @@ addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
addActionHandler('setAudioPlayerOrigin', (global, actions, payload) => { addActionHandler('setAudioPlayerOrigin', (global, actions, payload) => {
const { const {
origin, origin,
} = payload!; } = payload;
return { return {
...global, ...global,

View File

@ -196,6 +196,14 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE; cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
} }
if (cached.mediaViewer.volume === undefined) {
cached.mediaViewer.volume = DEFAULT_VOLUME;
}
if (cached.mediaViewer.playbackRate === undefined) {
cached.mediaViewer.playbackRate = DEFAULT_PLAYBACK_RATE;
}
if (!cached.groupCalls) { if (!cached.groupCalls) {
cached.groupCalls = initialState.groupCalls; cached.groupCalls = initialState.groupCalls;
} }
@ -246,6 +254,11 @@ function updateCache() {
playbackRate: global.audioPlayer.playbackRate, playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted, isMuted: global.audioPlayer.isMuted,
}, },
mediaViewer: {
volume: global.mediaViewer.volume,
playbackRate: global.mediaViewer.playbackRate,
isMuted: global.mediaViewer.isMuted,
},
isChatInfoShown: reduceShowChatInfo(global), isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global), users: reduceUsers(global),
chats: reduceChats(global), chats: reduceChats(global),

View File

@ -116,7 +116,11 @@ export const INITIAL_STATE: GlobalState = {
topInlineBots: {}, topInlineBots: {},
mediaViewer: {}, mediaViewer: {
volume: DEFAULT_VOLUME,
playbackRate: DEFAULT_PLAYBACK_RATE,
isMuted: false,
},
audioPlayer: { audioPlayer: {
volume: DEFAULT_VOLUME, volume: DEFAULT_VOLUME,

View File

@ -374,6 +374,9 @@ export type GlobalState = {
avatarOwnerId?: string; avatarOwnerId?: string;
profilePhotoIndex?: number; profilePhotoIndex?: number;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
volume: number;
playbackRate: number;
isMuted: boolean;
}; };
audioPlayer: { audioPlayer: {
@ -521,12 +524,60 @@ export interface ActionPayloads {
type?: MessageListType; type?: MessageListType;
shouldReplaceHistory?: boolean; shouldReplaceHistory?: boolean;
}; };
// Messages
setEditingDraft: { setEditingDraft: {
text?: ApiFormattedText; text?: ApiFormattedText;
chatId: string; chatId: string;
threadId: number; threadId: number;
type: MessageListType; type: MessageListType;
}; };
// Media Viewer & Audio Player
openMediaViewer: {
chatId?: string;
threadId?: number;
messageId?: number;
avatarOwnerId?: string;
profilePhotoIndex?: number;
origin?: MediaViewerOrigin;
volume?: number;
playbackRate?: number;
isMuted?: boolean;
};
closeMediaViewer: {};
setMediaViewerVolume: {
volume: number;
};
setMediaViewerPlaybackRate: {
playbackRate: number;
};
setMediaViewerMuted: {
isMuted: boolean;
};
openAudioPlayer: {
chatId: string;
threadId?: number;
messageId: number;
origin?: AudioOrigin;
volume?: number;
playbackRate?: number;
isMuted?: boolean;
};
closeAudioPlayer: {};
setAudioPlayerVolume: {
volume: number;
};
setAudioPlayerPlaybackRate: {
playbackRate: number;
};
setAudioPlayerMuted: {
isMuted: boolean;
};
setAudioPlayerOrigin: {
origin: AudioOrigin;
};
} }
export type NonTypedActionNames = ( export type NonTypedActionNames = (
@ -613,10 +664,6 @@ export type NonTypedActionNames = (
// bots // bots
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' | 'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' | 'startBot' | 'resetInlineBot' | 'restartBot' | 'startBot' |
// media viewer & audio player
'openMediaViewer' | 'closeMediaViewer' |
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted' | 'setAudioPlayerOrigin' | 'closeAudioPlayer' |
// misc // misc
'openPollModal' | 'closePollModal' | 'openPollModal' | 'closePollModal' |
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |