[RN] Add remote video menu

This commit is contained in:
Bettenbuk Zoltan
2018-12-19 19:40:17 +01:00
committed by Zoltan Bettenbuk
parent d4c0840659
commit 6b68fba220
27 changed files with 582 additions and 65 deletions

View File

@@ -0,0 +1,105 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { kickParticipant } from '../../../base/participants';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* The type of the React {@code Component} state of {@link KickButton}.
*/
type Props = {
/**
* Invoked to signal the participant with the passed in participantID
* should be removed from the conference.
*/
dispatch: Dispatch<*>,
/**
* Callback to invoke when {@code KickButton} is clicked.
*/
onClick: Function,
/**
* The ID of the participant linked to the onClick callback.
*/
participantID: string,
/**
* Invoked to obtain translated strings.
*/
t: Function,
};
/**
* Implements a React {@link Component} which displays a button for kicking out
* a participant from the conference.
*
* @extends Component
*/
class KickButton extends Component<Props> {
/**
* Initializes a new {@code KickButton} instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
return (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.kick') }
iconClass = 'icon-kick'
id = { `ejectlink_${participantID}` }
onClick = { this._onClick } />
);
}
_onClick: () => void;
/**
* Remove the participant with associated participantID from the conference.
*
* @private
* @returns {void}
*/
_onClick() {
const { dispatch, onClick, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'kick.button',
{
'participant_id': participantID
}));
dispatch(kickParticipant(participantID));
if (onClick) {
onClick();
}
}
}
export default translate(connect()(KickButton));

View File

@@ -0,0 +1,120 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { openDialog } from '../../../base/dialog';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
import MuteRemoteParticipantDialog from './MuteRemoteParticipantDialog';
/**
* The type of the React {@code Component} props of {@link MuteButton}.
*/
type Props = {
/**
* Invoked to send a request for muting the participant with the passed
* in participantID.
*/
dispatch: Dispatch<*>,
/**
* Whether or not the participant is currently audio muted.
*/
isAudioMuted: Function,
/**
* Callback to invoke when {@code MuteButton} is clicked.
*/
onClick: Function,
/**
* The ID of the participant linked to the onClick callback.
*/
participantID: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Implements a React {@link Component} which displays a button for audio muting
* a participant in the conference.
*
* @extends Component
*/
class MuteButton extends Component<Props> {
/**
* Initializes a new {@code MuteButton} instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { isAudioMuted, participantID, t } = this.props;
const muteConfig = isAudioMuted ? {
translationKey: 'videothumbnail.muted',
muteClassName: 'mutelink disabled'
} : {
translationKey: 'videothumbnail.domute',
muteClassName: 'mutelink'
};
return (
<RemoteVideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
iconClass = 'icon-mic-disabled'
id = { `mutelink_${participantID}` }
onClick = { this._onClick } />
);
}
_onClick: () => void;
/**
* Dispatches a request to mute the participant with the passed in
* participantID.
*
* @private
* @returns {void}
*/
_onClick() {
const { dispatch, onClick, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
if (onClick) {
onClick();
}
}
}
export default translate(connect()(MuteButton));

View File

@@ -0,0 +1,116 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import {
createRemoteMuteConfirmedEvent,
sendAnalytics
} from '../../../analytics';
import { muteRemoteParticipant } from '../../../base/participants';
/**
* The type of the React {@code Component} props of
* {@link MuteRemoteParticipantDialog}.
*/
type Props = {
/**
* Invoked to send a request for muting the participant with the passed
* in participantID.
*/
dispatch: Dispatch<*>,
/**
* The ID of the participant linked to the onClick callback.
*/
participantID: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting a remote participant.
*
* @extends Component
*/
class MuteRemoteParticipantDialog extends Component<Props> {
/**
* Initializes a new {@code MuteRemoteParticipantDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
this._renderContent = this._renderContent.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okTitleKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantTitle'
width = 'small'>
{ this._renderContent() }
</Dialog>
);
}
_onSubmit: () => void;
/**
* Handles the submit button action.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteMuteConfirmedEvent(participantID));
dispatch(muteRemoteParticipant(participantID));
return true;
}
_renderContent: () => React$Element<*>;
/**
* Renders the content of the dialog.
*
* @private
* @returns {Component} The React {@code Component} which is the view of the
* dialog content.
*/
_renderContent() {
const { t } = this.props;
return (
<div>
{ t('dialog.muteParticipantBody') }
</div>
);
}
}
export default translate(connect()(MuteRemoteParticipantDialog));

View File

@@ -0,0 +1,146 @@
/* @flow */
import React, { Component } from 'react';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
// TODO: Move these enums into the store after further reactification of the
// non-react RemoteVideo component.
export const REMOTE_CONTROL_MENU_STATES = {
NOT_SUPPORTED: 0,
NOT_STARTED: 1,
REQUESTING: 2,
STARTED: 3
};
/**
* The type of the React {@code Component} props of {@link RemoteControlButton}.
*/
type Props = {
/**
* The callback to invoke when the component is clicked.
*/
onClick: Function,
/**
* The ID of the participant linked to the onClick callback.
*/
participantID: string,
/**
* The current status of remote control. Should be a number listed in the
* enum REMOTE_CONTROL_MENU_STATES.
*/
remoteControlState: number,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Implements a React {@link Component} which displays a button showing the
* current state of remote control for a participant and can start or stop a
* remote control session.
*
* @extends Component
*/
class RemoteControlButton extends Component<Props> {
/**
* Initializes a new {@code RemoteControlButton} instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {null|ReactElement}
*/
render() {
const {
participantID,
remoteControlState,
t
} = this.props;
let className, icon;
switch (remoteControlState) {
case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
icon = 'icon-play';
break;
case REMOTE_CONTROL_MENU_STATES.REQUESTING:
className = ' disabled';
icon = 'icon-play';
break;
case REMOTE_CONTROL_MENU_STATES.STARTED:
icon = 'icon-stop';
break;
case REMOTE_CONTROL_MENU_STATES.NOT_SUPPORTED:
// Intentionally fall through.
default:
return null;
}
return (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.remoteControl') }
displayClass = { className }
iconClass = { icon }
id = { `remoteControl_${participantID}` }
onClick = { this._onClick } />
);
}
_onClick: () => void;
/**
* Sends analytics event for pressing the button and executes the passed
* onClick handler.
*
* @private
* @returns {void}
*/
_onClick() {
const { onClick, participantID, remoteControlState } = this.props;
// TODO: What do we do in case the state is e.g. "requesting"?
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED
|| remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
const enable
= remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'remote.control.button',
{
enable,
'participant_id': participantID
}));
}
if (onClick) {
onClick();
}
}
}
export default translate(RemoteControlButton);

View File

@@ -0,0 +1,44 @@
/* @flow */
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link RemoteVideoMenu}.
*/
type Props = {
/**
* The components to place as the body of the {@code RemoteVideoMenu}.
*/
children: React$Node,
/**
* The id attribute to be added to the component's DOM for retrieval when
* querying the DOM. Not used directly by the component.
*/
id: string
};
/**
* React {@code Component} responsible for displaying other components as a menu
* for manipulating remote participant state.
*
* @extends {Component}
*/
export default class RemoteVideoMenu extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ul
className = 'popupmenu'
id = { this.props.id }>
{ this.props.children }
</ul>
);
}
}

