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
This commit is contained in:
robertpin 2021-10-22 16:23:52 +03:00 committed by vp8x8
parent 78e825de36
commit 7aca5e71b9
32 changed files with 1507 additions and 1049 deletions

View File

@ -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",

View File

@ -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 */

View File

@ -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 (<button
aria-label = { accessibilityLabel }
className = { `${styles.button} ${className}` }
data-testid = { testId }
onClick = { onClick }>
{children}
</button>);
};
export default QuickActionButton;

View File

@ -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<HTMLDivElement | null>(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
? <JitsiPortal>
<Drawer
isOpen = { isDrawerOpen && _overflowDrawer }
onClose = { onDrawerClose }>
<div className = { styles.drawer }>
{children}
</div>
</Drawer>
</JitsiPortal>
: <div
className = { clsx(participantsPaneTheme.ignoredChildClassName,
styles.contextMenu,
isHidden && styles.contextMenuHidden,
className
) }
onClick = { onClick }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }>
{children}
</div>
;
};
export default ContextMenu;

View File

@ -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<Action>,
/**
* 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 (
<div className = { styles.contextMenuItemGroup }>
{children}
{actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => (
<div
aria-label = { accessibilityLabel }
className = { clsx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
className
) }
id = { id }
key = { text }
onClick = { onClick }>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
<span>{text}</span>
</div>
))}
</div>
);
};
export default ContextMenuItemGroup;

View File

@ -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 (
<div
className = { `list-item-container ${styles.container} ${isHighlighted ? styles.highlighted : ''}` }
id = { id }
onClick = { onClick }
onMouseLeave = { onMouseLeave }>
<div> {icon} </div>
<div className = { styles.detailsContainer }>
<div className = { styles.name }>
{textChildren}
</div>
{indicators && (
<div
className = { `indicators ${styles.indicators} ${
isHighlighted || trigger === ACTION_TRIGGER.PERMANENT
? styles.indicatorsHidden : ''}` }>
{indicators}
</div>
)}
{!hideActions && (
<div
className = { `actions ${styles.actionsContainer} ${
trigger === ACTION_TRIGGER.PERMANENT ? styles.actionsPermanent : ''} ${
isHighlighted ? styles.actionsVisible : ''}` }>
{actions}
</div>
)}
</div>
</div>
);
};
export default ListItem;

View File

@ -0,0 +1,10 @@
{
"colors": {
"moderationDisabled": "#E54B4B"
},
"headerSize": 60,
"ignoredChildClassName": "ignore-child",
"panePadding": 16,
"participantsPaneWidth": 315,
"MD_BREAKPOINT": "580px"
}

View File

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

View File

@ -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 (
<QuickActionButton
aria-label = { `unmute-${participantID}` }
onClick = { askToUnmute }
primary = { true }
theme = {{
panePadding: 16
}}>
{ askUnmuteText }
</QuickActionButton>
);
}

View File

@ -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 (
<ContextMenu
className = { clsx(classes.contextMenu, inDrawer && clsx(classes.drawer)) }
onMouseLeave = { onMouseLeave }>
<ContextMenuItemGroup>
<ContextMenuItem
id = 'participants-pane-context-menu-stop-video'
onClick = { muteAllVideo }>
<Icon
size = { 20 }
src = { IconVideoOff } />
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
</ContextMenuItem>
</ContextMenuItemGroup>
{isModerationSupported && (participantCount === 1 || !allModerators) ? (
<ContextMenuItemGroup>
<div className = { classes.text }>
{t('participantsPane.actions.allow')}
</div>
{ isAudioModerationEnabled ? (
<ContextMenuItem
id = 'participants-pane-context-menu-stop-audio-moderation'
onClick = { disableAudioModeration }>
<span className = { classes.paddedAction }>
{t('participantsPane.actions.audioModeration') }
</span>
</ContextMenuItem>
) : (
<ContextMenuItem
id = 'participants-pane-context-menu-start-audio-moderation'
onClick = { enableAudioModeration }>
<Icon
size = { 20 }
src = { IconCheck } />
<span>{t('participantsPane.actions.audioModeration') }</span>
</ContextMenuItem>
)}
{ isVideoModerationEnabled ? (
<ContextMenuItem
id = 'participants-pane-context-menu-stop-video-moderation'
onClick = { disableVideoModeration }>
<span className = { classes.paddedAction }>
{t('participantsPane.actions.videoModeration')}
</span>
</ContextMenuItem>
) : (
<ContextMenuItem
id = 'participants-pane-context-menu-start-video-moderation'
onClick = { enableVideoModeration }>
<Icon
size = { 20 }
src = { IconCheck } />
<span>{t('participantsPane.actions.videoModeration')}</span>
</ContextMenuItem>
)}
</ContextMenuItemGroup>
) : undefined
}
</ContextMenu>
);
};

View File

@ -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 (
<QuickActionButton
aria-label = { `mute-${participantID}` }
onClick = { muteAudio(participantID) }
primary = { true }>
{ muteParticipantButtonText }
</QuickActionButton>
);
}
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
return (
<AskToUnmuteButton
askUnmuteText = { askUnmuteText }
participantID = { participantID } />
);
}
default: {
return null;
}
}
}

View File

@ -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 (<ParticipantPaneBaseButton
accessibilityLabel = { accessibilityLabel }
className = { isIconButton ? styles.button : '' }
id = { id }
onClick = { onClick }>
{children}
</ParticipantPaneBaseButton>
);
};
export default FooterButton;

View File

@ -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 (
<ContextMenu
className = { classes.contextMenu }
hidden = { !isOpen }
isDrawerOpen = { isOpen }
onDrawerClose = { onDrawerClose }
onMouseLeave = { onMouseLeave }>
<ContextMenuItemGroup
actions = { [ {
accessibilityLabel: t('participantsPane.actions.stopEveryonesVideo'),
id: 'participants-pane-context-menu-stop-video',
icon: IconVideoOff,
onClick: muteAllVideo,
text: t('participantsPane.actions.stopEveryonesVideo')
} ] } />
{isModerationSupported && (participantCount === 1 || !allModerators) && (
<ContextMenuItemGroup actions = { actions }>
<div className = { classes.text }>
<span>{t('participantsPane.actions.allow')}</span>
</div>
</ContextMenuItemGroup>
)}
</ContextMenu>
);
};

View File

@ -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 (
<ParticipantInviteButton
aria-label = { t('participantsPane.actions.invite') }
onClick = { onInvite }>
<ParticipantPaneBaseButton
accessibilityLabel = { t('participantsPane.actions.invite') }
className = { styles.button }
onClick = { onInvite }
primary = { true }>
<Icon
size = { 20 }
src = { IconInviteMore } />
<span>{t('participantsPane.actions.invite')}</span>
</ParticipantInviteButton>
</ParticipantPaneBaseButton>
);
};

View File

@ -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 (
<ParticipantItem
@ -49,20 +59,20 @@ export const LobbyParticipantItem = ({
raisedHand = { hasRaisedHand(p) }
videoMediaState = { MEDIA_STATE.NONE }
youText = { t('chat.you') }>
<ParticipantActionButton
aria-label = { `Reject ${p.name}` }
data-testid = { `reject-${id}` }
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.reject')} ${p.name}` }
className = { styles.button }
onClick = { reject }
primary = { false }>
{t('lobby.reject')}
</ParticipantActionButton>
<ParticipantActionButton
aria-label = { `Admit ${p.name}` }
data-testid = { `admit-${id}` }
secondary = { true }
testId = { `reject-${id}` }>
{t('lobby.reject') }
</LobbyParticipantQuickAction>
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.admit')} ${p.name}` }
onClick = { admit }
primary = { true }>
testId = { `admit-${id}` }>
{t('lobby.admit')}
</ParticipantActionButton>
</LobbyParticipantQuickAction>
</ParticipantItem>
);
};

View File

@ -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 (
<QuickActionButton
accessibilityLabel = { accessibilityLabel }
className = { `${secondary ? styles.secondary : ''} ${className ?? ''}` }
onClick = { onClick }
testId = { testId }>
{children}
</QuickActionButton>
);
};
export default LobbyParticipantQuickAction;

View File

@ -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<Props, State> {
/**
* Reference to the context menu container div.
*/
_containerRef: Object;
class MeetingParticipantContextMenu extends Component<Props> {
/**
* Creates new instance of MeetingParticipantContextMenu.
@ -205,19 +163,13 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
_localVideoOwner,
_participant,
_volume = 1,
classes,
closeDrawer,
drawerParticipant,
offsetTarget,
onEnter,
onLeave,
onSelect,
@ -427,88 +330,83 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
}
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 && (
<ContextMenuItem onClick = { this._onStopSharedVideo }>
<ContextMenuIcon src = { IconShareVideo } />
<span>{t('toolbar.stopSharedVideo')}</span>
</ContextMenuItem>
<ContextMenuItemGroup
actions = { fakeParticipantActions } />
)}
</>
) : (
<>
{_isLocalModerator && (
<ContextMenuItemGroup>
<>
{overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted)
&& <ContextMenuItem onClick = { this._onAskToUnmute }>
<ContextMenuIcon src = { IconMicrophone } />
<span>
{t(_isAudioForceMuted
? 'participantsPane.actions.askUnmute'
: 'participantsPane.actions.allowVideo')}
</span>
</ContextMenuItem>
}
{
!_isParticipantAudioMuted && overflowDrawer
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
<ContextMenuIcon src = { IconMicDisabled } />
<span>{t('dialog.muteParticipantButton')}</span>
</ContextMenuItem>
}
{_isLocalModerator
&& <ContextMenuItemGroup actions = { moderatorActions1 } />
}
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
<ContextMenuIcon src = { IconMuteEveryoneElse } />
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
</ContextMenuItem>
</>
{
_isParticipantVideoMuted || (
<ContextMenuItem onClick = { this._onMuteVideo }>
<ContextMenuIcon src = { IconVideoOff } />
<span>{t('participantsPane.actions.stopVideo')}</span>
</ContextMenuItem>
)
}
</ContextMenuItemGroup>
)}
<ContextMenuItemGroup>
{
_isLocalModerator && (
<>
{
!_isParticipantModerator && (
<ContextMenuItem onClick = { this._onGrantModerator }>
<ContextMenuIcon src = { IconCrown } />
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
</ContextMenuItem>
)
}
<ContextMenuItem onClick = { this._onKick }>
<ContextMenuIcon src = { IconCloseCircle } />
<span>{ t('videothumbnail.kick') }</span>
</ContextMenuItem>
</>
)
}
{
_isChatButtonEnabled && (
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
<ContextMenuIcon src = { IconMessage } />
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
</ContextMenuItem>
)
}
</ContextMenuItemGroup>
<ContextMenuItemGroup actions = { moderatorActions2 } />
{ showVolumeSlider
&& <ContextMenuItemGroup>
<VolumeSlider
@ -521,36 +419,24 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
);
return (
<>
{ !overflowDrawer
&& <ContextMenu
className = { ignoredChildClassName }
innerRef = { this._containerRef }
isHidden = { this.state.isHidden }
onClick = { onSelect }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
{ actions }
</ContextMenu>}
<JitsiPortal>
<Drawer
isOpen = { drawerParticipant && overflowDrawer }
onClose = { closeDrawer }>
<div className = { classes && classes.drawer }>
<ContextMenuItemGroup>
<ContextMenuItem>
<Avatar
participantId = { drawerParticipant && drawerParticipant.participantID }
size = { 20 } />
<span>{ drawerParticipant && drawerParticipant.displayName }</span>
</ContextMenuItem>
</ContextMenuItemGroup>
{ actions }
</div>
</Drawer>
</JitsiPortal>
</>
<ContextMenu
entity = { _participant }
isDrawerOpen = { drawerParticipant }
offsetTarget = { offsetTarget }
onClick = { onSelect }
onDrawerClose = { closeDrawer }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
{overflowDrawer && <ContextMenuItemGroup
actions = { [ {
accessibilityLabel: drawerParticipant && drawerParticipant.displayName,
customIcon: <Avatar
participantId = { drawerParticipant && drawerParticipant.participantID }
size = { 20 } />,
text: drawerParticipant && drawerParticipant.displayName
} ] } />}
{actions}
</ContextMenu>
);
}
}
@ -595,4 +481,4 @@ function _mapStateToProps(state, ownProps): Object {
};
}
export default withStyles(styles)(translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)));
export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));

View File

@ -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 } />
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
accessibilityLabel = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
</>
}
{!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
accessibilityLabel = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
)}
</ParticipantItem>

View File

@ -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 (
<>
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
<div className = { styles.heading }>
{t('participantsPane.headings.participantsList', { count: participantsCount })}
</div>
{showInviteButton && <InviteButton />}
<ClearableInput
onChange = { setSearchString }

View File

@ -0,0 +1,43 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import QuickActionButton from '../../../base/components/buttons/QuickActionButton';
import { Icon, IconHorizontalPoints } from '../../../base/icons';
type Props = {
/**
* Label used for accessibility.
*/
accessibilityLabel: string,
/**
* Click handler function.
*/
onClick: Function
}
const useStyles = makeStyles(() => {
return {
button: {
padding: '6px'
}
};
});
const ParticipantActionEllipsis = ({ accessibilityLabel, onClick }: Props) => {
const styles = useStyles();
return (
<QuickActionButton
accessibilityLabel = { accessibilityLabel }
className = { styles.button }
onClick = { onClick }>
<Icon src = { IconHorizontalPoints } />
</QuickActionButton>
);
};
export default ParticipantActionEllipsis;

