feat(toolbox) allow any toolbox button to be displayed in main toolbar (#9488)

* feat(toolbox) allow any toolbox button to be displayed as main

fixes the previous behaviour where only a certain set of buttons were whitelisted for being displayed in the main toolbar

* code review

* code review - fix avatar icon position
This commit is contained in:
Avram Tudor 2021-07-08 16:42:07 +03:00 committed by GitHub
parent 074a783bd9
commit 62c78950cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1390 additions and 976 deletions

View File

@ -278,6 +278,10 @@
}
}
.profile-button-avatar {
align-items: center;
}
/**
* START of fade in animation for main toolbar
*/

View File

@ -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'
];

View File

@ -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<Props, *> {
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 (
<div
className = 'toolbar-button-with-badge'
key = 'chatcontainer'>
{super.render()}
<ChatCounter />
</div>
);
}
}
/**
* 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));

View File

@ -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';

View File

@ -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<Props, *> {
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));

View File

@ -1 +1,2 @@
export { default as EmbedMeetingButton } from './EmbedMeetingButton';
export { default as EmbedMeetingDialog } from './EmbedMeetingDialog';

View File

@ -32,6 +32,26 @@ class SharedDocumentButton extends AbstractButton<Props, *> {
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.
*

View File

@ -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<Props, *> {
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));

View File

@ -1 +1,2 @@
export { default as FeedbackButton } from './FeedbackButton';
export { default as FeedbackDialog } from './FeedbackDialog';

View File

@ -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<Props, *> {
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));

View File

@ -1 +1,2 @@
export { default as KeyboardShortcutsButton } from './KeyboardShortcutsButton';
export { default as KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';

View File

@ -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<Props> {
class LocalRecording extends AbstractButton<Props, *> {
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 (
<ToolbarButton
accessibilityLabel
= { t('toolbar.accessibilityLabel.localRecording') }
icon = { IconRec }
onClick = { this._onClick }
toggled = { isDialogShown }
tooltip = { t('localRecording.dialogTitle') } />
);
}
_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));

View File

@ -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<Props, *> {
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));

View File

@ -6,4 +6,5 @@ export * from './MeetingParticipantItem';
export * from './MeetingParticipantList';
export * from './ParticipantItem';
export * from './ParticipantsPane';
export * from './ParticipantsPaneButton';
export * from './RaisedHandIndicator';

View File

@ -39,6 +39,7 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
icon = IconSecurityOff;
label = 'toolbar.security';
toggledIcon = IconSecurityOn;
tooltip = 'toolbar.security';
/**
* Handles clicking / pressing the button, and opens / closes the appropriate dialog.

View File

@ -31,6 +31,7 @@ class SettingsButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.Settings';
icon = IconSettings;
label = 'toolbar.Settings';
tooltip = 'toolbar.Settings';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.

View File

@ -37,9 +37,28 @@ class SharedVideoButton extends AbstractButton<Props, *> {
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.
*

View File

@ -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<Props, *> {
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));

View File

@ -1 +1,2 @@
export { default as SpeakerStatsButton } from './SpeakerStatsButton';
export { default as SpeakerStats } from './SpeakerStats';

View File

@ -23,6 +23,7 @@ class DownloadButton extends AbstractButton<Props, *> {
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.

View File

@ -24,6 +24,7 @@ class HelpButton extends AbstractButton<Props, *> {
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.

View File

@ -30,6 +30,7 @@ class MuteEveryoneButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryone';
icon = IconMuteEveryone;
label = 'toolbar.muteEveryone';
tooltip = 'toolbar.muteEveryone';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.

View File

@ -30,6 +30,7 @@ class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
icon = IconMuteVideoEveryone;
label = 'toolbar.muteEveryonesVideo';
tooltip = 'toolbar.muteEveryonesVideo';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.

View File

@ -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<Props, *> {
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));

View File

@ -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<Props> {
/**
* 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 (
<li
aria-label = { t('toolbar.accessibilityLabel.profile') }
className = { classNames }
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<span className = 'overflow-menu-item-icon'>
<Avatar
participantId = { _localParticipant.id }
size = { 20 } />
</span>
<span className = 'profile-text'>
{ displayName }
</span>
</li>
);
}
_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));

View File

@ -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<Props, *> {
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));

View File

@ -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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _localParticipant } = this.props;
return (
<Avatar
participantId = { _localParticipant.id }
size = { 20 } />
);
}
}
/**
* 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));

View File

@ -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<Props, *> {
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));

View File

@ -0,0 +1,3 @@
import React from 'react';
export default () => <hr className = 'overflow-menu-hr' />;

View File

@ -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<Props, *> {
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));

View File

@ -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<Props, any> {
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));

View File

@ -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.

File diff suppressed because it is too large Load Diff

View File

@ -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' ]
}
];

View File

@ -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<string> {
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.
*

View File

@ -52,13 +52,13 @@ class TileViewButton<P: Props> extends AbstractButton<P, *> {
*/
_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));

View File

@ -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<Props> {
class VideoQualityButton extends AbstractButton<Props, *> {
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 (
<li
aria-label = { this.props.t('toolbar.accessibilityLabel.callQuality') }
className = 'overflow-menu-item'
onClick = { this.props.onClick }
onKeyPress = { this._onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<span className = 'overflow-menu-item-icon'>
<Icon src = { icon } />
</span>
<span className = 'profile-text'>
{ this.props.t('toolbar.callQuality') }
</span>
</li>
);
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));

View File

@ -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 {