View File

@@ -0,0 +1,77 @@
/* @flow */
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of
* {@link RemoteVideoMenuButton}.
*/
type Props = {
/**
* Text to display within the component that describes the onClick action.
*/
buttonText: string,
/**
* Additional CSS classes to add to the component.
*/
displayClass?: string,
/**
* The CSS classes for the icon that will display within the component.
*/
iconClass: string,
/**
* The id attribute to be added to the component's DOM for retrieval when
* querying the DOM. Not used directly by the component.
*/
id: string,
/**
* Callback to invoke when the component is clicked.
*/
onClick: Function,
};
/**
* React {@code Component} for displaying an action in {@code RemoteVideoMenu}.
*
* @extends {Component}
*/
export default class RemoteVideoMenuButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
buttonText,
displayClass,
iconClass,
id,
onClick
} = this.props;
const linkClassName = `popupmenu__link ${displayClass || ''}`;
return (
<li className = 'popupmenu__item'>
<a
className = { linkClassName }
id = { id }
onClick = { onClick }>
<span className = 'popupmenu__icon'>
<i className = { iconClass } />
</span>
<span className = 'popupmenu__text'>
{ buttonText }
</span>
</a>
</li>
);
}
}

View File

@@ -0,0 +1,210 @@
// @flow
import React, { Component } from 'react';
import { Popover } from '../../../base/popover';
import {
MuteButton,
KickButton,
RemoteControlButton,
RemoteVideoMenu,
VolumeSlider
} from './';
declare var $: Object;
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of
* {@link RemoteVideoMenuTriggerButton}.
*/
type Props = {
/**
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
*/
initialVolumeValue: number,
/**
* Whether or not the participant is currently muted.
*/
isAudioMuted: boolean,
/**
* Whether or not the participant is a conference moderator.
*/
isModerator: boolean,
/**
* Callback to invoke when the popover has been displayed.
*/
onMenuDisplay: Function,
/**
* Callback to invoke choosing to start a remote control session with
* the participant.
*/
onRemoteControlToggle: Function,
/**
* Callback to invoke when changing the level of the participant's
* audio element.
*/
onVolumeChange: Function,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
* {@code InlineDialog}.
*/
menuPosition: string,
/**
* The ID for the participant on which the remote video menu will act.
*/
participantID: string,
/**
* The current state of the participant's remote control session.
*/
remoteControlState: number
};
/**
* React {@code Component} for displaying an icon associated with opening the
* the {@code RemoteVideoMenu}.
*
* @extends {Component}
*/
class RemoteVideoMenuTriggerButton extends Component<Props> {
/**
* The internal reference to topmost DOM/HTML element backing the React
* {@code Component}. Accessed directly for associating an element as
* the trigger for a popover.
*
* @private
* @type {HTMLDivElement}
*/
_rootElement = null;
/**
* Initializes a new {#@code RemoteVideoMenuTriggerButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onShowRemoteMenu = this._onShowRemoteMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const content = this._renderRemoteVideoMenu();
if (!content) {
return null;
}
return (
<Popover
content = { content }
onPopoverOpen = { this._onShowRemoteMenu }
position = { this.props.menuPosition }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
<i
className = 'icon-thumb-menu'
title = 'Remote user controls' />
</span>
</Popover>
);
}
_onShowRemoteMenu: () => void;
/**
* Opens the {@code RemoteVideoMenu}.
*
* @private
* @returns {void}
*/
_onShowRemoteMenu() {
this.props.onMenuDisplay();
}
/**
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
* the remote participant.
*
* @private
* @returns {ReactElement}
*/
_renderRemoteVideoMenu() {
const {
initialVolumeValue,
isAudioMuted,
isModerator,
onRemoteControlToggle,
onVolumeChange,
remoteControlState,
participantID
} = this.props;
const buttons = [];
if (isModerator) {
buttons.push(
<MuteButton
isAudioMuted = { isAudioMuted }
key = 'mute'
participantID = { participantID } />
);
buttons.push(
<KickButton
key = 'kick'
participantID = { participantID } />
);
}
if (remoteControlState) {
buttons.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { remoteControlState } />
);
}
if (onVolumeChange) {
buttons.push(
<VolumeSlider
initialValue = { initialVolumeValue }
key = 'volume-slider'
onChange = { onVolumeChange } />
);
}
if (buttons.length > 0) {
return (
<RemoteVideoMenu id = { participantID }>
{ buttons }
</RemoteVideoMenu>
);
}
return null;
}
}
export default RemoteVideoMenuTriggerButton;

