From 5c0ae10ccb714511433b3aa0649afbc810ef4dd1 Mon Sep 17 00:00:00 2001 From: Bettenbuk Zoltan Date: Sat, 5 Jan 2019 17:49:21 +0100 Subject: [PATCH] Remote video menu post-PR improvements --- lang/main.json | 4 + .../base/dialog/components/native/styles.js | 20 +++- .../react/components/AbstractContainer.js | 6 - .../base/react/components/native/Container.js | 42 ++++--- react/features/base/tracks/functions.js | 16 +++ .../filmstrip/components/native/Thumbnail.js | 95 ++++++++------- .../mobile/audio-mode/components/styles.js | 3 +- .../components/AbstractKickButton.js | 46 +++++++ .../AbstractKickRemoteParticipantDialog.js | 66 ++++++++++ .../components/AbstractMuteButton.js | 105 ++++++++++++++++ .../AbstractMuteRemoteParticipantDialog.js | 70 +++++++++++ .../components/native/KickButton.js | 43 +------ .../native/KickRemoteParticipantDialog.js | 59 +-------- .../components/native/MuteButton.js | 113 +----------------- .../native/MuteRemoteParticipantDialog.js | 55 +-------- .../components/native/RemoteVideoMenu.js | 2 +- .../components/native/index.js | 6 + .../components/native/styles.js | 11 +- .../components/web/KickButton.js | 83 +++---------- .../web/KickRemoteParticipantDialog.js | 39 ++++++ .../components/web/MuteButton.js | 95 ++++----------- .../web/MuteRemoteParticipantDialog.js | 90 ++------------ .../remote-video-menu/components/web/index.js | 6 + 23 files changed, 531 insertions(+), 544 deletions(-) create mode 100644 react/features/remote-video-menu/components/AbstractKickButton.js create mode 100644 react/features/remote-video-menu/components/AbstractKickRemoteParticipantDialog.js create mode 100644 react/features/remote-video-menu/components/AbstractMuteButton.js create mode 100644 react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js create mode 100644 react/features/remote-video-menu/components/web/KickRemoteParticipantDialog.js diff --git a/lang/main.json b/lang/main.json index f608a30d3..326be4e45 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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.", diff --git a/react/features/base/dialog/components/native/styles.js b/react/features/base/dialog/components/native/styles.js index 574174911..e188468a7 100644 --- a/react/features/base/dialog/components/native/styles.js +++ b/react/features/base/dialog/components/native/styles.js @@ -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' } }); diff --git a/react/features/base/react/components/AbstractContainer.js b/react/features/base/react/components/AbstractContainer.js index 4945c2956..726a29a02 100644 --- a/react/features/base/react/components/AbstractContainer.js +++ b/react/features/base/react/components/AbstractContainer.js @@ -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}. diff --git a/react/features/base/react/components/native/Container.js b/react/features/base/react/components/native/Container.js index 5122bcc71..9880bcd31 100644 --- a/react/features/base/react/components/native/Container.js +++ b/react/features/base/react/components/native/Container.js @@ -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 extends AbstractContainer

