Media Viewer: Add volume and playback rate controls (#1768)
This commit is contained in:
parent
36bdd5dd97
commit
12f5acb7e0
@ -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 {
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
78
src/global/actions/ui/mediaViewer.ts
Normal file
78
src/global/actions/ui/mediaViewer.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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' |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user