From 7aca5e71b9ac4c419fd497608029da5f2f36f93b Mon Sep 17 00:00:00 2001 From: robertpin Date: Fri, 22 Oct 2021 16:23:52 +0300 Subject: [PATCH] refactor(participants-pane) Refactored with reusable components Created Reusable components for: - ListItem - used by participants list and lobby participants list - ContextMenu - used by participant context menu and advanced moderation context menu - Quick action button - used by quick action buttons on participant list items Moved participants custom theme to base/components/themes Created reusable button component for all participants pane buttons (Invite, Mute All, More) Moved web components to web folder Moved all styles from Styled Components to JSS Fixed accessibility labels for some buttons Removed unused code Updated all styles to use theme tokens --- lang/main.json | 1 + modules/UI/videolayout/LargeVideoManager.js | 2 +- .../components/buttons/QuickActionButton.js | 67 +++ .../components/context-menu/ContextMenu.js | 175 ++++++++ .../context-menu/ContextMenuItemGroup.js | 136 ++++++ .../particpants-pane-list/ListItem.js | 208 ++++++++++ .../themes/participantsPaneTheme.json | 10 + react/features/base/responsive-ui/actions.js | 2 +- .../components/AskToUnmuteButton.js | 46 --- .../components/FooterContextMenu.js | 162 -------- .../components/ParticipantQuickAction.js | 78 ---- .../components/web/FooterButton.js | 57 +++ .../components/web/FooterContextMenu.js | 145 +++++++ .../components/web/InviteButton.js | 26 +- .../components/web/LobbyParticipantItem.js | 34 +- .../web/LobbyParticipantQuickAction.js | 70 ++++ .../web/MeetingParticipantContextMenu.js | 298 +++++-------- .../components/web/MeetingParticipantItem.js | 12 +- .../components/web/MeetingParticipants.js | 29 +- .../web/ParticipantActionEllipsis.js | 43 ++ .../components/web/ParticipantItem.js | 109 ++--- .../web/ParticipantPaneBaseButton.js | 98 +++++ .../components/web/ParticipantQuickAction.js | 103 +++++ .../components/web/ParticipantsPane.js | 176 +++++--- .../{ => web}/ParticipantsPaneButton.js | 8 +- .../components/web/RaisedHandIndicator.js | 30 +- .../participants-pane/components/web/index.js | 2 +- .../components/web/styled.js | 390 ------------------ react/features/participants-pane/functions.js | 9 +- react/features/participants-pane/theme.json | 13 - .../features/toolbox/components/web/Drawer.js | 15 +- .../toolbox/components/web/Toolbox.js | 2 +- 32 files changed, 1507 insertions(+), 1049 deletions(-) create mode 100644 react/features/base/components/buttons/QuickActionButton.js create mode 100644 react/features/base/components/context-menu/ContextMenu.js create mode 100644 react/features/base/components/context-menu/ContextMenuItemGroup.js create mode 100644 react/features/base/components/particpants-pane-list/ListItem.js create mode 100644 react/features/base/components/themes/participantsPaneTheme.json delete mode 100644 react/features/participants-pane/components/AskToUnmuteButton.js delete mode 100644 react/features/participants-pane/components/FooterContextMenu.js delete mode 100644 react/features/participants-pane/components/ParticipantQuickAction.js create mode 100644 react/features/participants-pane/components/web/FooterButton.js create mode 100644 react/features/participants-pane/components/web/FooterContextMenu.js create mode 100644 react/features/participants-pane/components/web/LobbyParticipantQuickAction.js create mode 100644 react/features/participants-pane/components/web/ParticipantActionEllipsis.js create mode 100644 react/features/participants-pane/components/web/ParticipantPaneBaseButton.js create mode 100644 react/features/participants-pane/components/web/ParticipantQuickAction.js rename react/features/participants-pane/components/{ => web}/ParticipantsPaneButton.js (89%) delete mode 100644 react/features/participants-pane/components/web/styled.js delete mode 100644 react/features/participants-pane/theme.json diff --git a/lang/main.json b/lang/main.json index 19c6c070a..83168203c 100644 --- a/lang/main.json +++ b/lang/main.json @@ -620,6 +620,7 @@ "blockEveryoneMicCamera": "Block everyone's mic and camera", "invite": "Invite Someone", "askUnmute": "Ask to unmute", + "moreModerationActions": "More moderation options", "mute": "Mute", "muteAll": "Mute all", "muteEveryoneElse": "Mute everyone else", diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index eeac501b3..91adcac44 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -8,6 +8,7 @@ import { Provider } from 'react-redux'; import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics'; import { Avatar } from '../../../react/features/base/avatar'; +import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json'; import { i18next } from '../../../react/features/base/i18n'; import { JitsiParticipantConnectionStatus @@ -20,7 +21,6 @@ import { updateKnownLargeVideoResolution } from '../../../react/features/large-video/actions'; import { getParticipantsPaneOpen } from '../../../react/features/participants-pane/functions'; -import theme from '../../../react/features/participants-pane/theme.json'; import { PresenceLabel } from '../../../react/features/presence-status'; import { shouldDisplayTileView } from '../../../react/features/video-layout'; /* eslint-enable no-unused-vars */ diff --git a/react/features/base/components/buttons/QuickActionButton.js b/react/features/base/components/buttons/QuickActionButton.js new file mode 100644 index 000000000..b8e377c56 --- /dev/null +++ b/react/features/base/components/buttons/QuickActionButton.js @@ -0,0 +1,67 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +type Props = { + + /** + * Label used for accessibility. + */ + accessibilityLabel: string, + + /** + * Additional class name for custom styles. + */ + className: string, + + /** + * Children of the component. + */ + children: string | React$Node, + + /** + * Click handler + */ + onClick: Function, + + /** + * Data test id. + */ + testId?: string +} + +const useStyles = makeStyles(theme => { + return { + button: { + backgroundColor: theme.palette.action01, + color: theme.palette.text01, + borderRadius: `${theme.shape.borderRadius}px`, + ...theme.typography.labelBold, + lineHeight: `${theme.typography.labelBold.lineHeight}px`, + padding: '8px 12px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + border: 0, + + '&:hover': { + backgroundColor: theme.palette.action01Hover + } + } + }; +}); + +const QuickActionButton = ({ accessibilityLabel, className, children, onClick, testId }: Props) => { + const styles = useStyles(); + + return (); +}; + +export default QuickActionButton; diff --git a/react/features/base/components/context-menu/ContextMenu.js b/react/features/base/components/context-menu/ContextMenu.js new file mode 100644 index 000000000..1e78e1233 --- /dev/null +++ b/react/features/base/components/context-menu/ContextMenu.js @@ -0,0 +1,175 @@ +// @flow +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { getComputedOuterHeight } from '../../../participants-pane/functions'; +import { Drawer, JitsiPortal } from '../../../toolbox/components/web'; +import { showOverflowDrawer } from '../../../toolbox/functions.web'; +import participantsPaneTheme from '../themes/participantsPaneTheme.json'; + +type Props = { + + /** + * Children of the context menu. + */ + children: React$Node, + + /** + * Class name for context menu. Used to overwrite default styles. + */ + className?: string, + + /** + * The entity for which the context menu is displayed. + */ + entity?: Object, + + /** + * Whether or not the menu is hidden. Used to overwrite the internal isHidden. + */ + hidden?: boolean, + + /** + * Whether or not drawer should be open. + */ + isDrawerOpen: boolean, + + /** + * Target elements against which positioning calculations are made + */ + offsetTarget?: HTMLElement, + + /** + * Callback for click on an item in the menu + */ + onClick?: Function, + + /** + * Callback for drawer close. + */ + onDrawerClose: Function, + + /** + * Callback for the mouse entering the component + */ + onMouseEnter?: Function, + + /** + * Callback for the mouse leaving the component + */ + onMouseLeave: Function +}; + +const useStyles = makeStyles(theme => { + return { + contextMenu: { + backgroundColor: theme.palette.ui02, + borderRadius: `${theme.shape.borderRadius / 2}px`, + boxShadow: '0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25)', + color: theme.palette.text01, + ...theme.typography.bodyShortRegular, + lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px`, + marginTop: `${(participantsPaneTheme.panePadding * 2) + theme.typography.bodyShortRegular.fontSize}px`, + position: 'absolute', + right: `${participantsPaneTheme.panePadding}px`, + top: 0, + zIndex: 2 + }, + + contextMenuHidden: { + pointerEvents: 'none', + visibility: 'hidden' + }, + + drawer: { + + '& > div': { + ...theme.typography.bodyShortRegularLarge, + lineHeight: `${theme.typography.bodyShortRegularLarge.lineHeight}px`, + + '& svg': { + fill: theme.palette.icon01 + } + }, + + '& > *:first-child': { + paddingTop: '15px!important' + } + } + }; +}); + +const ContextMenu = ({ + children, + className, + entity, + hidden, + isDrawerOpen, + offsetTarget, + onClick, + onDrawerClose, + onMouseEnter, + onMouseLeave +}: Props) => { + const [ isHidden, setIsHidden ] = useState(true); + const containerRef = useRef(null); + const styles = useStyles(); + const _overflowDrawer = useSelector(showOverflowDrawer); + + useLayoutEffect(() => { + if (_overflowDrawer) { + return; + } + if (entity && offsetTarget + && containerRef.current + && offsetTarget?.offsetParent + && offsetTarget.offsetParent instanceof HTMLElement + ) { + const { current: container } = containerRef; + const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget; + const outerHeight = getComputedOuterHeight(container); + + container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop + ? `${offsetTop - outerHeight}` + : `${offsetTop}`; + + setIsHidden(false); + } else { + setIsHidden(true); + } + }, [ entity, offsetTarget, _overflowDrawer ]); + + useEffect(() => { + if (hidden !== undefined) { + setIsHidden(hidden); + } + }, [ hidden ]); + + return _overflowDrawer + ? + +
+ {children} +
+
+
+ :
+ {children} +
+ ; +}; + +export default ContextMenu; diff --git a/react/features/base/components/context-menu/ContextMenuItemGroup.js b/react/features/base/components/context-menu/ContextMenuItemGroup.js new file mode 100644 index 000000000..ac97a4f39 --- /dev/null +++ b/react/features/base/components/context-menu/ContextMenuItemGroup.js @@ -0,0 +1,136 @@ +// @flow +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { showOverflowDrawer } from '../../../toolbox/functions.web'; +import { Icon } from '../../icons'; + +export type Action = { + + /** + * Label used for accessibility. + */ + accessibilityLabel: string, + + /** + * CSS class name used for custom styles. + */ + className?: string, + + /** + * Custom icon. If used, the icon prop is ignored. + * Used to allow custom children instead of just the default icons. + */ + customIcon?: React$Node, + + /** + * Id of the action container. + */ + id?: string, + + /** + * Default icon for action. + */ + icon?: Function, + + /** + * Click handler. + */ + onClick?: Function, + + /** + * Action text. + */ + text: string +} + +type Props = { + + /** + * List of actions in this group. + */ + actions?: Array, + + /** + * The children of the component + */ + children?: React$Node, +}; + + +const useStyles = makeStyles(theme => { + return { + contextMenuItemGroup: { + '&:not(:empty)': { + padding: `${theme.spacing(2)}px 0` + }, + + '& + &:not(:empty)': { + borderTop: `1px solid ${theme.palette.ui04}` + } + }, + + contextMenuItem: { + alignItems: 'center', + cursor: 'pointer', + display: 'flex', + minHeight: '40px', + padding: '10px 16px', + boxSizing: 'border-box', + + '& > *:not(:last-child)': { + marginRight: `${theme.spacing(3)}px` + }, + + '&:hover': { + backgroundColor: theme.palette.ui04 + } + }, + + contextMenuItemDrawer: { + padding: '12px 16px' + }, + + contextMenuItemIcon: { + '& svg': { + fill: theme.palette.icon01 + } + } + }; +}); + +const ContextMenuItemGroup = ({ + actions, + children +}: Props) => { + const styles = useStyles(); + const _overflowDrawer = useSelector(showOverflowDrawer); + + return ( +
+ {children} + {actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => ( +
+ {customIcon ? customIcon + : icon && } + {text} +
+ ))} +
+ ); +}; + +export default ContextMenuItemGroup; diff --git a/react/features/base/components/particpants-pane-list/ListItem.js b/react/features/base/components/particpants-pane-list/ListItem.js new file mode 100644 index 000000000..ff26ce818 --- /dev/null +++ b/react/features/base/components/particpants-pane-list/ListItem.js @@ -0,0 +1,208 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +import { ACTION_TRIGGER } from '../../../participants-pane/constants'; +import participantsPaneTheme from '../themes/participantsPaneTheme.json'; + +type Props = { + + /** + * List item actions. + */ + actions: React$Node, + + /** + * Icon to be displayed on the list item. (Avatar for participants) + */ + icon: React$Node, + + /** + * Id of the container. + */ + id: string, + + /** + * Whether or not the actions should be hidden. + */ + hideActions?: Boolean, + + /** + * Indicators to be displayed on the list item. + */ + indicators?: React$Node, + + /** + * Whether or not the item is highlighted. + */ + isHighlighted?: boolean, + + /** + * Click handler. + */ + onClick: Function, + + /** + * Mouse leave handler. + */ + onMouseLeave: Function, + + /** + * Text children to be displayed on the list item. + */ + textChildren: React$Node | string, + + /** + * The actions trigger. Can be Hover or Permanent. + */ + trigger: string + +} + +const useStyles = makeStyles(theme => { + return { + container: { + alignItems: 'center', + color: theme.palette.text01, + display: 'flex', + ...theme.typography.bodyShortRegular, + lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px`, + margin: `0 -${participantsPaneTheme.panePadding}px`, + padding: `0 ${participantsPaneTheme.panePadding}px`, + position: 'relative', + boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)', + + '&:hover': { + backgroundColor: theme.palette.action02Active, + + '& .indicators': { + display: 'none' + }, + + '& .actions': { + display: 'flex', + boxShadow: `-15px 0px 10px -5px ${theme.palette.action02Active}`, + backgroundColor: theme.palette.action02Active + } + }, + + [`@media(max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: { + ...theme.typography.bodyShortRegularLarge, + lineHeight: `${theme.typography.bodyShortRegularLarge.lineHeight}px`, + padding: `${theme.spacing(2)}px ${participantsPaneTheme.panePadding}px` + } + }, + + highlighted: { + backgroundColor: theme.palette.action02Active + }, + + detailsContainer: { + display: 'flex', + alignItems: 'center', + flex: 1, + height: '100%', + overflow: 'hidden', + position: 'relative' + }, + + name: { + display: 'flex', + flex: 1, + marginRight: `${theme.spacing(2)}px`, + overflow: 'hidden', + flexDirection: 'column', + justifyContent: 'flex-start' + }, + + indicators: { + display: 'flex', + justifyContent: 'flex-end', + + '& > *': { + alignItems: 'center', + display: 'flex', + justifyContent: 'center' + }, + + '& > *:not(:last-child)': { + marginRight: `${theme.spacing(2)}px` + }, + + '& .jitsi-icon': { + padding: '3px' + } + }, + + indicatorsHidden: { + display: 'none' + }, + + actionsContainer: { + display: 'none', + boxShadow: `-15px 0px 10px -5px ${theme.palette.action02Active}`, + backgroundColor: theme.palette.action02Active + }, + + actionsPermanent: { + display: 'flex', + boxShadow: `-15px 0px 10px -5px ${theme.palette.ui01}`, + backgroundColor: theme.palette.ui01 + }, + + actionsVisible: { + display: 'flex', + boxShadow: `-15px 0px 10px -5px ${theme.palette.action02Active}`, + backgroundColor: theme.palette.action02Active + } + }; +}); + +const ListItem = ({ + actions, + icon, + id, + hideActions = false, + indicators, + isHighlighted, + onClick, + onMouseLeave, + textChildren, + trigger +}: Props) => { + const styles = useStyles(); + + return ( +
+
{icon}
+
+
+ {textChildren} +
+ {indicators && ( +
+ {indicators} +
+ )} + {!hideActions && ( +
+ {actions} +
+ )} +
+
+ ); +}; + +export default ListItem; diff --git a/react/features/base/components/themes/participantsPaneTheme.json b/react/features/base/components/themes/participantsPaneTheme.json new file mode 100644 index 000000000..24716f72b --- /dev/null +++ b/react/features/base/components/themes/participantsPaneTheme.json @@ -0,0 +1,10 @@ +{ + "colors": { + "moderationDisabled": "#E54B4B" + }, + "headerSize": 60, + "ignoredChildClassName": "ignore-child", + "panePadding": 16, + "participantsPaneWidth": 315, + "MD_BREAKPOINT": "580px" +} diff --git a/react/features/base/responsive-ui/actions.js b/react/features/base/responsive-ui/actions.js index a792280a8..b87fb2da0 100644 --- a/react/features/base/responsive-ui/actions.js +++ b/react/features/base/responsive-ui/actions.js @@ -5,7 +5,7 @@ import type { Dispatch } from 'redux'; import { CHAT_SIZE } from '../../chat/constants'; import { getParticipantsPaneOpen } from '../../participants-pane/functions'; -import theme from '../../participants-pane/theme.json'; +import theme from '../components/themes/participantsPaneTheme.json'; import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_CONTEXT_MENU_OPEN, SET_REDUCED_UI } from './actionTypes'; import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants'; diff --git a/react/features/participants-pane/components/AskToUnmuteButton.js b/react/features/participants-pane/components/AskToUnmuteButton.js deleted file mode 100644 index 48fab2c15..000000000 --- a/react/features/participants-pane/components/AskToUnmuteButton.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow - -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; - -import { approveParticipant } from '../../av-moderation/actions'; - -import { QuickActionButton } from './web/styled'; - -type Props = { - - /** - * The translated ask unmute text. - */ - askUnmuteText: string, - - /** - * Participant participantID. - */ - participantID: string, -} - -/** - * Component used to display the `ask to unmute` button. - * - * @param {Object} participant - Participant reference. - * @returns {React$Element<'button'>} - */ -export default function AskToUnmuteButton({ askUnmuteText, participantID }: Props) { - const dispatch = useDispatch(); - const askToUnmute = useCallback(() => { - dispatch(approveParticipant(participantID)); - }, [ dispatch, participantID ]); - - return ( - - { askUnmuteText } - - ); -} diff --git a/react/features/participants-pane/components/FooterContextMenu.js b/react/features/participants-pane/components/FooterContextMenu.js deleted file mode 100644 index 55b559e98..000000000 --- a/react/features/participants-pane/components/FooterContextMenu.js +++ /dev/null @@ -1,162 +0,0 @@ -// @flow - -import { makeStyles } from '@material-ui/core/styles'; -import clsx from 'clsx'; -import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; - -import { - requestDisableAudioModeration, - requestDisableVideoModeration, - requestEnableAudioModeration, - requestEnableVideoModeration -} from '../../av-moderation/actions'; -import { - isEnabled as isAvModerationEnabled, - isSupported as isAvModerationSupported -} from '../../av-moderation/functions'; -import { openDialog } from '../../base/dialog'; -import { Icon, IconCheck, IconVideoOff } from '../../base/icons'; -import { MEDIA_TYPE } from '../../base/media'; -import { - getParticipantCount, - isEveryoneModerator -} from '../../base/participants'; -import { MuteEveryonesVideoDialog } from '../../video-menu/components'; - -import { - ContextMenu, - ContextMenuItem, - ContextMenuItemGroup -} from './web/styled'; - -const useStyles = makeStyles(() => { - return { - contextMenu: { - bottom: 'auto', - margin: '0', - padding: '8px 0', - right: 0, - top: '-8px', - transform: 'translateY(-100%)', - width: '283px' - }, - drawer: { - width: '100%', - top: 'auto', - bottom: 0, - transform: 'none', - position: 'relative', - - '& > div': { - lineHeight: '32px' - } - }, - text: { - color: '#C2C2C2', - padding: '10px 16px' - }, - paddedAction: { - marginLeft: '36px' - } - }; -}); - -type Props = { - - /** - * Whether the menu is displayed inside a drawer. - */ - inDrawer?: boolean, - - /** - * Callback for the mouse leaving this item. - */ - onMouseLeave?: Function -}; - -export const FooterContextMenu = ({ inDrawer, onMouseLeave }: Props) => { - const dispatch = useDispatch(); - const isModerationSupported = useSelector(isAvModerationSupported()); - const allModerators = useSelector(isEveryoneModerator); - const participantCount = useSelector(getParticipantCount); - const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO)); - const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO)); - - const { t } = useTranslation(); - - const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]); - - const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]); - - const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]); - - const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]); - - const classes = useStyles(); - - const muteAllVideo = useCallback( - () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]); - - return ( - - - - - { t('participantsPane.actions.stopEveryonesVideo') } - - - {isModerationSupported && (participantCount === 1 || !allModerators) ? ( - -
- {t('participantsPane.actions.allow')} -
- { isAudioModerationEnabled ? ( - - - {t('participantsPane.actions.audioModeration') } - - - ) : ( - - - {t('participantsPane.actions.audioModeration') } - - )} - { isVideoModerationEnabled ? ( - - - {t('participantsPane.actions.videoModeration')} - - - ) : ( - - - {t('participantsPane.actions.videoModeration')} - - )} -
- ) : undefined - } -
- ); -}; diff --git a/react/features/participants-pane/components/ParticipantQuickAction.js b/react/features/participants-pane/components/ParticipantQuickAction.js deleted file mode 100644 index 6ccf3e62f..000000000 --- a/react/features/participants-pane/components/ParticipantQuickAction.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import React from 'react'; - -import { QUICK_ACTION_BUTTON } from '../constants'; - -import AskToUnmuteButton from './AskToUnmuteButton'; -import { QuickActionButton } from './web/styled'; - -type Props = { - - /** - * The translated ask unmute aria label. - */ - ariaLabel?: boolean, - - /** - * The translated "ask unmute" text. - */ - askUnmuteText: string, - - /** - * The type of button to be displayed. - */ - buttonType: string, - - /** - * Callback used to open a confirmation dialog for audio muting. - */ - muteAudio: Function, - - /** - * Label for mute participant button. - */ - muteParticipantButtonText: string, - - /** - * The ID of the participant. - */ - participantID: string, -} - -/** - * Component used to display mute/ask to unmute button. - * - * @param {Props} props - The props of the component. - * @returns {React$Element<'button'>} - */ -export default function ParticipantQuickAction({ - askUnmuteText, - buttonType, - muteAudio, - muteParticipantButtonText, - participantID -}: Props) { - switch (buttonType) { - case QUICK_ACTION_BUTTON.MUTE: { - return ( - - { muteParticipantButtonText } - - ); - } - case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: { - return ( - - ); - } - default: { - return null; - } - } -} diff --git a/react/features/participants-pane/components/web/FooterButton.js b/react/features/participants-pane/components/web/FooterButton.js new file mode 100644 index 000000000..302d74aa4 --- /dev/null +++ b/react/features/participants-pane/components/web/FooterButton.js @@ -0,0 +1,57 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +import ParticipantPaneBaseButton from './ParticipantPaneBaseButton'; + +type Props = { + + /** + * Label used for accessibility. + */ + accessibilityLabel: String, + + /** + * Children of the component. + */ + children: string | React$Node, + + /** + * button id. + */ + id?: string, + + /** + * Whether or not the button is icon button (no text). + */ + isIconButton?: boolean, + + /** + * Click handler + */ + onClick: Function +} + +const useStyles = makeStyles(theme => { + return { + button: { + padding: `${theme.spacing(2)}px` + } + }; +}); + +const FooterButton = ({ accessibilityLabel, children, id, isIconButton = false, onClick }: Props) => { + const styles = useStyles(); + + return ( + {children} + + ); +}; + +export default FooterButton; diff --git a/react/features/participants-pane/components/web/FooterContextMenu.js b/react/features/participants-pane/components/web/FooterContextMenu.js new file mode 100644 index 000000000..83025a34e --- /dev/null +++ b/react/features/participants-pane/components/web/FooterContextMenu.js @@ -0,0 +1,145 @@ +// @flow + +import { makeStyles } from '@material-ui/core/styles'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import { + requestDisableAudioModeration, + requestDisableVideoModeration, + requestEnableAudioModeration, + requestEnableVideoModeration +} from '../../../av-moderation/actions'; +import { + isEnabled as isAvModerationEnabled, + isSupported as isAvModerationSupported +} from '../../../av-moderation/functions'; +import ContextMenu from '../../../base/components/context-menu/ContextMenu'; +import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup'; +import { openDialog } from '../../../base/dialog'; +import { IconCheck, IconVideoOff } from '../../../base/icons'; +import { MEDIA_TYPE } from '../../../base/media'; +import { + getParticipantCount, + isEveryoneModerator +} from '../../../base/participants'; +import { MuteEveryonesVideoDialog } from '../../../video-menu/components'; + +const useStyles = makeStyles(theme => { + return { + contextMenu: { + bottom: 'auto', + margin: '0', + right: 0, + top: '-8px', + transform: 'translateY(-100%)', + width: '283px' + }, + + text: { + color: theme.palette.text02, + padding: '10px 16px', + height: '40px', + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + boxSizing: 'border-box' + }, + + indentedLabel: { + '& > span': { + marginLeft: '36px' + } + } + }; +}); + +type Props = { + + /** + * Whether the menu is open. + */ + isOpen: boolean, + + /** + * Drawer close callback. + */ + onDrawerClose: Function, + + /** + * Callback for the mouse leaving this item. + */ + onMouseLeave?: Function +}; + +export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props) => { + const dispatch = useDispatch(); + const isModerationSupported = useSelector(isAvModerationSupported()); + const allModerators = useSelector(isEveryoneModerator); + const participantCount = useSelector(getParticipantCount); + const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO)); + const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO)); + + const { t } = useTranslation(); + + const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]); + + const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]); + + const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]); + + const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]); + + const classes = useStyles(); + + const muteAllVideo = useCallback( + () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]); + + const actions = [ + { + accessibilityLabel: t('participantsPane.actions.audioModeration'), + className: isAudioModerationEnabled ? classes.indentedLabel : '', + id: isAudioModerationEnabled + ? 'participants-pane-context-menu-stop-audio-moderation' + : 'participants-pane-context-menu-start-audio-moderation', + icon: !isAudioModerationEnabled && IconCheck, + onClick: isAudioModerationEnabled ? disableAudioModeration : enableAudioModeration, + text: t('participantsPane.actions.audioModeration') + }, { + accessibilityLabel: t('participantsPane.actions.videoModeration'), + className: isVideoModerationEnabled ? classes.indentedLabel : '', + id: isVideoModerationEnabled + ? 'participants-pane-context-menu-stop-video-moderation' + : 'participants-pane-context-menu-start-video-moderation', + icon: !isVideoModerationEnabled && IconCheck, + onClick: isVideoModerationEnabled ? disableVideoModeration : enableVideoModeration, + text: t('participantsPane.actions.videoModeration') + } + ]; + + return ( + + ); +}; diff --git a/react/features/participants-pane/components/web/InviteButton.js b/react/features/participants-pane/components/web/InviteButton.js index d72a8f92a..912f2b033 100644 --- a/react/features/participants-pane/components/web/InviteButton.js +++ b/react/features/participants-pane/components/web/InviteButton.js @@ -1,5 +1,6 @@ // @flow +import { makeStyles } from '@material-ui/styles'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -8,11 +9,24 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics'; import { Icon, IconInviteMore } from '../../../base/icons'; import { beginAddPeople } from '../../../invite'; -import { ParticipantInviteButton } from './styled'; +import ParticipantPaneBaseButton from './ParticipantPaneBaseButton'; + +const useStyles = makeStyles(theme => { + return { + button: { + width: '100%', + + '& > *:not(:last-child)': { + marginRight: `${theme.spacing(2)}px` + } + } + }; +}); export const InviteButton = () => { const dispatch = useDispatch(); const { t } = useTranslation(); + const styles = useStyles(); const onInvite = useCallback(() => { sendAnalytics(createToolbarEvent('invite')); @@ -20,13 +34,15 @@ export const InviteButton = () => { }, [ dispatch ]); return ( - + {t('participantsPane.actions.invite')} - + ); }; diff --git a/react/features/participants-pane/components/web/LobbyParticipantItem.js b/react/features/participants-pane/components/web/LobbyParticipantItem.js index ba50c8f65..1fdfac17c 100644 --- a/react/features/participants-pane/components/web/LobbyParticipantItem.js +++ b/react/features/participants-pane/components/web/LobbyParticipantItem.js @@ -1,5 +1,6 @@ // @flow +import { makeStyles } from '@material-ui/styles'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,8 +8,8 @@ import { hasRaisedHand } from '../../../base/participants'; import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants'; import { useLobbyActions } from '../../hooks'; +import LobbyParticipantQuickAction from './LobbyParticipantQuickAction'; import ParticipantItem from './ParticipantItem'; -import { ParticipantActionButton } from './styled'; type Props = { @@ -28,6 +29,14 @@ type Props = { participant: Object }; +const useStyles = makeStyles(theme => { + return { + button: { + marginRight: `${theme.spacing(2)}px` + } + }; +}); + export const LobbyParticipantItem = ({ overflowDrawer, participant: p, @@ -36,6 +45,7 @@ export const LobbyParticipantItem = ({ const { id } = p; const [ admit, reject ] = useLobbyActions({ participantID: id }); const { t } = useTranslation(); + const styles = useStyles(); return ( - - {t('lobby.reject')} - - + {t('lobby.reject') } + + + testId = { `admit-${id}` }> {t('lobby.admit')} - + ); }; diff --git a/react/features/participants-pane/components/web/LobbyParticipantQuickAction.js b/react/features/participants-pane/components/web/LobbyParticipantQuickAction.js new file mode 100644 index 000000000..54878a374 --- /dev/null +++ b/react/features/participants-pane/components/web/LobbyParticipantQuickAction.js @@ -0,0 +1,70 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +import QuickActionButton from '../../../base/components/buttons/QuickActionButton'; + +type Props = { + + /** + * Label used for accessibility. + */ + accessibilityLabel: string, + + /** + * Component children + */ + children: string, + + /** + * Button class name. + */ + className?: string, + + /** + * Click handler function. + */ + onClick: Function, + + /** + * Whether or not the button is secondary. + */ + secondary?: boolean, + + /** + * Data test id. + */ + testId: string +} + +const useStyles = makeStyles(theme => { + return { + secondary: { + backgroundColor: theme.palette.ui04 + } + }; +}); + +const LobbyParticipantQuickAction = ({ + accessibilityLabel, + children, + className, + onClick, + secondary = false, + testId +}: Props) => { + const styles = useStyles(); + + return ( + + {children} + + ); +}; + +export default LobbyParticipantQuickAction; diff --git a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js index b70a1593d..110df84cf 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js +++ b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js @@ -1,9 +1,10 @@ // @flow -import { withStyles } from '@material-ui/core/styles'; import React, { Component } from 'react'; import { approveParticipant } from '../../../av-moderation/actions'; import { Avatar } from '../../../base/avatar'; +import ContextMenu from '../../../base/components/context-menu/ContextMenu'; +import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup'; import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; import { openDialog } from '../../../base/dialog'; import { isIosMobileBrowser } from '../../../base/environment/utils'; @@ -26,23 +27,13 @@ import { isParticipantModerator } from '../../../base/participants'; import { connect } from '../../../base/redux'; -import { withPixelLineHeight } from '../../../base/styles/functions.web'; import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks'; import { openChatById } from '../../../chat/actions'; import { setVolume } from '../../../filmstrip/actions.web'; -import { Drawer, JitsiPortal } from '../../../toolbox/components/web'; import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu'; import { VolumeSlider } from '../../../video-menu/components/web'; import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog'; -import { getComputedOuterHeight, isForceMuted } from '../../functions'; - -import { - ContextMenu, - ContextMenuIcon, - ContextMenuItem, - ContextMenuItemGroup, - ignoredChildClassName -} from './styled'; +import { isForceMuted } from '../../functions'; type Props = { @@ -153,49 +144,16 @@ type Props = { */ overflowDrawer: boolean, - /** * The translate function. */ t: Function }; -type State = { - - /** - * If true the context menu will be hidden. - */ - isHidden: boolean -}; - -const styles = theme => { - return { - drawer: { - '& > div': { - ...withPixelLineHeight(theme.typography.bodyShortRegularLarge), - lineHeight: '32px', - - '& svg': { - fill: theme.palette.icon01 - } - }, - '&:first-child': { - marginTop: 15 - } - } - }; -}; - - /** * Implements the MeetingParticipantContextMenu component. */ -class MeetingParticipantContextMenu extends Component { - - /** - * Reference to the context menu container div. - */ - _containerRef: Object; +class MeetingParticipantContextMenu extends Component { /** * Creates new instance of MeetingParticipantContextMenu. @@ -205,19 +163,13 @@ class MeetingParticipantContextMenu extends Component { constructor(props: Props) { super(props); - this.state = { - isHidden: true - }; - - this._containerRef = React.createRef(); - this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this); this._onGrantModerator = this._onGrantModerator.bind(this); this._onKick = this._onKick.bind(this); this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this); this._onMuteVideo = this._onMuteVideo.bind(this); this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this); - this._position = this._position.bind(this); + this._onStopSharedVideo = this._onStopSharedVideo.bind(this); this._onVolumeChange = this._onVolumeChange.bind(this); this._onAskToUnmute = this._onAskToUnmute.bind(this); } @@ -314,35 +266,6 @@ class MeetingParticipantContextMenu extends Component { overflowDrawer && closeDrawer(); } - _position: () => void; - - /** - * Positions the context menu. - * - * @returns {void} - */ - _position() { - const { _participant, offsetTarget } = this.props; - - if (_participant - && this._containerRef.current - && offsetTarget?.offsetParent - && offsetTarget.offsetParent instanceof HTMLElement - ) { - const { current: container } = this._containerRef; - const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget; - const outerHeight = getComputedOuterHeight(container); - - container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop - ? offsetTop - outerHeight - : offsetTop; - - this.setState({ isHidden: false }); - } else { - this.setState({ isHidden: true }); - } - } - _onVolumeChange: (number) => void; /** @@ -372,26 +295,6 @@ class MeetingParticipantContextMenu extends Component { dispatch(approveParticipant(id)); } - /** - * Implements React Component's componentDidMount. - * - * @inheritdoc - * @returns {void} - */ - componentDidMount() { - this._position(); - } - - /** - * Implements React Component's componentDidUpdate. - * - * @inheritdoc - */ - componentDidUpdate(prevProps: Props) { - if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) { - this._position(); - } - } /** * Implements React's {@link Component#render()}. @@ -411,9 +314,9 @@ class MeetingParticipantContextMenu extends Component { _localVideoOwner, _participant, _volume = 1, - classes, closeDrawer, drawerParticipant, + offsetTarget, onEnter, onLeave, onSelect, @@ -427,88 +330,83 @@ class MeetingParticipantContextMenu extends Component { } const showVolumeSlider = !isIosMobileBrowser() - && overflowDrawer - && typeof _volume === 'number' - && !isNaN(_volume); + && overflowDrawer + && typeof _volume === 'number' + && !isNaN(_volume); + + const fakeParticipantActions = [ { + accessibilityLabel: t('toolbar.stopSharedVideo'), + icon: IconShareVideo, + onClick: this._onStopSharedVideo, + text: t('toolbar.stopSharedVideo') + } ]; + + const moderatorActions1 = [ + overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) ? { + accessibilityLabel: t(_isAudioForceMuted + ? 'participantsPane.actions.askUnmute' + : 'participantsPane.actions.allowVideo'), + icon: IconMicrophone, + onClick: this._onAskToUnmute, + text: t(_isAudioForceMuted + ? 'participantsPane.actions.askUnmute' + : 'participantsPane.actions.allowVideo') + } : null, + !_isParticipantAudioMuted && overflowDrawer ? { + accessibilityLabel: t('dialog.muteParticipantButton'), + icon: IconMicDisabled, + onClick: muteAudio(_participant), + text: t('dialog.muteParticipantButton') + } : null, { + accessibilityLabel: t('toolbar.accessibilityLabel.muteEveryoneElse'), + icon: IconMuteEveryoneElse, + onClick: this._onMuteEveryoneElse, + text: t('toolbar.accessibilityLabel.muteEveryoneElse') + }, + _isParticipantVideoMuted ? null : { + accessibilityLabel: t('participantsPane.actions.stopVideo'), + icon: IconVideoOff, + onClick: this._onMuteVideo, + text: t('participantsPane.actions.stopVideo') + } + ].filter(Boolean); + + const moderatorActions2 = [ + _isLocalModerator && !_isParticipantModerator ? { + accessibilityLabel: t('toolbar.accessibilityLabel.grantModerator'), + icon: IconCrown, + onClick: this._onGrantModerator, + text: t('toolbar.accessibilityLabel.grantModerator') + } : null, + _isLocalModerator ? { + accessibilityLabel: t('videothumbnail.kick'), + icon: IconCloseCircle, + onClick: this._onKick, + text: t('videothumbnail.kick') + } : null, + _isChatButtonEnabled ? { + accessibilityLabel: t('toolbar.accessibilityLabel.privateMessage'), + icon: IconMessage, + onClick: this._onSendPrivateMessage, + text: t('toolbar.accessibilityLabel.privateMessage') + } : null + ].filter(Boolean); const actions = _participant?.isFakeParticipant ? ( <> {_localVideoOwner && ( - - - {t('toolbar.stopSharedVideo')} - + )} ) : ( <> - {_isLocalModerator && ( - - <> - {overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) - && - - - {t(_isAudioForceMuted - ? 'participantsPane.actions.askUnmute' - : 'participantsPane.actions.allowVideo')} - - - } - { - !_isParticipantAudioMuted && overflowDrawer - && - - {t('dialog.muteParticipantButton')} - - } + {_isLocalModerator + && + } - - - {t('toolbar.accessibilityLabel.muteEveryoneElse')} - - - - { - _isParticipantVideoMuted || ( - - - {t('participantsPane.actions.stopVideo')} - - ) - } - - )} - - - { - _isLocalModerator && ( - <> - { - !_isParticipantModerator && ( - - - {t('toolbar.accessibilityLabel.grantModerator')} - - ) - } - - - { t('videothumbnail.kick') } - - - ) - } - { - _isChatButtonEnabled && ( - - - {t('toolbar.accessibilityLabel.privateMessage')} - - ) - } - + { showVolumeSlider && { ); return ( - <> - { !overflowDrawer - && - { actions } - } - - - -
- - - - { drawerParticipant && drawerParticipant.displayName } - - - { actions } -
-
-
- + + {overflowDrawer && , + text: drawerParticipant && drawerParticipant.displayName + } ] } />} + {actions} + ); } } @@ -595,4 +481,4 @@ function _mapStateToProps(state, ownProps): Object { }; } -export default withStyles(styles)(translate(connect(_mapStateToProps)(MeetingParticipantContextMenu))); +export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)); diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.js b/react/features/participants-pane/components/web/MeetingParticipantItem.js index ca52858bb..59d1a60b9 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantItem.js +++ b/react/features/participants-pane/components/web/MeetingParticipantItem.js @@ -26,10 +26,11 @@ import { getParticipantVideoMediaState, getQuickActionButtonType } from '../../functions'; -import ParticipantQuickAction from '../ParticipantQuickAction'; +import ParticipantActionEllipsis from './ParticipantActionEllipsis'; import ParticipantItem from './ParticipantItem'; -import { ParticipantActionEllipsis } from './styled'; +import ParticipantQuickAction from './ParticipantQuickAction'; + type Props = { @@ -266,16 +267,17 @@ function MeetingParticipantItem({ buttonType = { _quickActionButtonType } muteAudio = { muteAudio } muteParticipantButtonText = { muteParticipantButtonText } - participantID = { _participantID } /> + participantID = { _participantID } + participantName = { _displayName } /> } {!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && ( )} diff --git a/react/features/participants-pane/components/web/MeetingParticipants.js b/react/features/participants-pane/components/web/MeetingParticipants.js index c660227bf..62973e8a1 100644 --- a/react/features/participants-pane/components/web/MeetingParticipants.js +++ b/react/features/participants-pane/components/web/MeetingParticipants.js @@ -1,10 +1,12 @@ // @flow +import { makeStyles } from '@material-ui/styles'; import React, { useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { rejectParticipantAudio } from '../../../av-moderation/actions'; +import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json'; import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; import { MEDIA_TYPE } from '../../../base/media'; import { @@ -14,14 +16,13 @@ import { connect } from '../../../base/redux'; import { normalizeAccents } from '../../../base/util/strings'; import { showOverflowDrawer } from '../../../toolbox/functions'; import { muteRemote } from '../../../video-menu/actions.any'; -import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions'; +import { findAncestorByClass, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions'; import { useParticipantDrawer } from '../../hooks'; import ClearableInput from './ClearableInput'; import { InviteButton } from './InviteButton'; import MeetingParticipantContextMenu from './MeetingParticipantContextMenu'; import MeetingParticipantItems from './MeetingParticipantItems'; -import { Heading, ParticipantContainer } from './styled'; type NullProto = { [key: string]: any, @@ -43,6 +44,22 @@ type RaiseContext = NullProto | {| const initialState = Object.freeze(Object.create(null)); +const useStyles = makeStyles(theme => { + return { + heading: { + color: theme.palette.text02, + ...theme.typography.labelButton, + lineHeight: `${theme.typography.labelButton.lineHeight}px`, + margin: `8px 0 ${participantsPaneTheme.panePadding}px`, + + [`@media(max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: { + ...theme.typography.labelButtonLarge, + lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px` + } + } + }; +}); + /** * Renders the MeetingParticipantList component. * NOTE: This component is not using useSelector on purpose. The child components MeetingParticipantItem @@ -82,7 +99,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw const raiseMenu = useCallback((participantID, target) => { setRaiseContext({ participantID, - offsetTarget: findStyledAncestor(target, ParticipantContainer) + offsetTarget: findAncestorByClass(target, 'list-item-container') }); }, [ raiseContext ]); @@ -122,9 +139,13 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw const askUnmuteText = t('participantsPane.actions.askUnmute'); const muteParticipantButtonText = t('dialog.muteParticipantButton'); + const styles = useStyles(); + return ( <> - {t('participantsPane.headings.participantsList', { count: participantsCount })} +
+ {t('participantsPane.headings.participantsList', { count: participantsCount })} +
{showInviteButton && } { + return { + button: { + padding: '6px' + } + }; +}); + +const ParticipantActionEllipsis = ({ accessibilityLabel, onClick }: Props) => { + const styles = useStyles(); + + return ( + + + + ); +}; + +export default ParticipantActionEllipsis; diff --git a/react/features/participants-pane/components/web/ParticipantItem.js b/react/features/participants-pane/components/web/ParticipantItem.js index f851cb32e..1b5fe842b 100644 --- a/react/features/participants-pane/components/web/ParticipantItem.js +++ b/react/features/participants-pane/components/web/ParticipantItem.js @@ -1,8 +1,10 @@ // @flow +import { makeStyles } from '@material-ui/styles'; import React, { type Node, useCallback } from 'react'; import { Avatar } from '../../../base/avatar'; +import ListItem from '../../../base/components/particpants-pane-list/ListItem'; import { translate } from '../../../base/i18n'; import { ACTION_TRIGGER, @@ -14,25 +16,6 @@ import { } from '../../constants'; import { RaisedHandIndicator } from './RaisedHandIndicator'; -import { - ModeratorLabel, - ParticipantActionsHover, - ParticipantActionsPermanent, - ParticipantContainer, - ParticipantContent, - ParticipantDetailsContainer, - ParticipantName, - ParticipantNameContainer, - ParticipantStates -} from './styled'; - -/** - * Participant actions component mapping depending on trigger type. - */ -const Actions = { - [ACTION_TRIGGER.HOVER]: ParticipantActionsHover, - [ACTION_TRIGGER.PERMANENT]: ParticipantActionsPermanent -}; type Props = { @@ -117,6 +100,28 @@ type Props = { youText: string } +const useStyles = makeStyles(theme => { + return { + nameContainer: { + display: 'flex', + flex: 1, + overflow: 'hidden' + }, + + name: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }, + + moderatorLabel: { + ...theme.typography.labelRegular, + lineHeight: `${theme.typography.labelRegular.lineHeight}px`, + color: theme.palette.text03 + } + }; +}); + /** * A component representing a participant entry in ParticipantPane and Lobby. * @@ -141,45 +146,55 @@ function ParticipantItem({ videoMediaState = MEDIA_STATE.NONE, youText }: Props) { - const ParticipantActions = Actions[actionsTrigger]; const onClick = useCallback( () => openDrawerForParticipant({ participantID, displayName })); + const styles = useStyles(); + + const icon = ( + + ); + + const text = ( + <> +
+
+ {displayName} +
+ {local ?  ({youText}) : null} +
+ {isModerator && !disableModeratorIndicator &&
+ {t('videothumbnail.moderator')} +
} + + ); + + const indicators = ( + <> + {raisedHand && } + {VideoStateIcons[videoMediaState]} + {AudioStateIcons[audioMediaState]} + + ); + return ( - - - - - - - { displayName } - - { local ?  ({ youText }) : null } - - {isModerator && !disableModeratorIndicator && - {t('videothumbnail.moderator')} - } - - { !local && } - - { raisedHand && } - { VideoStateIcons[videoMediaState] } - { AudioStateIcons[audioMediaState] } - - - + textChildren = { text } + trigger = { actionsTrigger } /> ); } diff --git a/react/features/participants-pane/components/web/ParticipantPaneBaseButton.js b/react/features/participants-pane/components/web/ParticipantPaneBaseButton.js new file mode 100644 index 000000000..b43571893 --- /dev/null +++ b/react/features/participants-pane/components/web/ParticipantPaneBaseButton.js @@ -0,0 +1,98 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json'; + +type Props = { + + /** + * Label used for accessibility. + */ + accessibilityLabel: String, + + /** + * Additional class name for custom styles. + */ + className?: string, + + /** + * Children of the component. + */ + children: string | React$Node, + + /** + * Button id. + */ + id?: string, + + /** + * Click handler + */ + onClick: Function, + + /** + * Whether or not the button should have primary button style. + */ + primary?: boolean +} + +const useStyles = makeStyles(theme => { + return { + button: { + alignItems: 'center', + backgroundColor: theme.palette.action02, + border: 0, + borderRadius: `${theme.shape.borderRadius}px`, + display: 'flex', + justifyContent: 'center', + minHeight: '40px', + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, + ...theme.typography.labelButton, + lineHeight: `${theme.typography.labelButton.lineHeight}px`, + + '&:hover': { + backgroundColor: theme.palette.action02Hover + }, + + [`@media (max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: { + ...theme.typography.labelButtonLarge, + lineHeight: `${theme.typography.labelButtonLarge.lineHeight}px`, + minWidth: '48px', + minHeight: '48px' + } + }, + + buttonPrimary: { + backgroundColor: theme.palette.action01, + + '&:hover': { + backgroundColor: theme.palette.action01Hover + } + } + }; +}); + +const ParticipantPaneBaseButton = ({ + accessibilityLabel, + className, + children, + id, + onClick, + primary = false +}: Props) => { + const styles = useStyles(); + + return ( + + ); +}; + +export default ParticipantPaneBaseButton; diff --git a/react/features/participants-pane/components/web/ParticipantQuickAction.js b/react/features/participants-pane/components/web/ParticipantQuickAction.js new file mode 100644 index 000000000..0c821812d --- /dev/null +++ b/react/features/participants-pane/components/web/ParticipantQuickAction.js @@ -0,0 +1,103 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { approveParticipant } from '../../../av-moderation/actions'; +import QuickActionButton from '../../../base/components/buttons/QuickActionButton'; +import { QUICK_ACTION_BUTTON } from '../../constants'; + +type Props = { + + /** + * The translated ask unmute aria label. + */ + ariaLabel?: boolean, + + /** + * The translated "ask unmute" text. + */ + askUnmuteText: string, + + /** + * The type of button to be displayed. + */ + buttonType: string, + + /** + * Callback used to open a confirmation dialog for audio muting. + */ + muteAudio: Function, + + /** + * Label for mute participant button. + */ + muteParticipantButtonText: string, + + /** + * The ID of the participant. + */ + participantID: string, + + /** + * The name of the participant. + */ + participantName: string +} + +const useStyles = makeStyles(theme => { + return { + button: { + marginRight: `${theme.spacing(2)}px` + } + }; +}); + +const ParticipantQuickAction = ({ + askUnmuteText, + buttonType, + muteAudio, + muteParticipantButtonText, + participantID, + participantName +}: Props) => { + const styles = useStyles(); + const dispatch = useDispatch(); + const { t } = useTranslation(); + + const askToUnmute = useCallback(() => { + dispatch(approveParticipant(participantID)); + }, [ dispatch, participantID ]); + + switch (buttonType) { + case QUICK_ACTION_BUTTON.MUTE: { + return ( + + {muteParticipantButtonText} + + ); + } + case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: { + return ( + + { askUnmuteText } + + ); + } + default: { + return null; + } + } +}; + +export default ParticipantQuickAction; diff --git a/react/features/participants-pane/components/web/ParticipantsPane.js b/react/features/participants-pane/components/web/ParticipantsPane.js index e714a9ee0..79fae3f6e 100644 --- a/react/features/participants-pane/components/web/ParticipantsPane.js +++ b/react/features/participants-pane/components/web/ParticipantsPane.js @@ -1,32 +1,22 @@ // @flow +import { withStyles } from '@material-ui/core'; import React, { Component } from 'react'; -import { ThemeProvider } from 'styled-components'; +import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json'; import { openDialog } from '../../../base/dialog'; import { translate } from '../../../base/i18n'; +import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons'; import { isLocalParticipantModerator } from '../../../base/participants'; import { connect } from '../../../base/redux'; -import { Drawer, JitsiPortal } from '../../../toolbox/components/web'; -import { showOverflowDrawer } from '../../../toolbox/functions'; import { MuteEveryoneDialog } from '../../../video-menu/components/'; import { close } from '../../actions'; -import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../../functions'; -import theme from '../../theme.json'; -import { FooterContextMenu } from '../FooterContextMenu'; +import { classList, findAncestorByClass, getParticipantsPaneOpen } from '../../functions'; +import FooterButton from './FooterButton'; +import { FooterContextMenu } from './FooterContextMenu'; import LobbyParticipants from './LobbyParticipants'; import MeetingParticipants from './MeetingParticipants'; -import { - AntiCollapse, - Close, - Container, - Footer, - FooterButton, - FooterEllipsisButton, - FooterEllipsisContainer, - Header -} from './styled'; /** * The type of the React {@code Component} props of {@link ParticipantsPane}. @@ -53,6 +43,11 @@ type Props = { */ dispatch: Function, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The i18n translate function. */ @@ -70,6 +65,68 @@ type State = { contextOpen: boolean, }; +const styles = theme => { + return { + container: { + boxSizing: 'border-box', + flex: 1, + overflowY: 'auto', + position: 'relative', + padding: `0 ${participantsPaneTheme.panePadding}px`, + + [`& > * + *:not(.${participantsPaneTheme.ignoredChildClassName})`]: { + marginTop: theme.spacing(3) + }, + + '&::-webkit-scrollbar': { + display: 'none' + } + }, + + closeButton: { + alignItems: 'center', + cursor: 'pointer', + display: 'flex', + justifyContent: 'center' + }, + + header: { + alignItems: 'center', + boxSizing: 'border-box', + display: 'flex', + height: `${participantsPaneTheme.headerSize}px`, + padding: '0 20px', + justifyContent: 'flex-end' + }, + + antiCollapse: { + fontSize: 0, + + '&:first-child': { + display: 'none' + }, + + '&:first-child + *': { + marginTop: 0 + } + }, + + footer: { + display: 'flex', + justifyContent: 'flex-end', + padding: `${theme.spacing(4)}px ${participantsPaneTheme.panePadding}px`, + + '& > *:not(:last-child)': { + marginRight: `${theme.spacing(3)}px` + } + }, + + footerMoreContainer: { + position: 'relative' + } + }; +}; + /** * Implements the participants list. */ @@ -121,9 +178,9 @@ class ParticipantsPane extends Component { */ render() { const { - _overflowDrawer, _paneOpen, _showFooter, + classes, t } = this.props; const { contextOpen } = this.state; @@ -135,46 +192,50 @@ class ParticipantsPane extends Component { } return ( - -
-
-
- -
- - - - - - {_showFooter && ( -
- - {t('participantsPane.actions.muteAll')} - - - - {this.state.contextOpen && !_overflowDrawer - && } - -
- )} +
+
+
+
+ +
- - - - - +
+ +
+ +
+ {_showFooter && ( +
+ + {t('participantsPane.actions.muteAll')} + +
+ + + + +
+
+ )}
- +
); } @@ -253,7 +314,7 @@ class ParticipantsPane extends Component { * @returns {void} */ _onWindowClickListener(e) { - if (this.state.contextOpen && !findStyledAncestor(e.target, FooterEllipsisContainer)) { + if (this.state.contextOpen && !findAncestorByClass(e.target, this.props.classes.footerMoreContainer)) { this.setState({ contextOpen: false }); @@ -278,10 +339,9 @@ function _mapStateToProps(state: Object) { const isPaneOpen = getParticipantsPaneOpen(state); return { - _overflowDrawer: showOverflowDrawer(state), _paneOpen: isPaneOpen, _showFooter: isPaneOpen && isLocalParticipantModerator(state) }; } -export default translate(connect(_mapStateToProps)(ParticipantsPane)); +export default translate(connect(_mapStateToProps)(withStyles(styles)(ParticipantsPane))); diff --git a/react/features/participants-pane/components/ParticipantsPaneButton.js b/react/features/participants-pane/components/web/ParticipantsPaneButton.js similarity index 89% rename from react/features/participants-pane/components/ParticipantsPaneButton.js rename to react/features/participants-pane/components/web/ParticipantsPaneButton.js index 69cf81101..b62540795 100644 --- a/react/features/participants-pane/components/ParticipantsPaneButton.js +++ b/react/features/participants-pane/components/web/ParticipantsPaneButton.js @@ -1,9 +1,9 @@ // @flow -import { translate } from '../../base/i18n'; -import { IconParticipants } from '../../base/icons'; -import { connect } from '../../base/redux'; -import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +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}. diff --git a/react/features/participants-pane/components/web/RaisedHandIndicator.js b/react/features/participants-pane/components/web/RaisedHandIndicator.js index 8034a13e8..491b0d447 100644 --- a/react/features/participants-pane/components/web/RaisedHandIndicator.js +++ b/react/features/participants-pane/components/web/RaisedHandIndicator.js @@ -1,15 +1,29 @@ // @flow +import { makeStyles } from '@material-ui/styles'; import React from 'react'; import { Icon, IconRaisedHandHollow } from '../../../base/icons'; -import { RaisedHandIndicatorBackground } from './styled'; +const useStyles = makeStyles(theme => { + return { + indicator: { + backgroundColor: theme.palette.warning02, + borderRadius: `${theme.shape.borderRadius / 2}px`, + height: '24px', + width: '24px' + } + }; +}); -export const RaisedHandIndicator = () => ( - - - -); +export const RaisedHandIndicator = () => { + const styles = useStyles(); + + return ( +
+ +
+ ); +}; diff --git a/react/features/participants-pane/components/web/index.js b/react/features/participants-pane/components/web/index.js index 78c66cead..9b96a1d96 100644 --- a/react/features/participants-pane/components/web/index.js +++ b/react/features/participants-pane/components/web/index.js @@ -1,5 +1,5 @@ export * from './InviteButton'; export * from './LobbyParticipantItem'; export { default as ParticipantsPane } from './ParticipantsPane'; -export * from '../ParticipantsPaneButton'; +export { default as ParticipantsPaneButton } from './ParticipantsPaneButton'; export * from './RaisedHandIndicator'; diff --git a/react/features/participants-pane/components/web/styled.js b/react/features/participants-pane/components/web/styled.js deleted file mode 100644 index 4f9faf29e..000000000 --- a/react/features/participants-pane/components/web/styled.js +++ /dev/null @@ -1,390 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { Icon, IconHorizontalPoints } from '../../../base/icons'; -import { ACTION_TRIGGER } from '../../constants'; - -const MD_BREAKPOINT = '580px'; - -export const ignoredChildClassName = 'ignore-child'; - -export const AntiCollapse = styled.br` - font-size: 0; -`; - -export const Button = styled.button` - align-items: center; - background-color: ${ - // eslint-disable-next-line no-confusing-arrow - props => props.primary ? '#0056E0' : '#3D3D3D' -}; - border: 0; - border-radius: 6px; - display: flex; - font-weight: unset; - justify-content: center; - min-height: 32px; - - &:hover { - background-color: ${ - // eslint-disable-next-line no-confusing-arrow - props => props.primary ? '#246FE5' : '#525252' -}; - } -`; - -export const QuickActionButton = styled(Button)` - padding: 0 12px; -`; - -export const Container = styled.div` - box-sizing: border-box; - flex: 1; - overflow-y: auto; - position: relative; - padding: 0 ${props => props.theme.panePadding}px; - - & > * + *:not(.${ignoredChildClassName}) { - margin-top: 16px; - } - - &::-webkit-scrollbar { - display: none; - } -`; - -export const ContextMenu = styled.div.attrs(props => { - return { - className: props.className - }; -})` - background-color: #292929; - border-radius: 3px; - box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25); - color: white; - font-size: ${props => props.theme.contextFontSize}px; - font-weight: ${props => props.theme.contextFontWeight}; - margin-top: ${props => { - const { - participantActionButtonHeight, - participantItemHeight - } = props.theme; - - return ((3 * participantItemHeight) + participantActionButtonHeight) / 4; - }}px; - position: absolute; - right: ${props => props.theme.panePadding}px; - top: 0; - z-index: 2; - - & > li { - list-style: none; - } - - ${props => props.isHidden && ` - pointer-events: none; - visibility: hidden; - `} -`; - -export const ContextMenuIcon = styled(Icon).attrs({ - size: 20 -})` - & > svg { - fill: #ffffff; - } -`; - -export const ContextMenuItem = styled.div` - align-items: center; - box-sizing: border-box; - cursor: pointer; - display: flex; - min-height: 40px; - padding: 10px 16px; - - & > *:not(:last-child) { - margin-right: 16px; - } - - &:hover { - background-color: #525252; - } -`; - -export const ContextMenuItemGroup = styled.div` - &:not(:empty) { - padding: 8px 0; - } - - & + &:not(:empty) { - border-top: 1px solid #4C4D50; - } -`; - -export const Close = styled.div` - align-items: center; - cursor: pointer; - display: flex; - height: 20px; - justify-content: center; - width: 20px; - - &:before, &:after { - content: ''; - background-color: #a4b8d1; - border-radius: 2px; - height: 2px; - position: absolute; - transform-origin: center center; - width: 21px; - } - - &:before { - transform: rotate(45deg); - } - - &:after { - transform: rotate(-45deg); - } -`; - -export const Footer = styled.div` - background-color: #141414; - display: flex; - justify-content: flex-end; - padding: 24px ${props => props.theme.panePadding}px; - - & > *:not(:last-child) { - margin-right: 16px; - } -`; - -export const FooterButton = styled(Button)` - height: 40px; - font-size: 15px; - padding: 0 16px; - - @media (max-width: ${MD_BREAKPOINT}) { - font-size: 16px; - height: 48px; - min-width: 48px; - } -`; - -export const FooterEllipsisButton = styled(FooterButton).attrs({ - children: -})` - padding: 8px; -`; - -export const FooterEllipsisContainer = styled.div` - position: relative; -`; - -export const Header = styled.div` - align-items: center; - box-sizing: border-box; - display: flex; - height: ${props => props.theme.headerSize}px; - padding: 0 20px; -`; - -export const Heading = styled.div` - color: #d1dbe8; - font-style: normal; - font-size: 15px; - line-height: 24px; - margin: 8px 0 ${props => props.theme.panePadding}px; - - @media (max-width: ${MD_BREAKPOINT}) { - font-size: 16px; - } -`; - -export const ParticipantActionButton = styled(Button)` - height: ${props => props.theme.participantActionButtonHeight}px; - padding: 6px 10px; -`; - -export const ParticipantActionEllipsis = styled(ParticipantActionButton).attrs({ - children: , - primary: true -})` - padding: 6px; -`; - -export const ParticipantActions = styled.div` - align-items: center; - z-index: 1; - - & > *:not(:last-child) { - margin-right: 8px; - } -`; - -export const ParticipantActionsHover = styled(ParticipantActions)` - background-color: #292929; - bottom: 1px; - display: none; - position: absolute; - right: ${props => props.theme.panePadding}; - top: 0; - - &:after { - content: ''; - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #292929 100%); - bottom: 0; - display: block; - left: 0; - pointer-events: none; - position: absolute; - top: 0; - transform: translateX(-100%); - } -`; - -export const ParticipantActionsPermanent = styled(ParticipantActions)` - display: flex; -`; - -export const ParticipantContent = styled.div` - align-items: center; - box-shadow: inset 0px -1px 0px rgba(255, 255, 255, 0.15); - display: flex; - flex: 1; - height: 100%; - overflow: hidden; - padding-right: ${props => props.theme.panePadding}px; -`; - -export const ParticipantStates = styled.div` - display: flex; - justify-content: flex-end; - - & > * { - align-items: center; - display: flex; - justify-content: center; - } - - & > *:not(:last-child) { - margin-right: 8px; - } - - .jitsi-icon { - padding: 3px; - } -`; - -export const ParticipantContainer = styled.div` - align-items: center; - color: white; - display: flex; - font-size: 13px; - font-weight: normal; - height: ${props => props.theme.participantItemHeight}px; - margin: 0 -${props => props.theme.panePadding}px; - padding-left: ${props => props.theme.panePadding}px; - position: relative; - - @media (max-width: ${MD_BREAKPOINT}) { - font-size: 16px; - height: 64px; - } - - &:hover { - ${ParticipantStates} { - ${props => !props.local && 'display: none'}; - } - } - - ${props => !props.isHighlighted && '&:hover {'} - background-color: #292929; - - & ${ParticipantActions} { - ${props => props.trigger === ACTION_TRIGGER.HOVER && ` - display: flex; - `} - } - - & ${ParticipantContent} { - box-shadow: none; - } - - & ${ParticipantStates} { - display: none; - } - ${props => !props.isHighlighted && '}'} -`; - -export const ParticipantInviteButton = styled(Button).attrs({ - primary: true -})` - font-size: 15px; - height: 40px; - width: 100%; - - & > *:not(:last-child) { - margin-right: 8px; - } - - @media (max-width: ${MD_BREAKPOINT}) { - font-size: 16px; - height: 48px; - } -`; - -export const ParticipantName = styled.div` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -export const ParticipantNameContainer = styled.div` - display: flex; - flex: 1; - overflow: hidden; -`; - -export const ModeratorLabel = styled.div` - font-size: 12px; - line-height: 16px; - color: #858585; -`; - -export const ParticipantDetailsContainer = styled.div` - display: flex; - flex: 1; - margin-right: 8px; - overflow: hidden; - flex-direction: column; - justify-content: flex-start; -`; - -export const RaisedHandIndicatorBackground = styled.div` - background-color: #ed9e1b; - border-radius: 3px; - height: 24px; - width: 24px; -`; - -export const VolumeInput = styled.input.attrs({ - type: 'range' -})` - width: 100%; -`; - -export const VolumeInputContainer = styled.div` - position: relative; - width: 100%; -`; - -export const VolumeOverlay = styled.div` - background-color: #0376da; - border-radius: 1px 0 0 1px; - height: 100%; - left: 0; - pointer-events: none; - position: absolute; -`; diff --git a/react/features/participants-pane/functions.js b/react/features/participants-pane/functions.js index 66281a976..2820b4bfb 100644 --- a/react/features/participants-pane/functions.js +++ b/react/features/participants-pane/functions.js @@ -28,20 +28,19 @@ import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants'; */ export const classList = (...args: Array) => args.filter(Boolean).join(' '); - /** * Find the first styled ancestor component of an element. * * @param {Element} target - Element to look up. - * @param {StyledComponentClass} component - Styled component reference. + * @param {string} cssClass - Styled component reference. * @returns {Element|null} Ancestor. */ -export const findStyledAncestor = (target: Object, component: any) => { - if (!target || target.matches(`.${component.styledComponentId}`)) { +export const findAncestorByClass = (target: Object, cssClass: string) => { + if (!target || target.classList.contains(cssClass)) { return target; } - return findStyledAncestor(target.parentElement, component); + return findAncestorByClass(target.parentElement, cssClass); }; /** diff --git a/react/features/participants-pane/theme.json b/react/features/participants-pane/theme.json deleted file mode 100644 index 855509f21..000000000 --- a/react/features/participants-pane/theme.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "colors": { - "moderationDisabled": "#E54B4B" - }, - "contextFontSize": 14, - "contextFontWeight": 400, - "headerSize": 60, - "panePadding": 16, - "participantActionButtonHeight": 32, - "participantItemHeight": 48, - "participantsPaneWidth": 315, - "rangeInputThumbSize": 14 -} diff --git a/react/features/toolbox/components/web/Drawer.js b/react/features/toolbox/components/web/Drawer.js index 8d3b0cafb..c41714723 100644 --- a/react/features/toolbox/components/web/Drawer.js +++ b/react/features/toolbox/components/web/Drawer.js @@ -1,5 +1,6 @@ // @flow +import { makeStyles } from '@material-ui/core'; import React, { useCallback } from 'react'; @@ -21,6 +22,14 @@ type Props = { onClose: Function }; +const useStyles = makeStyles(theme => { + return { + drawer: { + backgroundColor: theme.palette.ui02 + } + }; +}); + /** * Component that displays the mobile friendly drawer on web. * @@ -29,7 +38,9 @@ type Props = { function Drawer({ children, isOpen, - onClose }: Props) { + onClose +}: Props) { + const styles = useStyles(); /** * Handles clicks within the menu, preventing the propagation of the click event. @@ -58,7 +69,7 @@ function Drawer({ className = 'drawer-menu-container' onClick = { handleOutsideClick }>
{children}
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 745afc165..c82ac4345 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -37,7 +37,7 @@ import { close as closeParticipantsPane, open as openParticipantsPane } from '../../../participants-pane/actions'; -import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton'; +import { ParticipantsPaneButton } from '../../../participants-pane/components/web'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { addReactionToBuffer } from '../../../reactions/actions.any'; import { ReactionsMenuButton } from '../../../reactions/components';