This commit is contained in:
281
react/features/always-on-top/AlwaysOnTop.tsx
Normal file
281
react/features/always-on-top/AlwaysOnTop.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// We need to reference these files directly to avoid loading things that are not available
|
||||
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
|
||||
import StatelessAvatar from '../base/avatar/components/web/StatelessAvatar';
|
||||
import { getAvatarColor, getInitials } from '../base/avatar/functions';
|
||||
import { DEFAULT_ICON } from '../base/icons/svg/constants';
|
||||
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* The timeout in ms for hiding the toolbar.
|
||||
*/
|
||||
const TOOLBAR_TIMEOUT = 4000;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AlwaysOnTop}.
|
||||
*/
|
||||
interface IState {
|
||||
avatarURL: string;
|
||||
customAvatarBackgrounds: Array<string>;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
isVideoDisplayed: boolean;
|
||||
userID: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the always on top page.
|
||||
*
|
||||
* @class AlwaysOnTop
|
||||
* @augments Component
|
||||
*/
|
||||
export default class AlwaysOnTop extends Component<any, IState> {
|
||||
_hovered: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AlwaysOnTop} instance.
|
||||
*
|
||||
* @param {*} props - The read-only properties with which the new instance
|
||||
* is to be initialized.
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
avatarURL: '',
|
||||
customAvatarBackgrounds: [],
|
||||
displayName: '',
|
||||
formattedDisplayName: '',
|
||||
isVideoDisplayed: true,
|
||||
userID: '',
|
||||
visible: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._avatarChangedListener = this._avatarChangedListener.bind(this);
|
||||
this._displayNameChangedListener
|
||||
= this._displayNameChangedListener.bind(this);
|
||||
this._videoChangedListener
|
||||
= this._videoChangedListener.bind(this);
|
||||
this._mouseMove = this._mouseMove.bind(this);
|
||||
this._onMouseOut = this._onMouseOut.bind(this);
|
||||
this._onMouseOver = this._onMouseOver.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles avatar changed api events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_avatarChangedListener({ avatarURL, id }: { avatarURL: string; id: string; }) {
|
||||
if (api._getOnStageParticipant() === id
|
||||
&& avatarURL !== this.state.avatarURL) {
|
||||
this.setState({ avatarURL });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles display name changed api events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayNameChangedListener({ displayname, formattedDisplayName, id }: { displayname: string;
|
||||
formattedDisplayName: string; id: string; }) {
|
||||
if (api._getOnStageParticipant() === id
|
||||
&& (formattedDisplayName !== this.state.formattedDisplayName
|
||||
|| displayname !== this.state.displayName)) {
|
||||
// I think the API has a typo using lowercase n for the displayname
|
||||
this.setState({
|
||||
displayName: displayname,
|
||||
formattedDisplayName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the toolbar after a timeout.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_hideToolbarAfterTimeout() {
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this._hovered) {
|
||||
this._hideToolbarAfterTimeout();
|
||||
} else {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
},
|
||||
TOOLBAR_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles large video changed api events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_videoChangedListener() {
|
||||
const userID = api._getOnStageParticipant();
|
||||
const avatarURL = api.getAvatarURL(userID);
|
||||
const displayName = api.getDisplayName(userID);
|
||||
const formattedDisplayName = api._getFormattedDisplayName(userID);
|
||||
const isVideoDisplayed = Boolean(api._getPrejoinVideo?.() || api._getLargeVideo());
|
||||
|
||||
this.setState({
|
||||
avatarURL,
|
||||
displayName,
|
||||
formattedDisplayName,
|
||||
isVideoDisplayed,
|
||||
userID
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse move events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_mouseMove() {
|
||||
this.state.visible || this.setState({ visible: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar mouse out handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseOut() {
|
||||
this._hovered = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar mouse over handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseOver() {
|
||||
this._hovered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders display name and avatar for the on stage participant.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoNotAvailableScreen() {
|
||||
const {
|
||||
avatarURL,
|
||||
customAvatarBackgrounds,
|
||||
displayName,
|
||||
formattedDisplayName,
|
||||
isVideoDisplayed
|
||||
} = this.state;
|
||||
|
||||
if (isVideoDisplayed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id = 'videoNotAvailableScreen'>
|
||||
<div id = 'avatarContainer'>
|
||||
<StatelessAvatar
|
||||
color = { getAvatarColor(displayName, customAvatarBackgrounds) }
|
||||
iconUser = { DEFAULT_ICON.IconUser }
|
||||
id = 'avatar'
|
||||
initials = { getInitials(displayName) }
|
||||
url = { avatarURL } />)
|
||||
</div>
|
||||
<div
|
||||
className = 'displayname'
|
||||
id = 'displayname'>
|
||||
{ formattedDisplayName }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets mouse move listener and initial toolbar timeout.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
api.on('avatarChanged', this._avatarChangedListener);
|
||||
api.on('displayNameChange', this._displayNameChangedListener);
|
||||
api.on('largeVideoChanged', this._videoChangedListener);
|
||||
api.on('prejoinVideoChanged', this._videoChangedListener);
|
||||
api.on('videoConferenceJoined', this._videoChangedListener);
|
||||
|
||||
this._videoChangedListener();
|
||||
|
||||
window.addEventListener('mousemove', this._mouseMove);
|
||||
|
||||
this._hideToolbarAfterTimeout();
|
||||
api.getCustomAvatarBackgrounds()
|
||||
.then((res: { avatarBackgrounds?: string[]; }) =>
|
||||
this.setState({
|
||||
customAvatarBackgrounds: res.avatarBackgrounds || []
|
||||
}))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout to hide the toolbar when the toolbar is shown.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidUpdate(_prevProps: any, prevState: IState) {
|
||||
if (!prevState.visible && this.state.visible) {
|
||||
this._hideToolbarAfterTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
api.removeListener('avatarChanged', this._avatarChangedListener);
|
||||
api.removeListener(
|
||||
'displayNameChange',
|
||||
this._displayNameChangedListener);
|
||||
api.removeListener(
|
||||
'largeVideoChanged',
|
||||
this._videoChangedListener);
|
||||
api.removeListener(
|
||||
'prejoinVideoChanged',
|
||||
this._videoChangedListener);
|
||||
api.removeListener(
|
||||
'videoConferenceJoined',
|
||||
this._videoChangedListener);
|
||||
|
||||
window.removeEventListener('mousemove', this._mouseMove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<div id = 'alwaysOnTop'>
|
||||
<Toolbar
|
||||
className = { this.state.visible ? 'fadeIn' : 'fadeOut' }
|
||||
onMouseOut = { this._onMouseOut }
|
||||
onMouseOver = { this._onMouseOver } />
|
||||
{ this._renderVideoNotAvailableScreen() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
180
react/features/always-on-top/AudioMuteButton.tsx
Normal file
180
react/features/always-on-top/AudioMuteButton.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// We need to reference these files directly to avoid loading things that are not available
|
||||
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
|
||||
|
||||
import { DEFAULT_ICON } from '../base/icons/svg/constants';
|
||||
import { IProps } from '../base/toolbox/components/AbstractButton';
|
||||
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AudioMuteButton}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Whether audio is available is not.
|
||||
*/
|
||||
audioAvailable: boolean;
|
||||
|
||||
/**
|
||||
* Whether audio is muted or not.
|
||||
*/
|
||||
audioMuted: boolean;
|
||||
}
|
||||
|
||||
type Props = Partial<IProps>;
|
||||
|
||||
/**
|
||||
* Stateless "mute/unmute audio" button for the Always-on-Top windows.
|
||||
*/
|
||||
export default class AudioMuteButton extends Component<Props, IState> {
|
||||
icon = DEFAULT_ICON.IconMic;
|
||||
toggledIcon = DEFAULT_ICON.IconMicSlash;
|
||||
accessibilityLabel = 'Audio mute';
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioMuteButton} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code AudioMuteButton} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioAvailable: false,
|
||||
audioMuted: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._audioAvailabilityListener
|
||||
= this._audioAvailabilityListener.bind(this);
|
||||
this._audioMutedListener = this._audioMutedListener.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets mouse move listener and initial toolbar timeout.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
api.on('audioAvailabilityChanged', this._audioAvailabilityListener);
|
||||
api.on('audioMuteStatusChanged', this._audioMutedListener);
|
||||
|
||||
Promise.all([
|
||||
api.isAudioAvailable(),
|
||||
api.isAudioMuted(),
|
||||
api.isAudioDisabled?.() || Promise.resolve(false)
|
||||
])
|
||||
.then(([ audioAvailable, audioMuted, audioDisabled ]) =>
|
||||
this.setState({
|
||||
audioAvailable: audioAvailable && !audioDisabled,
|
||||
audioMuted
|
||||
}))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
api.removeListener(
|
||||
'audioAvailabilityChanged',
|
||||
this._audioAvailabilityListener);
|
||||
api.removeListener(
|
||||
'audioMuteStatusChanged',
|
||||
this._audioMutedListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles audio available api events.
|
||||
*
|
||||
* @param {{ available: boolean }} status - The new available status.
|
||||
* @returns {void}
|
||||
*/
|
||||
_audioAvailabilityListener({ available }: { available: boolean; }) {
|
||||
this.setState({ audioAvailable: available });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles audio muted api events.
|
||||
*
|
||||
* @param {{ muted: boolean }} status - The new muted status.
|
||||
* @returns {void}
|
||||
*/
|
||||
_audioMutedListener({ muted }: { muted: boolean; }) {
|
||||
this.setState({ audioMuted: muted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if audio is currently muted or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isAudioMuted() {
|
||||
return this.state.audioMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is disabled or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return !this.state.audioAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the muted state.
|
||||
*
|
||||
* @override
|
||||
* @param {boolean} _audioMuted - Whether audio should be muted or not.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_setAudioMuted(_audioMuted: boolean) {
|
||||
this.state.audioAvailable && api.executeCommand('toggleAudio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and toggles the audio mute state
|
||||
* accordingly.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._setAudioMuted(!this._isAudioMuted());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const toggled = this._isAudioMuted();
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
accessibilityLabel = { this.accessibilityLabel }
|
||||
disabled = { this._isDisabled() }
|
||||
icon = { toggled ? this.toggledIcon : this.icon }
|
||||
onClick = { this._onClick }
|
||||
toggled = { toggled } />
|
||||
);
|
||||
}
|
||||
}
|
||||
60
react/features/always-on-top/HangupButton.tsx
Normal file
60
react/features/always-on-top/HangupButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// We need to reference these files directly to avoid loading things that are not available
|
||||
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
|
||||
import { DEFAULT_ICON } from '../base/icons/svg/constants';
|
||||
import { IProps } from '../base/toolbox/components/AbstractButton';
|
||||
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
type Props = Partial<IProps>;
|
||||
|
||||
/**
|
||||
* Stateless hangup button for the Always-on-Top windows.
|
||||
*/
|
||||
export default class HangupButton extends Component<Props> {
|
||||
|
||||
accessibilityLabel = 'Hangup';
|
||||
icon = DEFAULT_ICON.IconHangup;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code HangupButton} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code HangupButton} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and disconnects the conference.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
api.executeCommand('hangup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<ToolbarButton
|
||||
accessibilityLabel = { this.accessibilityLabel }
|
||||
customClass = 'hangup-button'
|
||||
icon = { this.icon }
|
||||
onClick = { this._onClick } />
|
||||
);
|
||||
}
|
||||
}
|
||||
137
react/features/always-on-top/Toolbar.tsx
Normal file
137
react/features/always-on-top/Toolbar.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioMuteButton from './AudioMuteButton';
|
||||
import HangupButton from './HangupButton';
|
||||
import VideoMuteButton from './VideoMuteButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Toolbar}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Additional CSS class names to add to the root of the toolbar.
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* Callback invoked when no longer moused over the toolbar.
|
||||
*/
|
||||
onMouseOut: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when the mouse has moved over the toolbar.
|
||||
*/
|
||||
onMouseOver: (e?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link Toolbar}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Whether audio button to be shown or not.
|
||||
*/
|
||||
showAudioButton: boolean;
|
||||
|
||||
/**
|
||||
* Whether video button to be shown or not.
|
||||
*/
|
||||
showVideoButton: boolean;
|
||||
}
|
||||
|
||||
type Props = Partial<IProps>;
|
||||
|
||||
/**
|
||||
* Represents the toolbar in the Always On Top window.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export default class Toolbar extends Component<Props, IState> {
|
||||
/**
|
||||
* Initializes a new {@code Toolbar} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize the new {@code Toolbar} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showAudioButton: true,
|
||||
showVideoButton: true
|
||||
};
|
||||
|
||||
this._videoConferenceJoinedListener = this._videoConferenceJoinedListener.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets listens for changing meetings while showing the toolbar.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
api.on('videoConferenceJoined', this._videoConferenceJoinedListener);
|
||||
|
||||
this._videoConferenceJoinedListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles is visitor changes.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_videoConferenceJoinedListener() {
|
||||
// for electron clients that embed the api and are not updated
|
||||
if (!api.isVisitor) {
|
||||
console.warn('external API not updated');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isNotVisitor = !api.isVisitor();
|
||||
|
||||
this.setState({
|
||||
showAudioButton: isNotVisitor,
|
||||
showVideoButton: isNotVisitor
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
api.removeListener('videoConferenceJoined', this._videoConferenceJoinedListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
className = '',
|
||||
onMouseOut,
|
||||
onMouseOver
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `toolbox-content-items always-on-top-toolbox ${className}` }
|
||||
onMouseOut = { onMouseOut }
|
||||
onMouseOver = { onMouseOver }>
|
||||
{ this.state.showAudioButton && <AudioMuteButton /> }
|
||||
{ this.state.showVideoButton && <VideoMuteButton /> }
|
||||
<HangupButton customClass = 'hangup-button' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
69
react/features/always-on-top/ToolbarButton.tsx
Normal file
69
react/features/always-on-top/ToolbarButton.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import Icon from '../base/icons/components/Icon';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Accessibility label for button.
|
||||
*/
|
||||
accessibilityLabel: string;
|
||||
|
||||
/**
|
||||
* An extra class name to be added at the end of the element's class name
|
||||
* in order to enable custom styling.
|
||||
*/
|
||||
customClass?: string;
|
||||
|
||||
/**
|
||||
* Whether or not the button is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* Button icon.
|
||||
*/
|
||||
icon: Function;
|
||||
|
||||
/**
|
||||
* Click handler.
|
||||
*/
|
||||
onClick: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Whether or not the button is toggled.
|
||||
*/
|
||||
toggled?: boolean;
|
||||
}
|
||||
|
||||
const ToolbarButton = ({
|
||||
accessibilityLabel,
|
||||
customClass,
|
||||
disabled = false,
|
||||
onClick,
|
||||
icon,
|
||||
toggled = false
|
||||
}: IProps) => {
|
||||
const onKeyPress = useCallback(event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}, [ onClick ]);
|
||||
|
||||
return (<div
|
||||
aria-disabled = { disabled }
|
||||
aria-label = { accessibilityLabel }
|
||||
aria-pressed = { toggled }
|
||||
className = { `toolbox-button ${disabled ? ' disabled' : ''}` }
|
||||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<div className = { `toolbox-icon ${disabled ? 'disabled' : ''} ${customClass ?? ''}` }>
|
||||
<Icon src = { icon } />
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
||||
180
react/features/always-on-top/VideoMuteButton.tsx
Normal file
180
react/features/always-on-top/VideoMuteButton.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// We need to reference these files directly to avoid loading things that are not available
|
||||
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
|
||||
import { DEFAULT_ICON } from '../base/icons/svg/constants';
|
||||
import { IProps } from '../base/toolbox/components/AbstractButton';
|
||||
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
type Props = Partial<IProps>;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VideoMuteButton}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether video is available is not.
|
||||
*/
|
||||
videoAvailable: boolean;
|
||||
|
||||
/**
|
||||
* Whether video is muted or not.
|
||||
*/
|
||||
videoMuted: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stateless "mute/unmute video" button for the Always-on-Top windows.
|
||||
*/
|
||||
export default class VideoMuteButton extends Component<Props, State> {
|
||||
|
||||
icon = DEFAULT_ICON.IconVideo;
|
||||
toggledIcon = DEFAULT_ICON.IconVideoOff;
|
||||
accessibilityLabel = 'Video mute';
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoMuteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code VideoMuteButton} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
videoAvailable: false,
|
||||
videoMuted: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._videoAvailabilityListener
|
||||
= this._videoAvailabilityListener.bind(this);
|
||||
this._videoMutedListener = this._videoMutedListener.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets mouse move listener and initial toolbar timeout.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
api.on('videoAvailabilityChanged', this._videoAvailabilityListener);
|
||||
api.on('videoMuteStatusChanged', this._videoMutedListener);
|
||||
|
||||
Promise.all([
|
||||
api.isVideoAvailable(),
|
||||
api.isVideoMuted()
|
||||
])
|
||||
.then(([ videoAvailable, videoMuted ]) =>
|
||||
this.setState({
|
||||
videoAvailable,
|
||||
videoMuted
|
||||
}))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
api.removeListener(
|
||||
'videoAvailabilityChanged',
|
||||
this._videoAvailabilityListener);
|
||||
api.removeListener(
|
||||
'videoMuteStatusChanged',
|
||||
this._videoMutedListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is disabled or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return !this.state.videoAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if video is currently muted or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isVideoMuted() {
|
||||
return this.state.videoMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the muted state.
|
||||
*
|
||||
* @override
|
||||
* @param {boolean} _videoMuted - Whether video should be muted or not.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_setVideoMuted(_videoMuted: boolean) {
|
||||
this.state.videoAvailable && api.executeCommand('toggleVideo', false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles video available api events.
|
||||
*
|
||||
* @param {{ available: boolean }} status - The new available status.
|
||||
* @returns {void}
|
||||
*/
|
||||
_videoAvailabilityListener({ available }: { available: boolean; }) {
|
||||
this.setState({ videoAvailable: available });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles video muted api events.
|
||||
*
|
||||
* @param {{ muted: boolean }} status - The new muted status.
|
||||
* @returns {void}
|
||||
*/
|
||||
_videoMutedListener({ muted }: { muted: boolean; }) {
|
||||
this.setState({ videoMuted: muted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and toggles the video mute state
|
||||
* accordingly.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._setVideoMuted(!this._isVideoMuted());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const toggled = this._isVideoMuted();
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
accessibilityLabel = { this.accessibilityLabel }
|
||||
disabled = { this._isDisabled() }
|
||||
icon = { toggled ? this.toggledIcon : this.icon }
|
||||
onClick = { this._onClick }
|
||||
toggled = { toggled } />
|
||||
);
|
||||
}
|
||||
}
|
||||
13
react/features/always-on-top/index.tsx
Normal file
13
react/features/always-on-top/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import AlwaysOnTop from './AlwaysOnTop';
|
||||
|
||||
// Render the main/root Component.
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
ReactDOM.render(<AlwaysOnTop />, document.getElementById('react'));
|
||||
|
||||
window.addEventListener(
|
||||
'beforeunload',
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
() => ReactDOM.unmountComponentAtNode(document.getElementById('react') ?? document.body));
|
||||
Reference in New Issue
Block a user