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", "fullScreen": "Toggle full screen",
"hangup": "Leave the call", "hangup": "Leave the call",
"invite": "Invite people", "invite": "Invite people",
"kick": "Kick participant",
"localRecording": "Toggle local recording controls", "localRecording": "Toggle local recording controls",
"lockRoom": "Toggle room lock", "lockRoom": "Toggle room lock",
"moreActions": "Toggle more actions menu", "moreActions": "Toggle more actions menu",
@ -102,6 +103,7 @@
"profile": "Edit your profile", "profile": "Edit your profile",
"raiseHand": "Toggle raise hand", "raiseHand": "Toggle raise hand",
"recording": "Toggle recording", "recording": "Toggle recording",
"remoteMute": "Mute participant",
"Settings": "Toggle settings", "Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing", "sharedvideo": "Toggle Youtube video sharing",
"shareRoom": "Invite someone", "shareRoom": "Invite someone",
@ -385,7 +387,9 @@
"externalInstallationMsg": "You need to install our desktop sharing extension.", "externalInstallationMsg": "You need to install our desktop sharing extension.",
"inlineInstallationMsg": "You need to install our desktop sharing extension.", "inlineInstallationMsg": "You need to install our desktop sharing extension.",
"inlineInstallExtension": "Install now", "inlineInstallExtension": "Install now",
"kickParticipantButton": "Kick",
"kickParticipantDialog": "Are you sure you want to kick this participant?", "kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this member?",
"muteParticipantTitle": "Mute this member?", "muteParticipantTitle": "Mute this member?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", "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.", "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)'; const DIALOG_BORDER_COLOR = 'rgba(255, 255, 255, 0.2)';
export const FIELD_UNDERLINE = ColorPalette.transparent; 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; export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
/** /**
@ -25,7 +35,7 @@ const bottomSheetItemStyles = createStyleSheet({
style: { style: {
alignItems: 'center', alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
height: 48 height: MD_ITEM_HEIGHT
}, },
/** /**
@ -42,7 +52,7 @@ const bottomSheetItemStyles = createStyleSheet({
labelStyle: { labelStyle: {
color: ColorPalette.white, color: ColorPalette.white,
flexShrink: 1, flexShrink: 1,
fontSize: 16, fontSize: MD_FONT_SIZE,
marginLeft: 32, marginLeft: 32,
opacity: 0.90 opacity: 0.90
} }
@ -92,7 +102,7 @@ export const bottomSheetStyles = createStyleSheet({
sheet: { sheet: {
backgroundColor: 'rgb(0, 3, 6)', backgroundColor: 'rgb(0, 3, 6)',
flex: 1, flex: 1,
paddingHorizontal: 16, paddingHorizontal: MD_ITEM_MARGIN_PADDING,
paddingVertical: 8 paddingVertical: 8
} }
}); });
@ -134,7 +144,7 @@ export const brandedDialog = createStyleSheet({
closeStyle: { closeStyle: {
color: ColorPalette.white, color: ColorPalette.white,
fontSize: 16 fontSize: MD_FONT_SIZE
}, },
closeWrapper: { closeWrapper: {
@ -173,7 +183,7 @@ export const brandedDialog = createStyleSheet({
text: { text: {
color: ColorPalette.white, color: ColorPalette.white,
fontSize: 16, fontSize: MD_FONT_SIZE,
textAlign: 'center' textAlign: 'center'
} }
}); });

View File

@ -31,12 +31,6 @@ export type Props = {
*/ */
onClick?: ?Function, 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 * The style (as in stylesheet) to be applied to this
* {@code AbstractContainer}. * {@code AbstractContainer}.

View File