{ accessible, onClick, onLongPress, - touchFeedback = onClick, + touchFeedback = Boolean(onClick || onLongPress), underlayColor, visible = true, ...props @@ -50,19 +59,24 @@ export default class Container extends AbstractContainer

{ // 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; diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index fc7e16c07..2af87c71b 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -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 diff --git a/react/features/filmstrip/components/native/Thumbnail.js b/react/features/filmstrip/components/native/Thumbnail.js index cbec33300..50513c151 100644 --- a/react/features/filmstrip/components/native/Thumbnail.js +++ b/react/features/filmstrip/components/native/Thumbnail.js @@ -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 { - /** - * 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 { _audioTrack: audioTrack, _isModerator, _largeVideo: largeVideo, + _onClick, + _onShowRemoteVideoMenu, _videoTrack: videoTrack, disablePin, disableTint, @@ -129,10 +128,10 @@ class Thumbnail extends Component { return ( { ); } +} - _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); diff --git a/react/features/mobile/audio-mode/components/styles.js b/react/features/mobile/audio-mode/components/styles.js index 90b5ad8c1..033c82775 100644 --- a/react/features/mobile/audio-mode/components/styles.js +++ b/react/features/mobile/audio-mode/components/styles.js @@ -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 }, /** diff --git a/react/features/remote-video-menu/components/AbstractKickButton.js b/react/features/remote-video-menu/components/AbstractKickButton.js new file mode 100644 index 000000000..2364ef523 --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractKickButton.js @@ -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 { + 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 })); + } +} diff --git a/react/features/remote-video-menu/components/AbstractKickRemoteParticipantDialog.js b/react/features/remote-video-menu/components/AbstractKickRemoteParticipantDialog.js new file mode 100644 index 000000000..9a521fa98 --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractKickRemoteParticipantDialog.js @@ -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 { + /** + * 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; + } +} diff --git a/react/features/remote-video-menu/components/AbstractMuteButton.js b/react/features/remote-video-menu/components/AbstractMuteButton.js new file mode 100644 index 000000000..748a9fe7d --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteButton.js @@ -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 { + 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) + }; +} diff --git a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js new file mode 100644 index 000000000..d72903fdb --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js @@ -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 { + /** + * 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; + } +} diff --git a/react/features/remote-video-menu/components/native/KickButton.js b/react/features/remote-video-menu/components/native/KickButton.js index 2f10d590a..5f786b202 100644 --- a/react/features/remote-video-menu/components/native/KickButton.js +++ b/react/features/remote-video-menu/components/native/KickButton.js @@ -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 { - 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)); diff --git a/react/features/remote-video-menu/components/native/KickRemoteParticipantDialog.js b/react/features/remote-video-menu/components/native/KickRemoteParticipantDialog.js index 99615c0b5..e6fda5938 100644 --- a/react/features/remote-video-menu/components/native/KickRemoteParticipantDialog.js +++ b/react/features/remote-video-menu/components/native/KickRemoteParticipantDialog.js @@ -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 { - /** - * 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 { } _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)); diff --git a/react/features/remote-video-menu/components/native/MuteButton.js b/react/features/remote-video-menu/components/native/MuteButton.js index b95294806..6fcec06bc 100644 --- a/react/features/remote-video-menu/components/native/MuteButton.js +++ b/react/features/remote-video-menu/components/native/MuteButton.js @@ -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 { - 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)); diff --git a/react/features/remote-video-menu/components/native/MuteRemoteParticipantDialog.js b/react/features/remote-video-menu/components/native/MuteRemoteParticipantDialog.js index c06d1affc..51e79842b 100644 --- a/react/features/remote-video-menu/components/native/MuteRemoteParticipantDialog.js +++ b/react/features/remote-video-menu/components/native/MuteRemoteParticipantDialog.js @@ -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 { - /** - * 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 { } _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)); diff --git a/react/features/remote-video-menu/components/native/RemoteVideoMenu.js b/react/features/remote-video-menu/components/native/RemoteVideoMenu.js index 11d8ae0d8..b9c1f1ffa 100644 --- a/react/features/remote-video-menu/components/native/RemoteVideoMenu.js +++ b/react/features/remote-video-menu/components/native/RemoteVideoMenu.js @@ -72,7 +72,7 @@ class RemoteVideoMenu extends Component { const buttonProps = { afterClick: this._onCancel, showLabel: true, - participant: this.props.participant, + participantID: this.props.participant.id, styles: bottomSheetItemStylesCombined }; diff --git a/react/features/remote-video-menu/components/native/index.js b/react/features/remote-video-menu/components/native/index.js index c8393b1ac..6c509ecae 100644 --- a/react/features/remote-video-menu/components/native/index.js +++ b/react/features/remote-video-menu/components/native/index.js @@ -1,3 +1,9 @@ // @flow +export { + default as KickRemoteParticipantDialog +} from './KickRemoteParticipantDialog'; +export { + default as MuteRemoteParticipantDialog +} from './MuteRemoteParticipantDialog'; export { default as RemoteVideoMenu } from './RemoteVideoMenu'; diff --git a/react/features/remote-video-menu/components/native/styles.js b/react/features/remote-video-menu/components/native/styles.js index e82693bd2..24225ca82 100644 --- a/react/features/remote-video-menu/components/native/styles.js +++ b/react/features/remote-video-menu/components/native/styles.js @@ -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 } }); diff --git a/react/features/remote-video-menu/components/web/KickButton.js b/react/features/remote-video-menu/components/web/KickButton.js index b4a870bb6..6d4d47cea 100644 --- a/react/features/remote-video-menu/components/web/KickButton.js +++ b/react/features/remote-video-menu/components/web/KickButton.js @@ -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 { +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 { 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)); diff --git a/react/features/remote-video-menu/components/web/KickRemoteParticipantDialog.js b/react/features/remote-video-menu/components/web/KickRemoteParticipantDialog.js new file mode 100644 index 000000000..c5b17dbd1 --- /dev/null +++ b/react/features/remote-video-menu/components/web/KickRemoteParticipantDialog.js @@ -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 ( +

+
+ { this.props.t('dialog.kickParticipantDialog') } +
+
+ ); + } + + _onSubmit: () => boolean; +} + +export default translate(connect()(KickRemoteParticipantDialog)); diff --git a/react/features/remote-video-menu/components/web/MuteButton.js b/react/features/remote-video-menu/components/web/MuteButton.js index 20c3f0c57..c01643bd0 100644 --- a/react/features/remote-video-menu/components/web/MuteButton.js +++ b/react/features/remote-video-menu/components/web/MuteButton.js @@ -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 { +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 { * @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 { 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)); diff --git a/react/features/remote-video-menu/components/web/MuteRemoteParticipantDialog.js b/react/features/remote-video-menu/components/web/MuteRemoteParticipantDialog.js index 65d806726..46f9a7114 100644 --- a/react/features/remote-video-menu/components/web/MuteRemoteParticipantDialog.js +++ b/react/features/remote-video-menu/components/web/MuteRemoteParticipantDialog.js @@ -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 { - /** - * 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 { onSubmit = { this._onSubmit } titleKey = 'dialog.muteParticipantTitle' width = 'small'> - { this._renderContent() } +
+ { this.props.t('dialog.muteParticipantBody') } +
); } - _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 ( -
- { t('dialog.muteParticipantBody') } -
- ); - } - + _onSubmit: () => boolean; } export default translate(connect()(MuteRemoteParticipantDialog)); diff --git a/react/features/remote-video-menu/components/web/index.js b/react/features/remote-video-menu/components/web/index.js index 3485ba422..c27a7b59c 100644 --- a/react/features/remote-video-menu/components/web/index.js +++ b/react/features/remote-video-menu/components/web/index.js @@ -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