View File

@@ -0,0 +1,111 @@
/* @flow */
import React, { Component } from 'react';
/**
* Used to modify initialValue, which is expected to be a decimal value between
* 0 and 1, and converts it to a number representable by an input slider, which
* recognizes whole numbers.
*/
const VOLUME_SLIDER_SCALE = 100;
/**
* The type of the React {@code Component} props of {@link VolumeSlider}.
*/
type Props = {
/**
* The value of the audio slider should display at when the component first
* mounts. Changes will be stored in state. The value should be a number
* between 0 and 1.
*/
initialValue: number,
/**
* The callback to invoke when the audio slider value changes.
*/
onChange: Function
};
/**
* The type of the React {@code Component} state of {@link VolumeSlider}.
*/
type State = {
/**
* The volume of the participant's audio element. The value will
* be represented by a slider.
*/
volumeLevel: number
};
/**
* Implements a React {@link Component} which displays an input slider for
* adjusting the local volume of a remote participant.
*
* @extends Component
*/
class VolumeSlider extends Component<Props, State> {
/**
* Initializes a new {@code VolumeSlider} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
volumeLevel: (props.initialValue || 0) * VOLUME_SLIDER_SCALE
};
// Bind event handlers so they are only bound once for every instance.
this._onVolumeChange = this._onVolumeChange.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<li className = 'popupmenu__item'>
<div className = 'popupmenu__contents'>
<span className = 'popupmenu__icon'>
<i className = 'icon-volume' />
</span>
<div className = 'popupmenu__slider_container'>
<input
className = 'popupmenu__slider'
max = { VOLUME_SLIDER_SCALE }
min = { 0 }
onChange = { this._onVolumeChange }
type = 'range'
value = { this.state.volumeLevel } />
</div>
</div>
</li>
);
}
_onVolumeChange: (Object) => void;
/**
* Sets the internal state of the volume level for the volume slider.
* Invokes the prop onVolumeChange to notify of volume changes.
*
* @param {Object} event - DOM Event for slider change.
* @private
* @returns {void}
*/
_onVolumeChange(event) {
const volumeLevel = event.currentTarget.value;
this.props.onChange(volumeLevel / VOLUME_SLIDER_SCALE);
this.setState({ volumeLevel });
}
}
export default VolumeSlider;

View File

@@ -0,0 +1,13 @@
// @flow
export { default as KickButton } from './KickButton';
export { default as MuteButton } from './MuteButton';
export {
REMOTE_CONTROL_MENU_STATES,
default as RemoteControlButton
} from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
export {
default as RemoteVideoMenuTriggerButton
} from './RemoteVideoMenuTriggerButton';
export { default as VolumeSlider } from './VolumeSlider';