diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 023f9237bf..1d93b7267f 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -278,6 +278,10 @@ } } +.profile-button-avatar { + align-items: center; +} + /** * START of fade in animation for main toolbar */ diff --git a/react/features/base/config/constants.js b/react/features/base/config/constants.js index b41d3ff0f5..5600265b35 100644 --- a/react/features/base/config/constants.js +++ b/react/features/base/config/constants.js @@ -19,5 +19,5 @@ export const TOOLBAR_BUTTONS = [ 'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand', 'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts', 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', - 'security', 'toggle-camera' + 'security' ]; diff --git a/react/features/chat/components/web/ChatButton.js b/react/features/chat/components/web/ChatButton.js new file mode 100644 index 0000000000..9791dbb6b1 --- /dev/null +++ b/react/features/chat/components/web/ChatButton.js @@ -0,0 +1,109 @@ +// @flow + +import React from 'react'; + +import { translate } from '../../../base/i18n'; +import { IconChat } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; + +import ChatCounter from './ChatCounter'; + +/** + * The type of the React {@code Component} props of {@link ChatButton}. + */ +type Props = AbstractButtonProps & { + + /** + * Whether or not the chat feature is currently displayed. + */ + _chatOpen: boolean, + + /** + * External handler for click action. + */ + handleClick: Function +}; + +/** + * Implementation of a button for accessing chat pane. + */ +class ChatButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.chat'; + icon = IconChat; + label = 'toolbar.openChat'; + toggledLabel = 'toolbar.closeChat'; + + /** + * Retrieves tooltip dynamically. + */ + get tooltip() { + if (this._isToggled()) { + return 'toolbar.closeChat'; + } + + return 'toolbar.openChat'; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._chatOpen; + } + + /** + * Overrides AbstractButton's {@link Component#render()}. + * + * @override + * @protected + * @returns {boReact$Nodeolean} + */ + render(): React$Node { + return ( +
+ {super.render()} + +
+ ); + } +} + +/** + * Function that maps parts of Redux state tree into component props. + * + * @param {Object} state - Redux state. + * @returns {Object} + */ +const mapStateToProps = state => { + return { + _chatOpen: state['features/chat'].isOpen + }; +}; + +export default translate(connect(mapStateToProps)(ChatButton)); diff --git a/react/features/chat/components/web/index.js b/react/features/chat/components/web/index.js index 18dd324100..d175ebc845 100644 --- a/react/features/chat/components/web/index.js +++ b/react/features/chat/components/web/index.js @@ -1,5 +1,6 @@ // @flow export { default as Chat } from './Chat'; +export { default as ChatButton } from './ChatButton'; export { default as ChatCounter } from './ChatCounter'; export { default as ChatPrivacyDialog } from './ChatPrivacyDialog'; diff --git a/react/features/embed-meeting/components/EmbedMeetingButton.js b/react/features/embed-meeting/components/EmbedMeetingButton.js new file mode 100644 index 0000000000..95b0de9a8c --- /dev/null +++ b/react/features/embed-meeting/components/EmbedMeetingButton.js @@ -0,0 +1,46 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { IconCodeBlock } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; + +import EmbedMeetingDialog from './EmbedMeetingDialog'; + +/** + * The type of the React {@code Component} props of {@link EmbedMeetingButton}. + */ +type Props = AbstractButtonProps & { + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * Implementation of a button for opening embed meeting dialog. + */ +class EmbedMeetingButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.embedMeeting'; + icon = IconCodeBlock; + label = 'toolbar.embedMeeting'; + tooltip = 'toolbar.embedMeeting'; + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + const { dispatch } = this.props; + + sendAnalytics(createToolbarEvent('embed.meeting')); + dispatch(openDialog(EmbedMeetingDialog)); + } +} + +export default translate(connect()(EmbedMeetingButton)); diff --git a/react/features/embed-meeting/components/index.js b/react/features/embed-meeting/components/index.js index 572171561d..7dbfb06891 100644 --- a/react/features/embed-meeting/components/index.js +++ b/react/features/embed-meeting/components/index.js @@ -1 +1,2 @@ +export { default as EmbedMeetingButton } from './EmbedMeetingButton'; export { default as EmbedMeetingDialog } from './EmbedMeetingDialog'; diff --git a/react/features/etherpad/components/SharedDocumentButton.js b/react/features/etherpad/components/SharedDocumentButton.js index 202902859c..fd119a7f09 100644 --- a/react/features/etherpad/components/SharedDocumentButton.js +++ b/react/features/etherpad/components/SharedDocumentButton.js @@ -32,6 +32,26 @@ class SharedDocumentButton extends AbstractButton { label = 'toolbar.documentOpen'; toggledLabel = 'toolbar.documentClose'; + /** + * Dynamically retrieves tooltip based on sharing state. + */ + get tooltip() { + if (this._isToggled()) { + return 'toolbar.documentClose'; + } + + return 'toolbar.documentOpen'; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + /** * Handles clicking / pressing the button, and opens / closes the appropriate dialog. * diff --git a/react/features/video-quality/components/OverflowMenuVideoQualityItem.native.js b/react/features/feedback/components/FeedbackButton.native.js similarity index 100% rename from react/features/video-quality/components/OverflowMenuVideoQualityItem.native.js rename to react/features/feedback/components/FeedbackButton.native.js diff --git a/react/features/feedback/components/FeedbackButton.web.js b/react/features/feedback/components/FeedbackButton.web.js new file mode 100644 index 0000000000..f8bf191d9b --- /dev/null +++ b/react/features/feedback/components/FeedbackButton.web.js @@ -0,0 +1,56 @@ + +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { translate } from '../../base/i18n'; +import { IconFeedback } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +import { openFeedbackDialog } from '../actions'; + +/** + * The type of the React {@code Component} props of {@link FeedbackButton}. + */ +type Props = AbstractButtonProps & { + + /** + * The {@code JitsiConference} for the current conference. + */ + _conference: Object, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * Implementation of a button for opening feedback dialog. + */ +class FeedbackButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.feedback'; + icon = IconFeedback; + label = 'toolbar.feedback'; + tooltip = 'toolbar.feedback'; + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + const { _conference, dispatch } = this.props; + + sendAnalytics(createToolbarEvent('feedback')); + dispatch(openFeedbackDialog(_conference)); + } +} + +const mapStateToProps = state => { + return { + _conference: state['features/base/conference'].conference + }; +}; + +export default translate(connect(mapStateToProps)(FeedbackButton)); diff --git a/react/features/feedback/components/index.js b/react/features/feedback/components/index.js index b4cc07a734..bea3d61b69 100644 --- a/react/features/feedback/components/index.js +++ b/react/features/feedback/components/index.js @@ -1 +1,2 @@ +export { default as FeedbackButton } from './FeedbackButton'; export { default as FeedbackDialog } from './FeedbackDialog'; diff --git a/react/features/keyboard-shortcuts/components/KeyboardShortcutsButton.native.js b/react/features/keyboard-shortcuts/components/KeyboardShortcutsButton.native.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/react/features/keyboard-shortcuts/components/KeyboardShortcutsButton.web.js b/react/features/keyboard-shortcuts/components/KeyboardShortcutsButton.web.js new file mode 100644 index 0000000000..2e90567485 --- /dev/null +++ b/react/features/keyboard-shortcuts/components/KeyboardShortcutsButton.web.js @@ -0,0 +1,44 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { translate } from '../../base/i18n'; +import { IconDeviceDocument } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +import { openKeyboardShortcutsDialog } from '../actions'; + +/** + * The type of the React {@code Component} props of {@link KeyboardShortcutsButton}. + */ +type Props = AbstractButtonProps & { + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * Implementation of a button for opening keyboard shortcuts dialog. + */ +class KeyboardShortcutsButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.shortcuts'; + icon = IconDeviceDocument; + label = 'toolbar.shortcuts'; + tooltip = 'toolbar.shortcuts'; + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + const { dispatch } = this.props; + + sendAnalytics(createToolbarEvent('shortcuts')); + dispatch(openKeyboardShortcutsDialog()); + } +} + +export default translate(connect()(KeyboardShortcutsButton)); diff --git a/react/features/keyboard-shortcuts/components/index.js b/react/features/keyboard-shortcuts/components/index.js index 82366f063a..25f84920e7 100644 --- a/react/features/keyboard-shortcuts/components/index.js +++ b/react/features/keyboard-shortcuts/components/index.js @@ -1 +1,2 @@ +export { default as KeyboardShortcutsButton } from './KeyboardShortcutsButton'; export { default as KeyboardShortcutsDialog } from './KeyboardShortcutsDialog'; diff --git a/react/features/local-recording/components/LocalRecordingButton.web.js b/react/features/local-recording/components/LocalRecordingButton.web.js index 2b174f4455..0d1fc84518 100644 --- a/react/features/local-recording/components/LocalRecordingButton.web.js +++ b/react/features/local-recording/components/LocalRecordingButton.web.js @@ -1,85 +1,46 @@ -/* @flow */ - -import React, { Component } from 'react'; +// @flow +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { IconRec } from '../../base/icons'; -import { ToolbarButton } from '../../toolbox/components/web'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; + +import LocalRecordingInfoDialog from './LocalRecordingInfoDialog'; /** - * The type of the React {@code Component} state of - * {@link LocalRecordingButton}. + * The type of the React {@code Component} props of {@link LocalRecording}. */ -type Props = { +type Props = AbstractButtonProps & { /** - * Whether or not {@link LocalRecordingInfoDialog} should be displayed. + * The redux {@code dispatch} function. */ - isDialogShown: boolean, - - /** - * Callback function called when {@link LocalRecordingButton} is clicked. - */ - onClick: Function, - - /** - * Invoked to obtain translated strings. - */ - t: Function -} + dispatch: Function +}; /** - * A React {@code Component} for opening or closing the - * {@code LocalRecordingInfoDialog}. - * - * @extends Component + * Implementation of a button for opening local recording dialog. */ -class LocalRecordingButton extends Component { +class LocalRecording extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.localRecording'; + icon = IconRec; + label = 'localRecording.dialogTitle'; + tooltip = 'localRecording.dialogTitle'; /** - * Initializes a new {@code LocalRecordingButton} instance. + * Handles clicking / pressing the button, and opens the appropriate dialog. * - * @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._onClick = this._onClick.bind(this); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { isDialogShown, t } = this.props; - - return ( - - ); - } - - _onClick: () => void; - - /** - * Callback invoked when the Toolbar button is clicked. - * - * @private + * @protected * @returns {void} */ - _onClick() { - this.props.onClick(); + _handleClick() { + const { dispatch } = this.props; + + sendAnalytics(createToolbarEvent('local.recording')); + dispatch(openDialog(LocalRecordingInfoDialog)); } } -export default translate(LocalRecordingButton); +export default translate(connect()(LocalRecording)); diff --git a/react/features/participants-pane/components/ParticipantsPaneButton.js b/react/features/participants-pane/components/ParticipantsPaneButton.js new file mode 100644 index 0000000000..a670900725 --- /dev/null +++ b/react/features/participants-pane/components/ParticipantsPaneButton.js @@ -0,0 +1,39 @@ +// @flow + +import { translate } from '../../base/i18n'; +import { IconParticipants } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; + +/** + * The type of the React {@code Component} props of {@link ParticipantsPaneButton}. + */ +type Props = AbstractButtonProps & { + + /** + * External handler for click action. + */ + handleClick: Function +}; + +/** + * Implementation of a button for accessing participants pane. + */ +class ParticipantsPaneButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.participants'; + icon = IconParticipants; + label = 'toolbar.participants'; + tooltip = 'toolbar.participants'; + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); + } +} + +export default translate(connect()(ParticipantsPaneButton)); diff --git a/react/features/participants-pane/components/index.js b/react/features/participants-pane/components/index.js index 78902302e2..0b623880f4 100644 --- a/react/features/participants-pane/components/index.js +++ b/react/features/participants-pane/components/index.js @@ -6,4 +6,5 @@ export * from './MeetingParticipantItem'; export * from './MeetingParticipantList'; export * from './ParticipantItem'; export * from './ParticipantsPane'; +export * from './ParticipantsPaneButton'; export * from './RaisedHandIndicator'; diff --git a/react/features/security/components/security-dialog/SecurityDialogButton.js b/react/features/security/components/security-dialog/SecurityDialogButton.js index 6704824ef5..049342a7bf 100644 --- a/react/features/security/components/security-dialog/SecurityDialogButton.js +++ b/react/features/security/components/security-dialog/SecurityDialogButton.js @@ -39,6 +39,7 @@ class SecurityDialogButton extends AbstractButton { icon = IconSecurityOff; label = 'toolbar.security'; toggledIcon = IconSecurityOn; + tooltip = 'toolbar.security'; /** * Handles clicking / pressing the button, and opens / closes the appropriate dialog. diff --git a/react/features/settings/components/web/SettingsButton.js b/react/features/settings/components/web/SettingsButton.js index 4baca47b0b..52ddf7fb54 100644 --- a/react/features/settings/components/web/SettingsButton.js +++ b/react/features/settings/components/web/SettingsButton.js @@ -31,6 +31,7 @@ class SettingsButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.Settings'; icon = IconSettings; label = 'toolbar.Settings'; + tooltip = 'toolbar.Settings'; /** * Handles clicking / pressing the button, and opens the appropriate dialog. diff --git a/react/features/shared-video/components/web/SharedVideoButton.js b/react/features/shared-video/components/web/SharedVideoButton.js index cd9f3b6896..eb9b73c5de 100644 --- a/react/features/shared-video/components/web/SharedVideoButton.js +++ b/react/features/shared-video/components/web/SharedVideoButton.js @@ -37,9 +37,28 @@ class SharedVideoButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo'; icon = IconShareVideo; label = 'toolbar.sharedvideo'; - tooltip = 'toolbar.sharedvideo'; toggledLabel = 'toolbar.stopSharedVideo'; + /** + * Dynamically retrieves tooltip based on sharing state. + */ + get tooltip() { + if (this._isToggled()) { + return 'toolbar.stopSharedVideo'; + } + + return 'toolbar.sharedvideo'; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + /** * Handles clicking / pressing the button, and opens a new dialog. * diff --git a/react/features/speaker-stats/components/SpeakerStatsButton.js b/react/features/speaker-stats/components/SpeakerStatsButton.js new file mode 100644 index 0000000000..c00806c031 --- /dev/null +++ b/react/features/speaker-stats/components/SpeakerStatsButton.js @@ -0,0 +1,67 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { IconPresentation } from '../../base/icons'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; + +import SpeakerStats from './SpeakerStats'; + +/** + * The type of the React {@code Component} props of {@link SpeakerStatsButton}. + */ +type Props = AbstractButtonProps & { + + /** + * The {@code JitsiConference} for the current conference. + */ + _conference: Object, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * Implementation of a button for opening speaker stats dialog. + */ +class SpeakerStatsButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.speakerStats'; + icon = IconPresentation; + label = 'toolbar.speakerStats'; + tooltip = 'toolbar.speakerStats'; + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + const { _conference, dispatch } = this.props; + + sendAnalytics(createToolbarEvent('speaker.stats')); + dispatch(openDialog(SpeakerStats, { + conference: _conference + })); + } +} + +/** + * Maps (parts of) the Redux state to the associated + * {@code SpeakerStatsButton} component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Object} + */ +function mapStateToProps(state) { + return { + _conference: state['features/base/conference'].conference + }; +} + +export default translate(connect(mapStateToProps)(SpeakerStatsButton)); diff --git a/react/features/speaker-stats/components/index.js b/react/features/speaker-stats/components/index.js index 700a545b93..7787e1711c 100644 --- a/react/features/speaker-stats/components/index.js +++ b/react/features/speaker-stats/components/index.js @@ -1 +1,2 @@ +export { default as SpeakerStatsButton } from './SpeakerStatsButton'; export { default as SpeakerStats } from './SpeakerStats'; diff --git a/react/features/toolbox/components/DownloadButton.js b/react/features/toolbox/components/DownloadButton.js index 264b118e80..30bb1dfba3 100644 --- a/react/features/toolbox/components/DownloadButton.js +++ b/react/features/toolbox/components/DownloadButton.js @@ -23,6 +23,7 @@ class DownloadButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.download'; icon = IconDownload; label = 'toolbar.download'; + tooltip = 'toolbar.download'; /** * Handles clicking / pressing the button, and opens a new window with the user documentation. diff --git a/react/features/toolbox/components/HelpButton.js b/react/features/toolbox/components/HelpButton.js index 2115c20ab7..53b7c2cad9 100644 --- a/react/features/toolbox/components/HelpButton.js +++ b/react/features/toolbox/components/HelpButton.js @@ -24,6 +24,7 @@ class HelpButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.help'; icon = IconHelp; label = 'toolbar.help'; + tooltip = 'toolbar.help'; /** * Handles clicking / pressing the button, and opens a new window with the user documentation. diff --git a/react/features/toolbox/components/MuteEveryoneButton.js b/react/features/toolbox/components/MuteEveryoneButton.js index e759a391dd..226dfdaf81 100644 --- a/react/features/toolbox/components/MuteEveryoneButton.js +++ b/react/features/toolbox/components/MuteEveryoneButton.js @@ -30,6 +30,7 @@ class MuteEveryoneButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryone'; icon = IconMuteEveryone; label = 'toolbar.muteEveryone'; + tooltip = 'toolbar.muteEveryone'; /** * Handles clicking / pressing the button, and opens a confirmation dialog. diff --git a/react/features/toolbox/components/MuteEveryonesVideoButton.js b/react/features/toolbox/components/MuteEveryonesVideoButton.js index 04d3cb2cd7..a13f4452cf 100644 --- a/react/features/toolbox/components/MuteEveryonesVideoButton.js +++ b/react/features/toolbox/components/MuteEveryonesVideoButton.js @@ -30,6 +30,7 @@ class MuteEveryonesVideoButton extends AbstractButton { accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo'; icon = IconMuteVideoEveryone; label = 'toolbar.muteEveryonesVideo'; + tooltip = 'toolbar.muteEveryonesVideo'; /** * Handles clicking / pressing the button, and opens a confirmation dialog. diff --git a/react/features/toolbox/components/web/FullscreenButton.js b/react/features/toolbox/components/web/FullscreenButton.js new file mode 100644 index 0000000000..40e3a7328a --- /dev/null +++ b/react/features/toolbox/components/web/FullscreenButton.js @@ -0,0 +1,103 @@ +// @flow + +import { translate } from '../../../base/i18n'; +import { IconExitFullScreen, IconFullScreen } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; + +type Props = AbstractButtonProps & { + + /** + * Whether or not the app is currently in full screen. + */ + _fullScreen: boolean, + + /** + * External handler for click action. + */ + handleClick: Function +}; + +/** + * Implementation of a button for toggling fullscreen state. + */ +class FullscreenButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.fullScreen'; + label = 'toolbar.enterFullScreen'; + toggledLabel = 'toolbar.exitFullScreen' + + /** + * Retrieves icon dynamically. + */ + get icon() { + if (this._isToggled()) { + return IconExitFullScreen; + } + + return IconFullScreen; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set icon(value) { + return value; + } + + /** + * Retrieves icon dynamically. + */ + get tooltip() { + if (this._isToggled()) { + return 'toolbar.exitFullScreen'; + } + + return 'toolbar.enterFullScreen'; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._fullScreen; + } +} + +/** + * Function that maps parts of Redux state tree into component props. + * + * @param {Object} state - Redux state. + * @returns {Object} + */ +const mapStateToProps = state => { + return { + _fullScreen: state['features/toolbox'].fullScreen + }; +}; + +export default translate(connect(mapStateToProps)(FullscreenButton)); diff --git a/react/features/toolbox/components/web/OverflowMenuProfileItem.js b/react/features/toolbox/components/web/OverflowMenuProfileItem.js deleted file mode 100644 index 85bc8e442f..0000000000 --- a/react/features/toolbox/components/web/OverflowMenuProfileItem.js +++ /dev/null @@ -1,148 +0,0 @@ -// @flow - -import React, { Component } from 'react'; - -import { Avatar } from '../../../base/avatar'; -import { translate } from '../../../base/i18n'; -import { getLocalParticipant } from '../../../base/participants'; -import { connect } from '../../../base/redux'; - -declare var interfaceConfig: Object; - -/** - * The type of the React {@code Component} props of - * {@link OverflowMenuProfileItem}. - */ -type Props = { - - /** - * The redux representation of the local participant. - */ - _localParticipant: Object, - - /** - * Whether the button support clicking or not. - */ - _unclickable: boolean, - - /** - * The callback to invoke when {@code OverflowMenuProfileItem} is - * clicked. - */ - onClick: Function, - - /** - * Invoked to obtain translated strings. - */ - t: Function -}; - -/** - * A React {@code Component} for displaying a link with a profile avatar as an - * icon. - * - * @extends Component - */ -class OverflowMenuProfileItem extends Component { - /** - * Initializes a new {@code OverflowMenuProfileItem} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props: Props) { - super(props); - - // Bind event handler so it is only bound once for every instance. - this._onClick = this._onClick.bind(this); - this._onKeyPress = this._onKeyPress.bind(this); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { _localParticipant, _unclickable, t } = this.props; - const classNames = `overflow-menu-item ${ - _unclickable ? 'unclickable' : ''}`; - let displayName; - - if (_localParticipant && _localParticipant.name) { - displayName = _localParticipant.name; - } else { - displayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME; - } - - return ( -
  • - - - - - { displayName } - -
  • - ); - } - - _onClick: () => void; - - /** - * Invokes an on click callback if clicking is allowed. - * - * @returns {void} - */ - _onClick() { - if (!this.props._unclickable) { - this.props.onClick(); - } - } - - _onKeyPress: (Object) => void; - - /** - * KeyPress handler for accessibility. - * - * @param {Object} e - The key event to handle. - * - * @returns {void} - */ - _onKeyPress(e) { - if (!this.props._unclickable && (e.key === ' ' || e.key === 'Enter')) { - e.preventDefault(); - this.props.onClick(); - } - } -} - -/** - * Maps (parts of) the Redux state to the associated - * {@code OverflowMenuProfileItem} component's props. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _localParticipant: Object, - * _unclickable: boolean - * }} - */ -function _mapStateToProps(state) { - return { - _localParticipant: getLocalParticipant(state), - _unclickable: state['features/base/config'].disableProfile - || !interfaceConfig.SETTINGS_SECTIONS.includes('profile') - }; -} - -export default translate(connect(_mapStateToProps)(OverflowMenuProfileItem)); diff --git a/react/features/toolbox/components/web/ProfileButton.js b/react/features/toolbox/components/web/ProfileButton.js new file mode 100644 index 0000000000..884607e8a6 --- /dev/null +++ b/react/features/toolbox/components/web/ProfileButton.js @@ -0,0 +1,123 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../../analytics'; +import { translate } from '../../../base/i18n'; +import { getLocalParticipant } from '../../../base/participants'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { openSettingsDialog, SETTINGS_TABS } from '../../../settings'; + +import ProfileButtonAvatar from './ProfileButtonAvatar'; + +/** + * The type of the React {@code Component} props of {@link ProfileButton}. + */ +type Props = AbstractButtonProps & { + + /** + * The redux representation of the local participant. + */ + _localParticipant: Object, + + /** + * Whether the button support clicking or not. + */ + _unclickable: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +declare var interfaceConfig: Object; + +/** + * Implementation of a button for opening profile dialog. + */ +class ProfileButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.profile'; + icon = ProfileButtonAvatar; + + /** + * Retrieves the label. + */ + get label() { + const { _localParticipant } = this.props; + let displayName; + + if (_localParticipant && _localParticipant.name) { + displayName = _localParticipant.name; + } else { + displayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME; + } + + return displayName; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set label(value) { + return value; + } + + /** + * Retrieves the tooltip. + */ + get tooltip() { + return this.label; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + const { dispatch, _unclickable } = this.props; + + if (!_unclickable) { + sendAnalytics(createToolbarEvent('profile')); + dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE)); + } + } + + /** + * Indicates whether the button should be disabled or not. + * + * @protected + * @returns {void} + */ + _isDisabled() { + return this.props._unclickable; + } +} + +/** + * Function that maps parts of Redux state tree into component props. + * + * @param {Object} state - Redux state. + * @returns {Object} + */ +const mapStateToProps = state => { + return { + _localParticipant: getLocalParticipant(state), + _unclickable: !interfaceConfig.SETTINGS_SECTIONS.includes('profile'), + customClass: 'profile-button-avatar' + }; +}; + +export default translate(connect(mapStateToProps)(ProfileButton)); diff --git a/react/features/toolbox/components/web/ProfileButtonAvatar.js b/react/features/toolbox/components/web/ProfileButtonAvatar.js new file mode 100644 index 0000000000..b4b57f491d --- /dev/null +++ b/react/features/toolbox/components/web/ProfileButtonAvatar.js @@ -0,0 +1,63 @@ +// @flow + +import React, { Component } from 'react'; + +import { Avatar } from '../../../base/avatar'; +import { translate } from '../../../base/i18n'; +import { getLocalParticipant } from '../../../base/participants'; +import { connect } from '../../../base/redux'; + +/** + * The type of the React {@code Component} props of + * {@link ProfileButtonAvatar}. + */ +type Props = { + + /** + * The redux representation of the local participant. + */ + _localParticipant: Object, + +}; + +/** + * A React {@code Component} for displaying a profile avatar as an + * icon. + * + * @extends Component + */ +class ProfileButtonAvatar extends Component { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _localParticipant } = this.props; + + return ( + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated + * {@code ProfileButtonAvatar} component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _localParticipant: Object, + * }} + */ +function _mapStateToProps(state) { + return { + _localParticipant: getLocalParticipant(state) + }; +} + +export default translate(connect(_mapStateToProps)(ProfileButtonAvatar)); diff --git a/react/features/toolbox/components/web/RaiseHandButton.js b/react/features/toolbox/components/web/RaiseHandButton.js new file mode 100644 index 0000000000..4ebdb36135 --- /dev/null +++ b/react/features/toolbox/components/web/RaiseHandButton.js @@ -0,0 +1,83 @@ +// @flow + +import { translate } from '../../../base/i18n'; +import { IconRaisedHand } from '../../../base/icons'; +import { getLocalParticipant } from '../../../base/participants'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; + +type Props = AbstractButtonProps & { + + /** + * Whether or not the local participant's hand is raised. + */ + _raisedHand: boolean, + + /** + * External handler for click action. + */ + handleClick: Function +}; + +/** + * Implementation of a button for toggling raise hand functionality. + */ +class RaiseHandButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand'; + icon = IconRaisedHand + label = 'toolbar.raiseYourHand'; + toggledLabel = 'toolbar.lowerYourHand' + + /** + * Retrieves tooltip dynamically. + */ + get tooltip() { + return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand'; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The value. + */ + set tooltip(value) { + return value; + } + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._raisedHand; + } +} + +/** + * Function that maps parts of Redux state tree into component props. + * + * @param {Object} state - Redux state. + * @returns {Object} + */ +const mapStateToProps = state => { + const localParticipant = getLocalParticipant(state); + + return { + _raisedHand: localParticipant.raisedHand + }; +}; + +export default translate(connect(mapStateToProps)(RaiseHandButton)); diff --git a/react/features/toolbox/components/web/Separator.js b/react/features/toolbox/components/web/Separator.js new file mode 100644 index 0000000000..f5e4fc0922 --- /dev/null +++ b/react/features/toolbox/components/web/Separator.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default () =>
    ; diff --git a/react/features/toolbox/components/web/ShareDesktopButton.js b/react/features/toolbox/components/web/ShareDesktopButton.js new file mode 100644 index 0000000000..78dc4335cb --- /dev/null +++ b/react/features/toolbox/components/web/ShareDesktopButton.js @@ -0,0 +1,139 @@ +// @flow + +import { translate } from '../../../base/i18n'; +import { IconShareDesktop } from '../../../base/icons'; +import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; +import { getParticipants } from '../../../base/participants'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { getLocalVideoTrack } from '../../../base/tracks'; +import { isScreenAudioShared } from '../../../screen-share'; + +type Props = AbstractButtonProps & { + + /** + * Whether or not screensharing is initialized. + */ + _desktopSharingEnabled: boolean, + + /** + * The tooltip key to use when screensharing is disabled. Or undefined + * if non to be shown and the button to be hidden. + */ + _desktopSharingDisabledTooltipKey: string, + + /** + * Whether or not the local participant is screensharing. + */ + _screensharing: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function, + + /** + * External handler for click action. + */ + handleClick: Function +}; + +/** + * Implementation of a button for sharing desktop / windows. + */ +class ShareDesktopButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen'; + label = 'toolbar.startScreenSharing'; + icon = IconShareDesktop; + toggledLabel = 'toolbar.stopScreenSharing' + tooltip = 'toolbar.accessibilityLabel.shareYourScreen'; + + /** + * Retrieves tooltip dynamically. + */ + get tooltip() { + const { _desktopSharingDisabledTooltipKey, _desktopSharingEnabled, _screensharing } = this.props; + + if (_desktopSharingEnabled) { + if (_screensharing) { + return 'toolbar.stopScreenSharing'; + } + + return 'toolbar.startScreenSharing'; + } + + return _desktopSharingDisabledTooltipKey; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The icon value. + */ + set tooltip(value) { + return value; + } + + /** + * Handles clicking / pressing the button, and opens the appropriate dialog. + * + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._screensharing; + } + + /** + * Indicates whether this button is in disabled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isDisabled() { + return !this.props._desktopSharingEnabled; + } +} + +/** + * Function that maps parts of Redux state tree into component props. +* + * @param {Object} state - Redux state. + * @returns {Object} + */ +const mapStateToProps = state => { + const localVideo = getLocalVideoTrack(state['features/base/tracks']); + let desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); + const { enableFeaturesBasedOnToken } = state['features/base/config']; + + let desktopSharingDisabledTooltipKey; + + if (enableFeaturesBasedOnToken) { + // we enable desktop sharing if any participant already have this + // feature enabled + desktopSharingEnabled = getParticipants(state) + .find(({ features = {} }) => + String(features['screen-sharing']) === 'true') !== undefined; + desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled'; + } + + return { + _desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey, + _desktopSharingEnabled: desktopSharingEnabled, + _screensharing: (localVideo && localVideo.videoType === 'desktop') || isScreenAudioShared(state) + }; +}; + +export default translate(connect(mapStateToProps)(ShareDesktopButton)); diff --git a/react/features/toolbox/components/web/ToggleCameraButton.js b/react/features/toolbox/components/web/ToggleCameraButton.js deleted file mode 100644 index 60c2a07924..0000000000 --- a/react/features/toolbox/components/web/ToggleCameraButton.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow - -import { translate } from '../../../base/i18n'; -import { IconCameraRefresh } from '../../../base/icons'; -import { connect } from '../../../base/redux'; -import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; -import { isLocalCameraTrackMuted, isToggleCameraEnabled, toggleCamera } from '../../../base/tracks'; - -/** - * The type of the React {@code Component} props of {@link ToggleCameraButton}. - */ -type Props = AbstractButtonProps & { - - /** - * Whether the current conference is in audio only mode or not. - */ - _audioOnly: boolean, - - /** - * Whether video is currently muted or not. - */ - _videoMuted: boolean, - - /** - * The Redux dispatch function. - */ - dispatch: Function -}; - -/** - * An implementation of a button for toggling the camera facing mode. - */ -class ToggleCameraButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera'; - icon = IconCameraRefresh; - label = 'toolbar.toggleCamera'; - - /** - * Handles clicking/pressing the button. - * - * @returns {void} - */ - _handleClick() { - this.props.dispatch(toggleCamera()); - } - - /** - * Whether this button is disabled or not. - * - * @returns {boolean} - */ - _isDisabled() { - return this.props._audioOnly || this.props._videoMuted; - } -} - -/** - * Maps (parts of) the redux state to the associated props for the - * {@code ToggleCameraButton} component. - * - * @param {Object} state - The Redux state. - * @returns {Props} - */ -function mapStateToProps(state): Object { - const { enabled: audioOnly } = state['features/base/audio-only']; - const tracks = state['features/base/tracks']; - - return { - _audioOnly: Boolean(audioOnly), - _videoMuted: isLocalCameraTrackMuted(tracks), - visible: isToggleCameraEnabled(state) - }; -} - -export default translate(connect(mapStateToProps)(ToggleCameraButton)); diff --git a/react/features/toolbox/components/web/ToolbarButton.js b/react/features/toolbox/components/web/ToolbarButton.js index ccd0469042..cec25b2cad 100644 --- a/react/features/toolbox/components/web/ToolbarButton.js +++ b/react/features/toolbox/components/web/ToolbarButton.js @@ -11,7 +11,7 @@ import type { Props as AbstractToolbarButtonProps } /** * The type of the React {@code Component} props of {@link ToolbarButton}. */ -type Props = AbstractToolbarButtonProps & { +export type Props = AbstractToolbarButtonProps & { /** * The text to display in the tooltip. diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index bb49328d10..7552b9be99 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -14,19 +14,6 @@ import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; import { openDialog, toggleDialog } from '../../../base/dialog'; import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n'; -import { - IconChat, - IconCodeBlock, - IconDeviceDocument, - IconExitFullScreen, - IconFeedback, - IconFullScreen, - IconParticipants, - IconPresentation, - IconRaisedHand, - IconRec, - IconShareDesktop -} from '../../../base/icons'; import JitsiMeetJS from '../../../base/lib-jitsi-meet'; import { getLocalParticipant, @@ -34,19 +21,20 @@ import { raiseHand } from '../../../base/participants'; import { connect } from '../../../base/redux'; -import { OverflowMenuItem } from '../../../base/toolbox/components'; import { getLocalVideoTrack } from '../../../base/tracks'; import { isVpaasMeeting } from '../../../billing-counter/functions'; -import { ChatCounter, toggleChat } from '../../../chat'; -import { EmbedMeetingDialog } from '../../../embed-meeting'; +import { toggleChat } from '../../../chat'; +import { ChatButton } from '../../../chat/components'; +import { EmbedMeetingButton } from '../../../embed-meeting'; import { SharedDocumentButton } from '../../../etherpad'; -import { openFeedbackDialog } from '../../../feedback'; -import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts'; -import { LocalRecordingInfoDialog } from '../../../local-recording'; +import { FeedbackButton } from '../../../feedback'; +import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts'; +import { LocalRecordingButton } from '../../../local-recording'; import { close as closeParticipantsPane, open as openParticipantsPane } from '../../../participants-pane/actions'; +import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { LiveStreamButton, @@ -59,13 +47,9 @@ import { startScreenShareFlow } from '../../../screen-share/'; import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton'; -import { - SETTINGS_TABS, - SettingsButton, - openSettingsDialog -} from '../../../settings'; +import { SettingsButton } from '../../../settings'; import { SharedVideoButton } from '../../../shared-video/components'; -import { SpeakerStats } from '../../../speaker-stats'; +import { SpeakerStatsButton } from '../../../speaker-stats'; import { ClosedCaptionButton } from '../../../subtitles'; @@ -74,10 +58,7 @@ import { shouldDisplayTileView, toggleTileView } from '../../../video-layout'; -import { - OverflowMenuVideoQualityItem, - VideoQualityDialog -} from '../../../video-quality'; +import { VideoQualityDialog, VideoQualityButton } from '../../../video-quality/components'; import { VideoBackgroundButton } from '../../../virtual-background'; import { toggleBackgroundEffect } from '../../../virtual-background/actions'; import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants'; @@ -88,7 +69,8 @@ import { setToolbarHovered, showToolbox } from '../../actions'; -import { getToolbarAdditionalButtons, isToolboxVisible } from '../../functions'; +import { THRESHOLDS } from '../../constants'; +import { isToolboxVisible } from '../../functions'; import DownloadButton from '../DownloadButton'; import HangupButton from '../HangupButton'; import HelpButton from '../HelpButton'; @@ -96,13 +78,14 @@ import MuteEveryoneButton from '../MuteEveryoneButton'; import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton'; import AudioSettingsButton from './AudioSettingsButton'; +import FullscreenButton from './FullscreenButton'; import OverflowMenuButton from './OverflowMenuButton'; -import OverflowMenuProfileItem from './OverflowMenuProfileItem'; -import ToggleCameraButton from './ToggleCameraButton'; -import ToolbarButton from './ToolbarButton'; +import ProfileButton from './ProfileButton'; +import RaiseHandButton from './RaiseHandButton'; +import Separator from './Separator'; +import ShareDesktopButton from './ShareDesktopButton'; import VideoSettingsButton from './VideoSettingsButton'; - /** * The type of the React {@code Component} props of {@link Toolbox}. */ @@ -179,17 +162,6 @@ type Props = { */ _localParticipantID: String, - /** - * The subsection of Redux state for local recording - */ - _localRecState: Object, - - /** - * The value for how the conference is locked (or undefined if not locked) - * as defined by room-lock constants. - */ - _locked: boolean, - /** * The JitsiLocalTrack to display. */ @@ -280,18 +252,12 @@ class Toolbox extends Component { this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this); this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this); this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this); - this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this); this._onToolbarToggleParticipantsPane = this._onToolbarToggleParticipantsPane.bind(this); - this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this); - this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this); - this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this); this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this); this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this); this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this); - this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this); this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this); this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this); - this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this); this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this); this._onEscKey = this._onEscKey.bind(this); } @@ -429,50 +395,6 @@ class Toolbox extends Component { _overflowMenuVisible && dispatch(setOverflowMenuVisible(false)); } - /** - * Callback invoked to display {@code FeedbackDialog}. - * - * @private - * @returns {void} - */ - _doOpenFeedback() { - const { _conference } = this.props; - - this.props.dispatch(openFeedbackDialog(_conference)); - } - - /** - * Callback invoked to display {@code FeedbackDialog}. - * - * @private - * @returns {void} - */ - _doOpenEmbedMeeting() { - this.props.dispatch(openDialog(EmbedMeetingDialog)); - } - - /** - * Dispatches an action to display {@code KeyboardShortcuts}. - * - * @private - * @returns {void} - */ - _doOpenKeyboardShorcuts() { - this.props.dispatch(openKeyboardShortcutsDialog()); - } - - /** - * Callback invoked to display {@code SpeakerStats}. - * - * @private - * @returns {void} - */ - _doOpenSpeakerStats() { - this.props.dispatch(openDialog(SpeakerStats, { - conference: this.props._conference - })); - } - /** * Dispatches an action to open the video quality dialog. * @@ -505,16 +427,6 @@ class Toolbox extends Component { this.props.dispatch(setFullScreen(fullScreen)); } - /** - * Dispatches an action to show or hide the profile edit panel. - * - * @private - * @returns {void} - */ - _doToggleProfile() { - this.props.dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE)); - } - /** * Dispatches an action to toggle the local participant's raised hand state. * @@ -539,8 +451,31 @@ class Toolbox extends Component { * @returns {void} */ _doToggleScreenshare() { - if (this.props._desktopSharingEnabled) { - this.props.dispatch(startScreenShareFlow()); + const { + _backgroundType, + _desktopSharingEnabled, + _localVideo, + _virtualSource, + dispatch + } = this.props; + + if (_backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE) { + const noneOptions = { + enabled: false, + backgroundType: VIRTUAL_BACKGROUND_TYPE.NONE, + selectedThumbnail: VIRTUAL_BACKGROUND_TYPE.NONE, + backgroundEffectEnabled: false + }; + + _virtualSource.dispose(); + + dispatch(toggleBackgroundEffect(noneOptions, _localVideo)); + + return; + } + + if (_desktopSharingEnabled) { + dispatch(startScreenShareFlow()); } } @@ -564,6 +499,273 @@ class Toolbox extends Component { this.props.dispatch(toggleTileView()); } + /** + * Returns all buttons that could be rendered. + * + * @param {Object} state - The redux state. + * @returns {Object} The button maps mainMenuButtons and overflowMenuButtons. + */ + _getAllButtons() { + const { + _feedbackConfigured, + _isMobile, + _screensharing + } = this.props; + + const microphone = { + key: 'microphone', + Content: AudioSettingsButton, + group: 0 + }; + + const camera = { + key: 'camera', + Content: VideoSettingsButton, + group: 0 + }; + + const profile = this._isProfileVisible() && { + key: 'profile', + Content: ProfileButton, + group: 1 + }; + + const chat = { + key: 'chat', + Content: ChatButton, + handleClick: this._onToolbarToggleChat, + group: 2 + }; + + const desktop = this._showDesktopSharingButton() && { + key: 'desktop', + Content: ShareDesktopButton, + handleClick: this._onToolbarToggleScreenshare, + group: 2 + }; + + const raisehand = { + key: 'raisehand', + Content: RaiseHandButton, + handleClick: this._onToolbarToggleRaiseHand, + group: 2 + }; + + const participants = { + key: 'participants-pane', + Content: ParticipantsPaneButton, + handleClick: this._onToolbarToggleParticipantsPane, + group: 2 + }; + + const tileview = { + key: 'tileview', + Content: TileViewButton, + group: 2 + }; + + const videoQuality = { + key: 'videoquality', + Content: VideoQualityButton, + handleClick: this._onToolbarOpenVideoQuality, + group: 2 + }; + + const fullscreen = !_isMobile && { + key: 'fullscreen', + Content: FullscreenButton, + handleClick: this._onToolbarToggleFullScreen, + group: 2 + }; + + const security = { + key: 'security', + Content: SecurityDialogButton, + group: 2 + }; + + const info = { + key: 'info', + Content: SecurityDialogButton, + group: 2 + }; + + const cc = { + key: 'closedcaptions', + Content: ClosedCaptionButton, + group: 2 + }; + + const recording = { + key: 'recording', + Content: RecordButton, + group: 2 + }; + + const localRecording = { + key: 'localrecording', + Content: LocalRecordingButton, + group: 2 + }; + + const muteEveryone = { + key: 'mute-everyone', + Content: MuteEveryoneButton, + group: 2 + }; + + const muteVideoEveryone = { + key: 'mute-video-everyone', + Content: MuteEveryonesVideoButton, + group: 2 + }; + + const livestreaming = { + key: 'livestreaming', + Content: LiveStreamButton, + group: 2 + }; + + const shareVideo = { + key: 'sharedvideo', + Content: SharedVideoButton, + group: 3 + }; + + const shareAudio = this._showAudioSharingButton() && { + key: 'shareaudio', + Content: ShareAudioButton, + group: 3 + }; + + const etherpad = { + key: 'etherpad', + Content: SharedDocumentButton, + group: 3 + }; + + const virtualBackground = !_screensharing && checkBlurSupport() && { + key: 'select-background', + Content: VideoBackgroundButton, + group: 3 + }; + + const speakerStats = { + key: 'stats', + Content: SpeakerStatsButton, + group: 3 + }; + + const settings = { + key: 'settings', + Content: SettingsButton, + group: 4 + }; + + const shortcuts = !_isMobile && keyboardShortcut.getEnabled() && { + key: 'shortcuts', + Content: KeyboardShortcutsButton, + group: 4 + }; + + const embed = this._isEmbedMeetingVisible() && { + key: 'embedmeeting', + Content: EmbedMeetingButton, + group: 4 + }; + + const feedback = _feedbackConfigured && { + key: 'feedback', + Content: FeedbackButton, + group: 4 + }; + + const download = { + key: 'download', + Content: DownloadButton, + group: 4 + }; + + const help = { + key: 'help', + Content: HelpButton, + group: 4 + }; + + return { + microphone, + camera, + profile, + desktop, + chat, + raisehand, + participants, + tileview, + videoQuality, + fullscreen, + security, + info, + cc, + recording, + localRecording, + muteEveryone, + muteVideoEveryone, + livestreaming, + shareVideo, + shareAudio, + etherpad, + virtualBackground, + speakerStats, + settings, + shortcuts, + embed, + feedback, + download, + help + }; + } + + /** + * Returns all buttons that need to be rendered. + * + * @param {Object} state - The redux state. + * @returns {Object} The visible buttons arrays . + */ + _getVisibleButtons() { + const { + _clientWidth, + _shouldShowButton + } = this.props; + + + const buttons = this._getAllButtons(); + const isHangupVisible = _shouldShowButton('hangup'); + const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width) + || THRESHOLDS[THRESHOLDS.length - 1]; + let sliceIndex = order.length + 2; + + const keys = Object.keys(buttons); + + const filtered = [ + ...order.map(key => buttons[key]), + ...Object.values(buttons).filter((button, index) => !order.includes(keys[index])) + ].filter(Boolean).filter(({ key }) => _shouldShowButton(key)); + + if (isHangupVisible) { + sliceIndex -= 1; + } + + // This implies that the overflow button will be displayed, so save some space for it. + if (sliceIndex < filtered.length) { + sliceIndex -= 1; + } + + return { + mainMenuButtons: filtered.slice(0, sliceIndex), + overflowMenuButtons: filtered.slice(sliceIndex) + }; + } + _onMouseOut: () => void; /** @@ -728,11 +930,12 @@ class Toolbox extends Component { * @returns {void} */ _onShortcutToggleScreenshare() { - sendAnalytics(createToolbarEvent( - 'screen.sharing', - { - enable: !this.props._screensharing - })); + sendAnalytics(createShortcutEvent( + 'toggle.screen.sharing', + ACTION_SHORTCUT_TRIGGERED, + { + enable: !this.props._screensharing + })); this._doToggleScreenshare(); } @@ -749,22 +952,6 @@ class Toolbox extends Component { this.props.dispatch(showToolbox()); } } - - _onToolbarOpenFeedback: () => void; - - /** - * Creates an analytics toolbar event and dispatches an action for toggling - * display of feedback. - * - * @private - * @returns {void} - */ - _onToolbarOpenFeedback() { - sendAnalytics(createToolbarEvent('feedback')); - - this._doOpenFeedback(); - } - _onToolbarToggleParticipantsPane: () => void; /** @@ -783,51 +970,6 @@ class Toolbox extends Component { } } - _onToolbarOpenKeyboardShortcuts: () => void; - - /** - * Creates an analytics toolbar event and dispatches an action for opening - * the modal for showing available keyboard shortcuts. - * - * @private - * @returns {void} - */ - _onToolbarOpenKeyboardShortcuts() { - sendAnalytics(createToolbarEvent('shortcuts')); - - this._doOpenKeyboardShorcuts(); - } - - _onToolbarOpenEmbedMeeting: () => void; - - /** - * Creates an analytics toolbar event and dispatches an action for opening - * the embed meeting modal. - * - * @private - * @returns {void} - */ - _onToolbarOpenEmbedMeeting() { - sendAnalytics(createToolbarEvent('embed.meeting')); - - this._doOpenEmbedMeeting(); - } - - _onToolbarOpenSpeakerStats: () => void; - - /** - * Creates an analytics toolbar event and dispatches an action for opening - * the speaker stats modal. - * - * @private - * @returns {void} - */ - _onToolbarOpenSpeakerStats() { - sendAnalytics(createToolbarEvent('speaker.stats')); - - this._doOpenSpeakerStats(); - } - _onToolbarOpenVideoQuality: () => void; /** @@ -881,21 +1023,6 @@ class Toolbox extends Component { this._doToggleFullScreen(); } - _onToolbarToggleProfile: () => void; - - /** - * Creates an analytics toolbar event and dispatches an action for showing - * or hiding the profile edit panel. - * - * @private - * @returns {void} - */ - _onToolbarToggleProfile() { - sendAnalytics(createToolbarEvent('profile')); - - this._doToggleProfile(); - } - _onToolbarToggleRaiseHand: () => void; /** @@ -923,25 +1050,7 @@ class Toolbox extends Component { * @returns {void} */ _onToolbarToggleScreenshare() { - if (this.props._backgroundType === VIRTUAL_BACKGROUND_TYPE.DESKTOP_SHARE) { - const noneOptions = { - enabled: false, - backgroundType: VIRTUAL_BACKGROUND_TYPE.NONE, - selectedThumbnail: VIRTUAL_BACKGROUND_TYPE.NONE, - backgroundEffectEnabled: false - }; - - this.props._virtualSource.dispose(); - - this.props.dispatch(toggleBackgroundEffect(noneOptions, this.props._localVideo)); - - return; - } - if (!this.props._desktopSharingEnabled) { - return; - } - - sendAnalytics(createShortcutEvent( + sendAnalytics(createToolbarEvent( 'toggle.screen.sharing', ACTION_SHORTCUT_TRIGGERED, { enable: !this.props._screensharing })); @@ -950,18 +1059,18 @@ class Toolbox extends Component { this._doToggleScreenshare(); } - _onToolbarOpenLocalRecordingInfoDialog: () => void; - /** - * Opens the {@code LocalRecordingInfoDialog}. + * Returns true if the audio sharing button should be visible and + * false otherwise. * - * @private - * @returns {void} + * @returns {boolean} */ - _onToolbarOpenLocalRecordingInfoDialog() { - sendAnalytics(createToolbarEvent('local.recording')); + _showAudioSharingButton() { + const { + _desktopSharingEnabled + } = this.props; - this.props.dispatch(openDialog(LocalRecordingInfoDialog)); + return _desktopSharingEnabled && isScreenAudioSupported(); } /** @@ -976,11 +1085,7 @@ class Toolbox extends Component { _desktopSharingDisabledTooltipKey } = this.props; - return ( - (_desktopSharingEnabled - || _desktopSharingDisabledTooltipKey) - && this.props._shouldShowButton('desktop') - ); + return _desktopSharingEnabled || _desktopSharingDisabledTooltipKey; } /** @@ -990,8 +1095,7 @@ class Toolbox extends Component { */ _isEmbedMeetingVisible() { return !this.props._isVpaasMeeting - && !this.props._isMobile - && this.props._shouldShowButton('embedmeeting'); + && !this.props._isMobile; } /** @@ -1000,310 +1104,7 @@ class Toolbox extends Component { * @returns {boolean} */ _isProfileVisible() { - return !this.props._isProfileDisabled && this.props._shouldShowButton('profile'); - } - - /** - * Renders the list elements of the overflow menu. - * - * @private - * @param {Array} additionalButtons - Additional buttons to be displayed. - * @returns {Array} - */ - _renderOverflowMenuContent(additionalButtons: Array>) { - const { - _desktopSharingEnabled, - _feedbackConfigured, - _fullScreen, - _isMobile, - _screensharing, - t - } = this.props; - - const group1 = [ - ...additionalButtons, - - this.props._shouldShowButton('toggle-camera') - && , - this.props._shouldShowButton('videoquality') - && , - this.props._shouldShowButton('fullscreen') - && !_isMobile - && , - (this.props._shouldShowButton('security') || this.props._shouldShowButton('info')) - && , - this.props._shouldShowButton('closedcaptions') - && , - this.props._shouldShowButton('recording') - && , - this.props._shouldShowButton('localrecording') - && , - this.props._shouldShowButton('mute-everyone') - && , - this.props._shouldShowButton('mute-video-everyone') - && , - this.props._shouldShowButton('livestreaming') - && - ]; - - const group2 = [ - this.props._shouldShowButton('sharedvideo') - && , - this.props._shouldShowButton('shareaudio') - && _desktopSharingEnabled - && isScreenAudioSupported() - && , - this.props._shouldShowButton('etherpad') - && , - (this.props._shouldShowButton('select-background') || this.props._shouldShowButton('videobackgroundblur')) - && , - this.props._shouldShowButton('stats') - && - ]; - - - return [ - this._isProfileVisible() - && , - this._isProfileVisible() - &&
    , - - ...group1, - group1.some(Boolean) - &&
    , - - ...group2, - group2.some(Boolean) - &&
    , - - this.props._shouldShowButton('settings') - && , - this.props._shouldShowButton('shortcuts') - && !_isMobile - && keyboardShortcut.getEnabled() - && , - this._isEmbedMeetingVisible() - && , - this.props._shouldShowButton('feedback') - && _feedbackConfigured - && , - this.props._shouldShowButton('download') - && , - this.props._shouldShowButton('help') - && - ]; - } - - /** - * Returns the buttons to be displayed in main or the overflow menu. - * - * @param {Set} buttons - A set containing the buttons to be displayed - * in the toolbar beside the main audio/video & hanugup. - * @returns {Object} - */ - _getAdditionalButtons(buttons) { - const { - _chatOpen, - _desktopSharingEnabled, - _desktopSharingDisabledTooltipKey, - _screensharing, - t - } = this.props; - - const overflowMenuAdditionalButtons = []; - const mainMenuAdditionalButtons = []; - - if (this._showDesktopSharingButton()) { - buttons.has('desktop') - ? mainMenuAdditionalButtons.push() - : overflowMenuAdditionalButtons.push(); - } - - if (this.props._shouldShowButton('chat')) { - buttons.has('chat') - ? mainMenuAdditionalButtons.push(
    - - -
    ) : overflowMenuAdditionalButtons.push(); - } - - if (this.props._shouldShowButton('raisehand')) { - const raisedHand = this.props._raisedHand || false; - - buttons.has('raisehand') - ? mainMenuAdditionalButtons.push() - : overflowMenuAdditionalButtons.push(); - } - - if (this.props._shouldShowButton('participants-pane') || this.props._shouldShowButton('invite')) { - buttons.has('participants-pane') - ? mainMenuAdditionalButtons.push( - ) - : overflowMenuAdditionalButtons.push( - - ); - } - - if (this.props._shouldShowButton('tileview')) { - buttons.has('tileview') - ? mainMenuAdditionalButtons.push( - ) - : overflowMenuAdditionalButtons.push( - ); - } - - return { - mainMenuAdditionalButtons, - overflowMenuAdditionalButtons - }; - } - - /** - * Renders the Audio controlling button. - * - * @returns {ReactElement} - */ - _renderAudioButton() { - return this.props._shouldShowButton('microphone') - ? - : null; - } - - /** - * Renders the Video controlling button. - * - * @returns {ReactElement} - */ - _renderVideoButton() { - return this.props._shouldShowButton('camera') - ? - : null; + return !this.props._isProfileDisabled; } /** @@ -1313,23 +1114,15 @@ class Toolbox extends Component { */ _renderToolboxContent() { const { - _clientWidth, _isMobile, _overflowMenuVisible, t } = this.props; - const buttonSet = getToolbarAdditionalButtons(_clientWidth, _isMobile); const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; - const showOverflowMenuButton = buttonSet.has('overflow'); const containerClassName = `toolbox-content${_isMobile ? ' toolbox-content-mobile' : ''}`; - let overflowMenuAdditionalButtons = []; - let mainMenuAdditionalButtons = []; - - if (showOverflowMenuButton) { - ({ overflowMenuAdditionalButtons, mainMenuAdditionalButtons } = this._getAdditionalButtons(buttonSet)); - } + const { mainMenuButtons, overflowMenuButtons } = this._getVisibleButtons(); return (
    @@ -1339,23 +1132,40 @@ class Toolbox extends Component { onMouseOut = { this._onMouseOut } onMouseOver = { this._onMouseOver }>
    - { this._renderAudioButton() } - { this._renderVideoButton() } - { mainMenuAdditionalButtons } - { showOverflowMenuButton && - - } + {mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && ( + ))} + + {Boolean(overflowMenuButtons.length) && ( + + + + )} + { * @returns {{}} */ function _mapStateToProps(state) { - const { conference, locked } = state['features/base/conference']; + const { conference } = state['features/base/conference']; let desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); const { callStatsID, @@ -1387,7 +1197,6 @@ function _mapStateToProps(state) { overflowMenuVisible } = state['features/toolbox']; const localParticipant = getLocalParticipant(state); - const localRecordingStates = state['features/local-recording']; const localVideo = getLocalVideoTrack(state['features/base/tracks']); const { clientWidth } = state['features/base/responsive-ui']; @@ -1419,8 +1228,6 @@ function _mapStateToProps(state) { _tileViewEnabled: shouldDisplayTileView(state), _localParticipantID: localParticipant.id, _localVideo: localVideo, - _localRecState: localRecordingStates, - _locked: locked, _overflowMenuVisible: overflowMenuVisible, _participantsPaneOpen: getParticipantsPaneOpen(state), _raisedHand: localParticipant.raisedHand, diff --git a/react/features/toolbox/constants.js b/react/features/toolbox/constants.js new file mode 100644 index 0000000000..ba6ea423bd --- /dev/null +++ b/react/features/toolbox/constants.js @@ -0,0 +1,29 @@ +/** + * Thresholds for displaying toolbox buttons + */ +export const THRESHOLDS = [ + { + width: 520, + order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants', 'tileview' ] + }, + { + width: 470, + order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants' ] + }, + { + width: 420, + order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants' ] + }, + { + width: 370, + order: [ 'microphone', 'camera', 'chat', 'participants' ] + }, + { + width: 320, + order: [ 'microphone', 'camera', 'chat' ] + }, + { + width: 270, + order: [ 'microphone', 'camera' ] + } +]; diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 1bf4a42a97..28ea635a0e 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -3,70 +3,6 @@ import { getToolbarButtons } from '../base/config'; import { hasAvailableDevices } from '../base/devices'; -const WIDTH = { - FIT_9_ICONS: 520, - FIT_8_ICONS: 470, - FIT_7_ICONS: 420, - FIT_6_ICONS: 370, - FIT_5_ICONS: 320, - FIT_4_ICONS: 280 -}; - -/** - * Returns a set of button names to be displayed in the toolbox, based on the screen width and platform. - * - * @param {number} width - The width of the screen. - * @param {number} isMobile - The device is a mobile one. - * @returns {Set} The button set. - */ -export function getToolbarAdditionalButtons(width: number, isMobile: boolean): Set { - let buttons = []; - - switch (true) { - case width >= WIDTH.FIT_9_ICONS: { - buttons = isMobile - ? [ 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ] - : [ 'desktop', 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ]; - break; - } - - case width >= WIDTH.FIT_8_ICONS: { - buttons = [ 'desktop', 'chat', 'raisehand', 'participants-pane', 'overflow' ]; - break; - } - - case width >= WIDTH.FIT_7_ICONS: { - buttons = [ 'desktop', 'chat', 'participants-pane', 'overflow' ]; - break; - } - - case width >= WIDTH.FIT_6_ICONS: { - buttons = [ 'chat', 'participants-pane', 'overflow' ]; - break; - } - - case width >= WIDTH.FIT_5_ICONS: { - buttons = [ 'chat', 'overflow' ]; - break; - } - - case width >= WIDTH.FIT_4_ICONS: { - buttons = isMobile - ? [ 'chat', 'overflow' ] - : [ 'overflow' ]; - break; - } - - default: { - buttons = isMobile - ? [ 'chat', 'overflow' ] - : []; - } - } - - return new Set(buttons); -} - /** * Helper for getting the height of the toolbox. * diff --git a/react/features/video-layout/components/TileViewButton.js b/react/features/video-layout/components/TileViewButton.js index 0ec2feb4af..243ce289d8 100644 --- a/react/features/video-layout/components/TileViewButton.js +++ b/react/features/video-layout/components/TileViewButton.js @@ -52,13 +52,13 @@ class TileViewButton extends AbstractButton { */ _handleClick() { const { _tileViewEnabled, dispatch } = this.props; + const value = !_tileViewEnabled; sendAnalytics(createToolbarEvent( 'tileview.button', { - 'is_enabled': _tileViewEnabled + 'is_enabled': value })); - const value = !_tileViewEnabled; logger.debug(`Tile view ${value ? 'enable' : 'disable'}`); dispatch(setTileView(value)); diff --git a/react/features/video-quality/components/VideoQualityButton.native.js b/react/features/video-quality/components/VideoQualityButton.native.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js b/react/features/video-quality/components/VideoQualityButton.web.js similarity index 54% rename from react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js rename to react/features/video-quality/components/VideoQualityButton.web.js index 60260b71c7..87979b99dc 100644 --- a/react/features/video-quality/components/OverflowMenuVideoQualityItem.web.js +++ b/react/features/video-quality/components/VideoQualityButton.web.js @@ -1,16 +1,14 @@ // @flow -import React, { Component } from 'react'; - import { translate } from '../../base/i18n'; import { - Icon, IconVideoQualityAudioOnly, IconVideoQualityHD, IconVideoQualityLD, IconVideoQualitySD } from '../../base/icons'; import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; import { VIDEO_QUALITY_LEVELS } from '../constants'; import { findNearestQualityLevel } from '../functions'; @@ -29,9 +27,9 @@ const VIDEO_QUALITY_TO_ICON = { /** * The type of the React {@code Component} props of - * {@link OverflowMenuVideoQualityItem}. + * {@link VideoQualityButton}. */ -type Props = { +type Props = AbstractButtonProps & { /** * Whether or not audio only mode is currently enabled. @@ -45,9 +43,9 @@ type Props = { _videoQuality: number, /** - * Callback to invoke when {@link OverflowMenuVideoQualityItem} is clicked. + * Callback to invoke when {@link VideoQualityButton} is clicked. */ - onClick: Function, + handleClick: Function, /** * Invoked to obtain translated strings. @@ -62,72 +60,50 @@ type Props = { * * @extends Component */ -class OverflowMenuVideoQualityItem extends Component { +class VideoQualityButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.callQuality'; + label = 'toolbar.callQuality'; + tooltip = 'toolbar.callQuality'; /** - * Initializes a new {@code OverflowMenuVideoQualityItem} instance. - * - * @param {*} props - The read-only properties with which the new instance - * is to be initialized. + * Dynamically retrieves the icon. */ - constructor(props) { - super(props); - - // Bind event handler so it is only bound once for every instance. - this._onKeyPress = this._onKeyPress.bind(this); - } - - _onKeyPress: (Object) => void; - - /** - * KeyPress handler for accessibility. - * - * @param {Object} e - The key event to handle. - * - * @returns {void} - */ - _onKeyPress(e) { - if (this.props.onClick && (e.key === ' ' || e.key === 'Enter')) { - e.preventDefault(); - this.props.onClick(); - } - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { + get icon() { const { _audioOnly, _videoQuality } = this.props; + const videoQualityLevel = findNearestQualityLevel(_videoQuality); + const icon = _audioOnly || !videoQualityLevel ? IconVideoQualityAudioOnly : VIDEO_QUALITY_TO_ICON[videoQualityLevel]; - return ( -
  • - - - - - { this.props.t('toolbar.callQuality') } - -
  • - ); + return icon; + } + + /** + * Required by linter due to AbstractButton overwritten prop being writable. + * + * @param {string} value - The icon value. + */ + set icon(value) { + return value; + } + + /** + * Handles clicking / pressing the button. + * + * @override + * @protected + * @returns {void} + */ + _handleClick() { + this.props.handleClick(); } } /** * Maps (parts of) the Redux state to the associated props for the - * {@code OverflowMenuVideoQualityItem} component. + * {@code VideoQualityButton} component. * * @param {Object} state - The Redux state. * @private @@ -144,4 +120,4 @@ function _mapStateToProps(state) { } export default translate( - connect(_mapStateToProps)(OverflowMenuVideoQualityItem)); + connect(_mapStateToProps)(VideoQualityButton)); diff --git a/react/features/video-quality/components/index.js b/react/features/video-quality/components/index.js index a12857be5f..6f188e6448 100644 --- a/react/features/video-quality/components/index.js +++ b/react/features/video-quality/components/index.js @@ -1,6 +1,4 @@ -export { - default as OverflowMenuVideoQualityItem -} from './OverflowMenuVideoQualityItem'; +export { default as VideoQualityButton } from './VideoQualityButton.web'; export { default as VideoQualityDialog } from './VideoQualityDialog'; export { default as VideoQualityLabel } from './VideoQualityLabel'; export {