Remote video menu post-PR improvements

This commit is contained in:
Bettenbuk Zoltan 2019-01-05 17:49:21 +01:00 committed by Saúl Ibarra Corretgé
parent 82f6931ee8
commit 5c0ae10ccb
23 changed files with 531 additions and 544 deletions

View File

@ -93,6 +93,7 @@
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
"invite": "Invite people",
"kick": "Kick participant",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle room lock",
"moreActions": "Toggle more actions menu",
@ -102,6 +103,7 @@
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing",
"shareRoom": "Invite someone",
@ -385,7 +387,9 @@
"externalInstallationMsg": "You need to install our desktop sharing extension.",
"inlineInstallationMsg": "You need to install our desktop sharing extension.",
"inlineInstallExtension": "Install now",
"kickParticipantButton": "Kick",
"kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this member?",
"muteParticipantTitle": "Mute this member?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",

View File

@ -10,6 +10,16 @@ const BORDER_RADIUS = 5;
const DIALOG_BORDER_COLOR = 'rgba(255, 255, 255, 0.2)';
export const FIELD_UNDERLINE = ColorPalette.transparent;
/**
* NOTE: These Material guidelines based values are currently only used in
* dialogs (and related) but later on it would be nice to export it into a base
* Material feature.
*/
export const MD_FONT_SIZE = 16;
export const MD_ITEM_HEIGHT = 48;
export const MD_ITEM_MARGIN_PADDING = 16;
export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
/**
@ -25,7 +35,7 @@ const bottomSheetItemStyles = createStyleSheet({
style: {
alignItems: 'center',
flexDirection: 'row',
height: 48
height: MD_ITEM_HEIGHT
},
/**
@ -42,7 +52,7 @@ const bottomSheetItemStyles = createStyleSheet({
labelStyle: {
color: ColorPalette.white,
flexShrink: 1,
fontSize: 16,
fontSize: MD_FONT_SIZE,
marginLeft: 32,
opacity: 0.90
}
@ -92,7 +102,7 @@ export const bottomSheetStyles = createStyleSheet({
sheet: {
backgroundColor: 'rgb(0, 3, 6)',
flex: 1,
paddingHorizontal: 16,
paddingHorizontal: MD_ITEM_MARGIN_PADDING,
paddingVertical: 8
}
});
@ -134,7 +144,7 @@ export const brandedDialog = createStyleSheet({
closeStyle: {
color: ColorPalette.white,
fontSize: 16
fontSize: MD_FONT_SIZE
},
closeWrapper: {
@ -173,7 +183,7 @@ export const brandedDialog = createStyleSheet({
text: {
color: ColorPalette.white,
fontSize: 16,
fontSize: MD_FONT_SIZE,
textAlign: 'center'
}
});

View File

@ -31,12 +31,6 @@ export type Props = {
*/
onClick?: ?Function,
/**
* The event handler/listener to be invoked when this
* {@code AbstractContainer} is long pressed on React Native.
*/
onLongPress?: ?Function,
/**
* The style (as in stylesheet) to be applied to this
* {@code AbstractContainer}.

View File

@ -8,7 +8,16 @@ import {
} from 'react-native';
import AbstractContainer from '../AbstractContainer';
import type { Props } from '../AbstractContainer';
import type { Props as AbstractProps } from '../AbstractContainer';
type Props = AbstractProps & {
/**
* The event handler/listener to be invoked when this
* {@code AbstractContainer} is long pressed on React Native.
*/
onLongPress?: ?Function,
};
/**
* Represents a container of React Native/mobile {@link Component} children.
@ -28,7 +37,7 @@ export default class Container<P: Props> extends AbstractContainer<P> {
accessible,
onClick,
onLongPress,
touchFeedback = onClick,
touchFeedback = Boolean(onClick || onLongPress),
underlayColor,
visible = true,
...props
@ -50,19 +59,24 @@ export default class Container<P: Props> extends AbstractContainer<P> {
// onClick & touchFeedback
if (element && onClickOrTouchFeedback) {
const touchableProps = {
accessibilityLabel,
accessible,
onLongPress,
onPress: onClick
};
element
= React.createElement(
touchFeedback
? TouchableHighlight
: TouchableWithoutFeedback,
{
accessibilityLabel,
accessible,
onLongPress,
onPress: onClick,
...touchFeedback && { underlayColor }
},
element);
= touchFeedback
? React.createElement(
TouchableHighlight,
{
...touchableProps,
underlayColor
},
element)
: React.createElement(
TouchableWithoutFeedback, touchableProps, element);
}
return element;

View File

@ -209,6 +209,22 @@ export function isLocalTrackMuted(tracks, mediaType) {
return !track || track.muted;
}
/**
* Returns true if the remote track of the given media type and the given
* participant is muted, false otherwise.
*
* @param {Track[]} tracks - List of all tracks.
* @param {MEDIA_TYPE} mediaType - The media type of tracks to be checked.
* @param {*} participantId - Participant ID.
* @returns {boolean}
*/
export function isRemoteTrackMuted(tracks, mediaType, participantId) {
const track = getTrackByMediaTypeAndParticipant(
tracks, mediaType, participantId);
return !track || track.muted;
}
/**
* Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of
* the specified {@code track} is already in accord with the specified

View File

@ -43,6 +43,16 @@ type Props = {
*/
_largeVideo: Object,
/**
* Handles click/tap event on the thumbnail.
*/
_onClick: ?Function,
/**
* Handles long press on the thumbnail.
*/
_onShowRemoteVideoMenu: ?Function,
/**
* The Redux representation of the participant's video track.
*/
@ -83,19 +93,6 @@ type Props = {
* @extends Component
*/
class Thumbnail extends Component<Props> {
/**
* Initializes new Video Thumbnail component.
*
* @param {Object} props - Component props.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
this._onShowRemoteVideoMenu = this._onShowRemoteVideoMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@ -107,6 +104,8 @@ class Thumbnail extends Component<Props> {
_audioTrack: audioTrack,
_isModerator,
_largeVideo: largeVideo,
_onClick,
_onShowRemoteVideoMenu,
_videoTrack: videoTrack,
disablePin,
disableTint,
@ -129,10 +128,10 @@ class Thumbnail extends Component<Props> {
return (
<Container
onClick = { disablePin ? undefined : this._onClick }
onClick = { disablePin ? undefined : _onClick }
onLongPress = {
showRemoteVideoMenu
? this._onShowRemoteVideoMenu : undefined }
? _onShowRemoteVideoMenu : undefined }
style = { [
styles.thumbnail,
participant.pinned && !disablePin
@ -169,43 +168,53 @@ class Thumbnail extends Component<Props> {
</Container>
);
}
}
_onClick: () => void;
/**
* Maps part of redux actions to component's props.
*
* @param {Function} dispatch - Redux's {@code dispatch} function.
* @param {Props} ownProps - The own props of the component.
* @returns {{
* _onClick: Function,
* _onShowRemoteVideoMenu: Function
* }}
*/
function _mapDispatchToProps(dispatch: Function, ownProps): Object {
return {
/**
* Handles click/tap event on the thumbnail.
*
* @protected
* @returns {void}
*/
_onClick() {
const { participant } = ownProps;
/**
* Handles click/tap event on the thumbnail.
*
* @returns {void}
*/
_onClick() {
const { dispatch, participant } = this.props;
dispatch(
pinParticipant(participant.pinned ? null : participant.id));
},
// TODO The following currently ignores interfaceConfig.filmStripOnly.
dispatch(pinParticipant(participant.pinned ? null : participant.id));
}
/**
* Handles long press on the thumbnail.
*
* @returns {void}
*/
_onShowRemoteVideoMenu() {
const { participant } = ownProps;
_onShowRemoteVideoMenu: () => void;
/**
* Handles long press on the thumbnail.
*
* @returns {void}
*/
_onShowRemoteVideoMenu() {
const { dispatch, participant } = this.props;
dispatch(openDialog(RemoteVideoMenu, {
participant
}));
}
dispatch(openDialog(RemoteVideoMenu, {
participant
}));
}
};
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @param {Props} ownProps - Properties of component.
* @returns {{
* _audioTrack: Track,
* _isModerator: boolean,
@ -233,4 +242,4 @@ function _mapStateToProps(state, ownProps) {
};
}
export default connect(_mapStateToProps)(Thumbnail);
export default connect(_mapStateToProps, _mapDispatchToProps)(Thumbnail);

View File

@ -1,5 +1,6 @@
// @flow
import { MD_ITEM_HEIGHT } from '../../../base/dialog';
import { ColorPalette, createStyleSheet } from '../../../base/styles';
/**
@ -16,7 +17,7 @@ export default createStyleSheet({
deviceRow: {
alignItems: 'center',
flexDirection: 'row',
height: 48
height: MD_ITEM_HEIGHT
},
/**

View File

@ -0,0 +1,46 @@
// @flow
import { openDialog } from '../../base/dialog';
import { AbstractButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { KickRemoteParticipantDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant that this button is supposed to kick.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which kicks the remote participant.
*/
export default class AbstractKickButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.kick';
iconName = 'icon-kick';
label = 'videothumbnail.kick';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
}
}

View File

@ -0,0 +1,66 @@
// @flow
import { Component } from 'react';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { kickParticipant } from '../../base/participants';
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the remote participant to be kicked.
*/
participantID: string,
/**
* Function to translate i18n labels.
*/
t: Function
};
/**
* Abstract dialog to confirm a remote participant kick action.
*/
export default class AbstractKickRemoteParticipantDialog
extends Component<Props> {
/**
* Initializes a new {@code AbstractKickRemoteParticipantDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
_onSubmit: () => boolean;
/**
* Callback for the confirm button.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'kick.button',
{
'participant_id': participantID
}));
dispatch(kickParticipant(participantID));
return true;
}
}

View File

@ -0,0 +1,105 @@
// @flow
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { openDialog } from '../../base/dialog';
import { MEDIA_TYPE } from '../../base/media';
import {
AbstractButton,
type AbstractButtonProps
} from '../../base/toolbox';
import { isRemoteTrackMuted } from '../../base/tracks';
import { MuteRemoteParticipantDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* Boolean to indicate if the audio track of the participant is muted or
* not.
*/
_audioTrackMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to
* mute/unmute.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes the remote participant.
*/
export default class AbstractMuteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.remoteMute';
iconName = 'icon-mic-disabled';
label = 'videothumbnail.domute';
toggledLabel = 'videothumbnail.muted';
/**
* Handles clicking / pressing the button, and mutes the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
}
/**
* Renders the item disabled if the participant is muted.
*
* @inheritdoc
*/
_isDisabled() {
return this.props._audioTrackMuted;
}
/**
* Renders the item toggled if the participant is muted.
*
* @inheritdoc
*/
_isToggled() {
return this.props._audioTrackMuted;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* _audioTrackMuted: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const tracks = state['features/base/tracks'];
return {
_audioTrackMuted: isRemoteTrackMuted(
tracks, MEDIA_TYPE.AUDIO, ownProps.participantID)
};
}

View File

@ -0,0 +1,70 @@
// @flow
import { Component } from 'react';
import {
createRemoteMuteConfirmedEvent,
sendAnalytics
} from '../../analytics';
import { muteRemoteParticipant } from '../../base/participants';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantDialog}.
*/
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the remote participant to be muted.
*/
participantID: string,
/**
* Function to translate i18n labels.
*/
t: Function
};
/**
* Abstract dialog to confirm a remote participant mute action.
*
* @extends Component
*/
export default class AbstractMuteRemoteParticipantDialog
extends Component<Props> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} 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);
}
_onSubmit: () => boolean;
/**
* 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;
}
}

View File

@ -2,45 +2,14 @@
import { connect } from 'react-redux';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { AbstractButton } from '../../../base/toolbox';
import type { AbstractButtonProps } from '../../../base/toolbox';
import KickRemoteParticipantDialog from './KickRemoteParticipantDialog';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The participant object that this button is supposed to kick.
*/
participant: Object
};
import AbstractKickButton from '../AbstractKickButton';
/**
* A remote video menu button which kicks the remote participant.
* We don't need any further implementation for this on mobile, but we keep it
* here for clarity and consistency with web. Once web uses the
* {@code AbstractButton} base class, we can remove all these and just use
* the {@code AbstractKickButton} as {@KickButton}.
*/
class KickButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.audioRoute';
iconName = 'icon-kick';
label = 'videothumbnail.kick';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participant } = this.props;
dispatch(openDialog(KickRemoteParticipantDialog, { participant }));
}
}
export default translate(connect()(KickButton));
export default translate(connect()(AbstractKickButton));