View File

@ -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 = (
<Avatar
className = 'participant-avatar'
participantId = { participantID }
size = { 32 } />
);
const text = (
<>
<div className = { styles.nameContainer }>
<div className = { styles.name }>
{displayName}
</div>
{local ? <span>&nbsp;({youText})</span> : null}
</div>
{isModerator && !disableModeratorIndicator && <div className = { styles.moderatorLabel }>
{t('videothumbnail.moderator')}
</div>}
</>
);
const indicators = (
<>
{raisedHand && <RaisedHandIndicator />}
{VideoStateIcons[videoMediaState]}
{AudioStateIcons[audioMediaState]}
</>
);
return (
<ParticipantContainer
<ListItem
actions = { children }
hideActions = { local }
icon = { icon }
id = { `participant-item-${participantID}` }
indicators = { indicators }
isHighlighted = { isHighlighted }
local = { local }
onClick = { !local && overflowDrawer ? onClick : undefined }
onMouseLeave = { onLeave }
trigger = { actionsTrigger }>
<Avatar
className = 'participant-avatar'
participantId = { participantID }
size = { 32 } />
<ParticipantContent>
<ParticipantDetailsContainer>
<ParticipantNameContainer>
<ParticipantName>
{ displayName }
</ParticipantName>
{ local ? <span>&nbsp;({ youText })</span> : null }
</ParticipantNameContainer>
{isModerator && !disableModeratorIndicator && <ModeratorLabel>
{t('videothumbnail.moderator')}
</ModeratorLabel>}
</ParticipantDetailsContainer>
{ !local && <ParticipantActions children = { children } /> }
<ParticipantStates>
{ raisedHand && <RaisedHandIndicator /> }
{ VideoStateIcons[videoMediaState] }
{ AudioStateIcons[audioMediaState] }
</ParticipantStates>
</ParticipantContent>
</ParticipantContainer>
textChildren = { text }
trigger = { actionsTrigger } />
);
}

