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 & {
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 &,
body:not(.initial) & {
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 {

View File

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

View File

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

View File

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

View File

@ -28,77 +28,67 @@
pointer-events: all;
}
&.mobile {
.player-file-size {
position: static;
transform: none;
margin-left: auto;
}
.fullscreen {
margin-left: 1rem;
}
.player-time + .fullscreen {
margin-left: auto;
}
.buttons {
display: flex;
align-items: center;
width: 100%;
}
.Button {
width: 2.25rem;
.spacer {
flex-grow: 1;
}
.Button.round {
width: 2rem;
padding: 0;
margin: 0.25rem;
height: 1.75rem;
@media (max-width: 600px) {
height: 2.25rem;
height: 2rem;
}
.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 {
line-height: 1.5rem;
.volume:hover + .volume-slider {
overflow: hidden;
width: var(--volume-slider-width);
.RangeSlider__input {
opacity: 1;
}
}
.player-time {
margin: 0 1rem;
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 {
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (max-width: 600px) {
position: static;
transform: none;
margin-left: auto;
margin-right: 1rem;
& + .fullscreen {
margin-left: 0;
}
}
margin-left: 0.5rem;
}
.player-seekline {
@ -155,4 +145,9 @@
}
}
}
.playback-rate-menu .bubble {
min-width: 4rem;
margin-right: 4rem;
}
}

View File

@ -1,19 +1,23 @@
import React, {
FC, useEffect, useRef, useCallback,
FC, useEffect, useRef, useCallback, useMemo,
} from '../../lib/teact/teact';
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 formatFileSize from './helpers/formatFileSize';
import useLang from '../../hooks/useLang';
import { captureEvents } from '../../util/captureEvents';
import Button from '../ui/Button';
import RangeSlider from '../ui/RangeSlider';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import './VideoPlayerControls.scss';
type IProps = {
type OwnProps = {
bufferedProgress: number;
currentTime: number;
duration: number;
@ -22,9 +26,16 @@ type IProps = {
isPlayed: boolean;
isFullscreenSupported: boolean;
isFullscreen: boolean;
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
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;
onSeek: (position: number) => void;
};
@ -33,9 +44,16 @@ const stopEvent = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
};
const PLAYBACK_RATES = [
0.5,
1,
1.5,
2,
];
const HIDE_CONTROLS_TIMEOUT_MS = 1500;
const VideoPlayerControls: FC<IProps> = ({
const VideoPlayerControls: FC<OwnProps> = ({
bufferedProgress,
currentTime,
duration,
@ -44,12 +62,20 @@ const VideoPlayerControls: FC<IProps> = ({
isPlayed,
isFullscreenSupported,
isFullscreen,
onChangeFullscreen,
onPlayPause,
isVisible,
isBuffered,
volume,
isMuted,
playbackRate,
onChangeFullscreen,
onVolumeClick,
onVolumeChange,
onPlaybackRateChange,
onPlayPause,
setVisibility,
onSeek,
}) => {
const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag();
// eslint-disable-next-line no-null/no-null
const seekerRef = useRef<HTMLDivElement>(null);
const isSeekingRef = useRef<boolean>(false);
@ -57,7 +83,7 @@ const VideoPlayerControls: FC<IProps> = ({
useEffect(() => {
let timeout: number | undefined;
if (!isVisible || !isPlayed || isSeeking) {
if (!isVisible || !isPlayed || isSeeking || isPlaybackMenuOpen) {
if (timeout) window.clearTimeout(timeout);
return undefined;
}
@ -67,7 +93,7 @@ const VideoPlayerControls: FC<IProps> = ({
return () => {
if (timeout) window.clearTimeout(timeout);
};
}, [isPlayed, isVisible, isSeeking, setVisibility]);
}, [isPlayed, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]);
useEffect(() => {
if (isVisible) {
@ -80,6 +106,12 @@ const VideoPlayerControls: FC<IProps> = ({
};
}, [isVisible]);
useEffect(() => {
if (!isVisible) {
closePlaybackMenu();
}
}, [closePlaybackMenu, isVisible]);
const lang = useLang();
const handleSeek = useCallback((e: MouseEvent | TouchEvent) => {
@ -112,35 +144,84 @@ const VideoPlayerControls: FC<IProps> = ({
});
}, [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 (
<div
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')}
onClick={stopEvent}
>
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)}
<Button
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 && (
<div className="buttons">
<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"
color="translucent-white"
className="fullscreen"
onClick={onChangeFullscreen}
className="volume"
round
onClick={onVolumeClick}
>
<i className={`${isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'}`} />
<i className={volumeIcon} />
</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>
);
};

View File

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

View File

@ -73,6 +73,18 @@
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
@include reset-range();

View File

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

View File

@ -9,6 +9,7 @@ import './ui/settings';
import './ui/misc';
import './ui/payments';
import './ui/calls';
import './ui/mediaViewer';
import './api/initial';
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) => {
const {
chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
} = payload!;
} = payload;
return {
...global,
@ -204,7 +178,7 @@ addActionHandler('openAudioPlayer', (global, actions, payload) => {
addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
const {
volume,
} = payload!;
} = payload;
return {
...global,
@ -218,7 +192,7 @@ addActionHandler('setAudioPlayerVolume', (global, actions, payload) => {
addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
const {
playbackRate,
} = payload!;
} = payload;
return {
...global,
@ -232,7 +206,7 @@ addActionHandler('setAudioPlayerPlaybackRate', (global, actions, payload) => {
addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
const {
isMuted,
} = payload!;
} = payload;
return {
...global,
@ -246,7 +220,7 @@ addActionHandler('setAudioPlayerMuted', (global, actions, payload) => {
addActionHandler('setAudioPlayerOrigin', (global, actions, payload) => {
const {
origin,
} = payload!;
} = payload;
return {
...global,

View File

@ -196,6 +196,14 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
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) {
cached.groupCalls = initialState.groupCalls;
}
@ -246,6 +254,11 @@ function updateCache() {
playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted,
},
mediaViewer: {
volume: global.mediaViewer.volume,
playbackRate: global.mediaViewer.playbackRate,
isMuted: global.mediaViewer.isMuted,
},
isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global),
chats: reduceChats(global),

View File

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

View File

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