View File

@ -1,49 +1,18 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { kickParticipant } from '../../../base/participants';
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The remote participant to be kicked.
*/
participant: Object,
/**
* Function to translate i18n labels.
*/
t: Function
};
import AbstractKickRemoteParticipantDialog
from '../AbstractKickRemoteParticipantDialog';
/**
* Dialog to confirm a remote participant kick action.
*/
class KickRemoteParticipantDialog extends Component<Props> {
/**
* Initializes a new {@code KickRemoteParticipantDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
class KickRemoteParticipantDialog extends AbstractKickRemoteParticipantDialog {
/**
* Implements React's {@link Component#render()}.
*
@ -59,26 +28,6 @@ class KickRemoteParticipantDialog extends Component<Props> {
}
_onSubmit: () => boolean;
/**
* Callback for the confirm button.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participant } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'kick.button',
{
'participant_id': participant.id
}));
dispatch(kickParticipant(participant.id));
return true;
}
}
export default translate(connect()(KickRemoteParticipantDialog));

View File

@ -2,115 +2,14 @@
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { MEDIA_TYPE } from '../../../base/media';
import {
AbstractButton,
type AbstractButtonProps
} from '../../../base/toolbox';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import MuteRemoteParticipantDialog from './MuteRemoteParticipantDialog';
type Props = AbstractButtonProps & {
/**
* The audio track of the participant.
*/
_audioTrack: ?Object,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The participant object that this button is supposed to mute/unmute.
*/
participant: Object
};
import AbstractMuteButton, { _mapStateToProps } from '../AbstractMuteButton';
/**
* A remote video menu button which mutes the remote participant.
* We don't need any further implementation for this on mobile, but we keep it
* here for clarity and consistency with web. Once web uses the
* {@code AbstractButton} base class, we can remove all these and just use
* the {@code AbstractMuteButton} as {@MuteButton}.
*/
class MuteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.audioRoute';
iconName = 'icon-mic-disabled';
label = 'videothumbnail.domute';
toggledLabel = 'videothumbnail.muted';
/**
* Handles clicking / pressing the button, and mutes the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participant } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
{
'participant_id': participant.id
}));
dispatch(openDialog(MuteRemoteParticipantDialog, { participant }));
}
/**
* Renders the item disabled if the participant is muted.
*
* @inheritdoc
*/
_isDisabled() {
return this._isMuted();
}
/**
* Returns true if the participant is muted, false otherwise.
*
* @returns {boolean}
*/
_isMuted() {
const { _audioTrack } = this.props;
return !_audioTrack || _audioTrack.muted;
}
/**
* Renders the item toggled if the participant is muted.
*
* @inheritdoc
*/
_isToggled() {
return this._isMuted();
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* _audioTrack: Track
* }}
*/
function _mapStateToProps(state, ownProps) {
const tracks = state['features/base/tracks'];
const audioTrack
= getTrackByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.AUDIO, ownProps.participant.id);
return {
_audioTrack: audioTrack
};
}
export default translate(connect(_mapStateToProps)(MuteButton));
export default translate(connect(_mapStateToProps)(AbstractMuteButton));