View File

@ -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 (
<button
aria-label = { accessibilityLabel }
className = { `${styles.button} ${primary ? styles.buttonPrimary : ''} ${className ?? ''}` }
id = { id }
onClick = { onClick }>
{children}
</button>
);
};
export default ParticipantPaneBaseButton;

View File

@ -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 (
<QuickActionButton
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
className = { styles.button }
onClick = { muteAudio(participantID) }
testId = { `mute-${participantID}` }>
{muteParticipantButtonText}
</QuickActionButton>
);
}
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
return (
<QuickActionButton
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
className = { styles.button }
onClick = { askToUnmute }
testId = { `unmute-${participantID}` }>
{ askUnmuteText }
</QuickActionButton>
);
}
default: {
return null;
}
}
};
export default ParticipantQuickAction;

View File

@ -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<Props, State> {
*/
render() {
const {
_overflowDrawer,
_paneOpen,
_showFooter,
classes,
t
} = this.props;
const { contextOpen } = this.state;
@ -135,46 +192,50 @@ class ParticipantsPane extends Component<Props, State> {
}
return (
<ThemeProvider theme = { theme }>
<div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
<div className = 'participants_pane-content'>
<Header>
<Close
aria-label = { t('participantsPane.close', 'Close') }
onClick = { this._onClosePane }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 } />
</Header>
<Container>
<LobbyParticipants />
<AntiCollapse />
<MeetingParticipants />
</Container>
{_showFooter && (
<Footer>
<FooterButton onClick = { this._onMuteAll }>
{t('participantsPane.actions.muteAll')}
</FooterButton>
<FooterEllipsisContainer>
<FooterEllipsisButton
id = 'participants-pane-context-menu'
onClick = { this._onToggleContext } />
{this.state.contextOpen && !_overflowDrawer
&& <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
</FooterEllipsisContainer>
</Footer>
)}
<div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
<div className = 'participants_pane-content'>
<div className = { classes.header }>
<div
aria-label = { t('participantsPane.close', 'Close') }
className = { classes.closeButton }
onClick = { this._onClosePane }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
size = { 24 }
src = { IconClose } />
</div>
</div>
<JitsiPortal>
<Drawer
isOpen = { contextOpen && _overflowDrawer }
onClose = { this._onDrawerClose }>
<FooterContextMenu inDrawer = { true } />
</Drawer>
</JitsiPortal>
<div className = { classes.container }>
<LobbyParticipants />
<br className = { classes.antiCollapse } />
<MeetingParticipants />
</div>
{_showFooter && (
<div className = { classes.footer }>
<FooterButton
accessibilityLabel = { t('participantsPane.actions.muteAll') }
onClick = { this._onMuteAll }>
{t('participantsPane.actions.muteAll')}
</FooterButton>
<div className = { classes.footerMoreContainer }>
<FooterButton
accessibilityLabel = { t('participantsPane.actions.moreModerationActions') }
id = 'participants-pane-context-menu'
isIconButton = { true }
onClick = { this._onToggleContext }>
<Icon src = { IconHorizontalPoints } />
</FooterButton>
<FooterContextMenu
isOpen = { contextOpen }
onDrawerClose = { this._onDrawerClose }
onMouseLeave = { this._onToggleContext } />
</div>
</div>
)}
</div>
</ThemeProvider>
</div>
);
}
@ -253,7 +314,7 @@ class ParticipantsPane extends Component<Props, State> {
* @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)));