@ -8,7 +8,16 @@ import {
} from 'react-native'; } from 'react-native';
import AbstractContainer from '../AbstractContainer'; 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. * Represents a container of React Native/mobile {@link Component} children.
@ -28,7 +37,7 @@ export default class Container<P: Props> extends AbstractContainer<P> {
accessible, accessible,
onClick, onClick,
onLongPress, onLongPress,
touchFeedback = onClick, touchFeedback = Boolean(onClick || onLongPress),
underlayColor, underlayColor,
visible = true, visible = true,
...props ...props
@ -50,19 +59,24 @@ export default class Container<P: Props> extends AbstractContainer<P> {
// onClick & touchFeedback // onClick & touchFeedback
if (element && onClickOrTouchFeedback) { if (element && onClickOrTouchFeedback) {
const touchableProps = {
accessibilityLabel,
accessible,
onLongPress,
onPress: onClick
};
element element
= React.createElement( = touchFeedback
touchFeedback ? React.createElement(
? TouchableHighlight TouchableHighlight,
: TouchableWithoutFeedback, {
{ ...touchableProps,
accessibilityLabel, underlayColor
accessible, },
onLongPress, element)
onPress: onClick, : React.createElement(
...touchFeedback && { underlayColor } TouchableWithoutFeedback, touchableProps, element);
},
element);
} }
return element; return element;

View File

@ -209,6 +209,22 @@ export function isLocalTrackMuted(tracks, mediaType) {
return !track || track.muted; 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 * Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of
* the specified {@code track} is already in accord with the specified * the specified {@code track} is already in accord with the specified

View File

@ -43,6 +43,16 @@ type Props = {
*/ */
_largeVideo: Object, _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. * The Redux representation of the participant's video track.
*/ */
@ -83,19 +93,6 @@ type Props = {
* @extends Component * @extends Component
*/ */
class Thumbnail extends Component<Props> { 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()}. * Implements React's {@link Component#render()}.
* *
@ -107,6 +104,8 @@ class Thumbnail extends Component<Props> {
_audioTrack: audioTrack, _audioTrack: audioTrack,
_isModerator, _isModerator,
_largeVideo: largeVideo, _largeVideo: largeVideo,
_onClick,
_onShowRemoteVideoMenu,
_videoTrack: videoTrack, _videoTrack: videoTrack,
disablePin, disablePin,
disableTint, disableTint,
@ -129,10 +128,10 @@ class Thumbnail extends Component<Props> {
return ( return (
<Container <Container
onClick = { disablePin ? undefined : this._onClick } onClick = { disablePin ? undefined : _onClick }
onLongPress = { onLongPress = {
showRemoteVideoMenu showRemoteVideoMenu
? this._onShowRemoteVideoMenu : undefined } ? _onShowRemoteVideoMenu : undefined }
style = { [ style = { [
styles.thumbnail, styles.thumbnail,
participant.pinned && !disablePin participant.pinned && !disablePin
@ -169,43 +168,53 @@ class Thumbnail extends Component<Props> {
</Container> </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;
/** dispatch(
* Handles click/tap event on the thumbnail. pinParticipant(participant.pinned ? null : participant.id));
* },
* @returns {void}
*/
_onClick() {
const { dispatch, participant } = this.props;
// 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; dispatch(openDialog(RemoteVideoMenu, {
participant
/** }));
* Handles long press on the thumbnail. }
* };
* @returns {void}
*/
_onShowRemoteVideoMenu() {
const { dispatch, participant } = this.props;
dispatch(openDialog(RemoteVideoMenu, {
participant
}));
}
} }
/** /**
* Function that maps parts of Redux state tree into component props. * Function that maps parts of Redux state tree into component props.
* *
* @param {Object} state - Redux state. * @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component. * @param {Props} ownProps - Properties of component.
* @private
* @returns {{ * @returns {{
* _audioTrack: Track, * _audioTrack: Track,
* _isModerator: boolean, * _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 // @flow
import { MD_ITEM_HEIGHT } from '../../../base/dialog';
import { ColorPalette, createStyleSheet } from '../../../base/styles'; import { ColorPalette, createStyleSheet } from '../../../base/styles';
/** /**
@ -16,7 +17,7 @@ export default createStyleSheet({
deviceRow: { deviceRow: {
alignItems: 'center', alignItems: 'center',
flexDirection: 'row', 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 { connect } from 'react-redux';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { AbstractButton } from '../../../base/toolbox';
import type { AbstractButtonProps } from '../../../base/toolbox';
import KickRemoteParticipantDialog from './KickRemoteParticipantDialog'; import AbstractKickButton from '../AbstractKickButton';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The participant object that this button is supposed to kick.
*/
participant: Object
};
/** /**
* 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, *> { export default translate(connect()(AbstractKickButton));
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));

View File

@ -1,49 +1,18 @@
// @flow // @flow
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { ConfirmDialog } from '../../../base/dialog'; import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { kickParticipant } from '../../../base/participants';
type Props = { import AbstractKickRemoteParticipantDialog
from '../AbstractKickRemoteParticipantDialog';
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The remote participant to be kicked.
*/
participant: Object,
/**
* Function to translate i18n labels.
*/
t: Function
};
/** /**
* Dialog to confirm a remote participant kick action. * Dialog to confirm a remote participant kick action.
*/ */
class KickRemoteParticipantDialog extends Component<Props> { class KickRemoteParticipantDialog extends AbstractKickRemoteParticipantDialog {
/**
* Initializes a new {@code KickRemoteParticipantDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -59,26 +28,6 @@ class KickRemoteParticipantDialog extends Component<Props> {
} }
_onSubmit: () => boolean; _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)); export default translate(connect()(KickRemoteParticipantDialog));

View File

@ -2,115 +2,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; 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'; import AbstractMuteButton, { _mapStateToProps } from '../AbstractMuteButton';
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
};
/** /**
* 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, *> { export default translate(connect(_mapStateToProps)(AbstractMuteButton));
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));

View File

@ -1,49 +1,18 @@
// @flow // @flow
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRemoteMuteConfirmedEvent,
sendAnalytics
} from '../../../analytics';
import { ConfirmDialog } from '../../../base/dialog'; import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { muteRemoteParticipant } from '../../../base/participants';
type Props = { import AbstractMuteRemoteParticipantDialog
from '../AbstractMuteRemoteParticipantDialog';
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The remote participant to be muted.
*/
participant: Object,
/**
* Function to translate i18n labels.
*/
t: Function
};
/** /**
* Dialog to confirm a remote participant mute action. * Dialog to confirm a remote participant mute action.
*/ */
class MuteRemoteParticipantDialog extends Component<Props> { class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
/**
* Initializes a new {@code MuteRemoteParticipantDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSubmit = this._onSubmit.bind(this);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -59,22 +28,6 @@ class MuteRemoteParticipantDialog extends Component<Props> {
} }
_onSubmit: () => boolean; _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)); export default translate(connect()(MuteRemoteParticipantDialog));

View File

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

View File

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

View File

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

View File

@ -1,62 +1,36 @@
/* @flow */ /* @flow */
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { kickParticipant } from '../../../base/participants';
import AbstractKickButton, {
type Props
} from '../AbstractKickButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton'; 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 * Implements a React {@link Component} which displays a button for kicking out
* a participant from the conference. * 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 * @inheritdoc
* the new instance is to be initialized.
*/ */
constructor(props) { constructor(props: Props) {
super(props); super(props);
// Bind event handlers so they are only bound once for every instance. this._handleClick = this._handleClick.bind(this);
this._onClick = this._onClick.bind(this);
} }
/** /**
@ -73,33 +47,12 @@ class KickButton extends Component<Props> {
buttonText = { t('videothumbnail.kick') } buttonText = { t('videothumbnail.kick') }
iconClass = 'icon-kick' iconClass = 'icon-kick'
id = { `ejectlink_${participantID}` } id = { `ejectlink_${participantID}` }
onClick = { this._onClick } /> // eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
); );
} }
_onClick: () => void; _handleClick: () => 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)); 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 */ /* @flow */
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { openDialog } from '../../../base/dialog';
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton'; 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 * Implements a React {@link Component} which displays a button for audio muting
* a participant in the conference. * 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 * @inheritdoc
* the new instance is to be initialized.
*/ */
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
// Bind event handlers so they are only bound once for every instance. this._handleClick = this._handleClick.bind(this);
this._onClick = this._onClick.bind(this);
} }
/** /**
@ -72,8 +41,8 @@ class MuteButton extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { isAudioMuted, participantID, t } = this.props; const { _audioTrackMuted, participantID, t } = this.props;
const muteConfig = isAudioMuted ? { const muteConfig = _audioTrackMuted ? {
translationKey: 'videothumbnail.muted', translationKey: 'videothumbnail.muted',
muteClassName: 'mutelink disabled' muteClassName: 'mutelink disabled'
} : { } : {
@ -87,34 +56,12 @@ class MuteButton extends Component<Props> {
displayClass = { muteConfig.muteClassName } displayClass = { muteConfig.muteClassName }
iconClass = 'icon-mic-disabled' iconClass = 'icon-mic-disabled'
id = { `mutelink_${participantID}` } id = { `mutelink_${participantID}` }
onClick = { this._onClick } /> // eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
); );
} }
_onClick: () => void; _handleClick: () => 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)); export default translate(connect(_mapStateToProps)(MuteButton));

View File

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

View File

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