View File

@ -1,49 +1,18 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import {
createRemoteMuteConfirmedEvent,
sendAnalytics
} from '../../../analytics';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { muteRemoteParticipant } from '../../../base/participants';
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The remote participant to be muted.
*/
participant: Object,
/**
* Function to translate i18n labels.
*/
t: Function
};
import AbstractMuteRemoteParticipantDialog
from '../AbstractMuteRemoteParticipantDialog';
/**
* Dialog to confirm a remote participant mute action.
*/
class MuteRemoteParticipantDialog extends Component<Props> {
/**
* Initializes a new {@code MuteRemoteParticipantDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
/**
* Implements React's {@link Component#render()}.
*
@ -59,22 +28,6 @@ class MuteRemoteParticipantDialog extends Component<Props> {
}
_onSubmit: () => boolean;
/**
* Callback for the confirm button.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participant } = this.props;
sendAnalytics(createRemoteMuteConfirmedEvent(participant.id));
dispatch(muteRemoteParticipant(participant.id));
return true;
}
}
export default translate(connect()(MuteRemoteParticipantDialog));

View File

@ -72,7 +72,7 @@ class RemoteVideoMenu extends Component<Props> {
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participant: this.props.participant,
participantID: this.props.participant.id,
styles: bottomSheetItemStylesCombined
};

View File

@ -1,3 +1,9 @@
// @flow
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@ -1,5 +1,10 @@
// @flow
import {
MD_FONT_SIZE,
MD_ITEM_HEIGHT,
MD_ITEM_MARGIN_PADDING
} from '../../../base/dialog';
import { ColorPalette, createStyleSheet } from '../../../base/styles';
export default createStyleSheet({
@ -8,14 +13,14 @@ export default createStyleSheet({
borderBottomColor: ColorPalette.darkGrey,
borderBottomWidth: 1,
flexDirection: 'row',
height: 48
height: MD_ITEM_HEIGHT
},
participantNameLabel: {
color: ColorPalette.lightGrey,
flexShrink: 1,
fontSize: 16,
marginLeft: 16,
fontSize: MD_FONT_SIZE,
marginLeft: MD_ITEM_MARGIN_PADDING,
opacity: 0.90
}
});

View File

@ -1,62 +1,36 @@
/* @flow */
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { kickParticipant } from '../../../base/participants';
import AbstractKickButton, {
type Props
} from '../AbstractKickButton';
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
* NOTE: At the time of writing this is a button that doesn't use the
* {@code AbstractButton} base component, but is inherited from the same
* super class ({@code AbstractKickButton} that extends {@code AbstractButton})
* for the sake of code sharing between web and mobile. Once web uses the
* {@code AbstractButton} base component, this can be fully removed.
*/
class KickButton extends Component<Props> {
class KickButton extends AbstractKickButton {
/**
* Initializes a new {@code KickButton} instance.
* Instantiates a new {@code Component}.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
* @inheritdoc
*/
constructor(props) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
this._handleClick = this._handleClick.bind(this);
}
/**
@ -73,33 +47,12 @@ class KickButton extends Component<Props> {
buttonText = { t('videothumbnail.kick') }
iconClass = 'icon-kick'
id = { `ejectlink_${participantID}` }
onClick = { this._onClick } />
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_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();
}
}
_handleClick: () => void
}
export default translate(connect()(KickButton));

View File

@ -0,0 +1,39 @@
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import AbstractKickRemoteParticipantDialog
from '../AbstractKickRemoteParticipantDialog';
/**
* Dialog to confirm a remote participant kick action.
*/
class KickRemoteParticipantDialog extends AbstractKickRemoteParticipantDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okTitleKey = 'dialog.kickParticipantButton'
onSubmit = { this._onSubmit }
titleKey = 'dialog.kickParticipantTitle'
width = 'small'>
<div>
{ this.props.t('dialog.kickParticipantDialog') }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(KickRemoteParticipantDialog));

View File

@ -1,68 +1,37 @@
/* @flow */
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import { openDialog } from '../../../base/dialog';
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteButton';
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
* NOTE: At the time of writing this is a button that doesn't use the
* {@code AbstractButton} base component, but is inherited from the same
* super class ({@code AbstractMuteButton} that extends {@code AbstractButton})
* for the sake of code sharing between web and mobile. Once web uses the
* {@code AbstractButton} base component, this can be fully removed.
*/
class MuteButton extends Component<Props> {
class MuteButton extends AbstractMuteButton {
/**
* Initializes a new {@code MuteButton} instance.
* Instantiates a new {@code Component}.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
this._handleClick = this._handleClick.bind(this);
}
/**
@ -72,8 +41,8 @@ class MuteButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { isAudioMuted, participantID, t } = this.props;
const muteConfig = isAudioMuted ? {
const { _audioTrackMuted, participantID, t } = this.props;
const muteConfig = _audioTrackMuted ? {
translationKey: 'videothumbnail.muted',
muteClassName: 'mutelink disabled'
} : {
@ -87,34 +56,12 @@ class MuteButton extends Component<Props> {
displayClass = { muteConfig.muteClassName }
iconClass = 'icon-mic-disabled'
id = { `mutelink_${participantID}` }
onClick = { this._onClick } />
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_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();
}
}
_handleClick: () => void
}
export default translate(connect()(MuteButton));
export default translate(connect(_mapStateToProps)(MuteButton));

View File

@ -1,39 +1,13 @@
/* @flow */
import React, { Component } from 'react';
import React 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
};
import AbstractMuteRemoteParticipantDialog
from '../AbstractMuteRemoteParticipantDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
@ -41,21 +15,7 @@ type Props = {
*
* @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);
}
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
/**
* Implements React's {@link Component#render()}.
*
@ -69,48 +29,14 @@ class MuteRemoteParticipantDialog extends Component<Props> {
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantTitle'
width = 'small'>
{ this._renderContent() }
<div>
{ this.props.t('dialog.muteParticipantBody') }
</div>
</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>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(MuteRemoteParticipantDialog));

View File

@ -1,7 +1,13 @@
// @flow
export { default as KickButton } from './KickButton';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export {
REMOTE_CONTROL_MENU_STATES,
default as RemoteControlButton