View File

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

View File

@ -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 = () => (
<RaisedHandIndicatorBackground>
<Icon
size = { 15 }
src = { IconRaisedHandHollow } />
</RaisedHandIndicatorBackground>
);
export const RaisedHandIndicator = () => {
const styles = useStyles();
return (
<div className = { styles.indicator }>
<Icon
size = { 15 }
src = { IconRaisedHandHollow } />
</div>
);
};

View File

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

View File

@ -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: <Icon src = { IconHorizontalPoints } />
})`
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: <Icon src = { IconHorizontalPoints } />,
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;
`;

View File

@ -28,20 +28,19 @@ import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants';
*/
export const classList = (...args: Array<string | boolean>) => 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);
};
/**

View File

@ -1,13 +0,0 @@
{
"colors": {
"moderationDisabled": "#E54B4B"
},
"contextFontSize": 14,
"contextFontWeight": 400,
"headerSize": 60,
"panePadding": 16,
"participantActionButtonHeight": 32,
"participantItemHeight": 48,
"participantsPaneWidth": 315,
"rangeInputThumbSize": 14
}

View File

@ -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 }>
<div
className = 'drawer-menu'
className = { `drawer-menu ${styles.drawer}` }
onClick = { handleInsideClick }>
{children}
</div>

View File

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