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:
parent
78e825de36
commit
7aca5e71b9
|
@ -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",
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"colors": {
|
||||
"moderationDisabled": "#E54B4B"
|
||||
},
|
||||
"headerSize": 60,
|
||||
"ignoredChildClassName": "ignore-child",
|
||||
"panePadding": 16,
|
||||
"participantsPaneWidth": 315,
|
||||
"MD_BREAKPOINT": "580px"
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
|
@ -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> ({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> ({ 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 } />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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)));
|
||||
|
|
|
@ -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}.
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
`;
|
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"colors": {
|
||||
"moderationDisabled": "#E54B4B"
|
||||
},
|
||||
"contextFontSize": 14,
|
||||
"contextFontWeight": 400,
|
||||
"headerSize": 60,
|
||||
"panePadding": 16,
|
||||
"participantActionButtonHeight": 32,
|
||||
"participantItemHeight": 48,
|
||||
"participantsPaneWidth": 315,
|
||||
"rangeInputThumbSize": 14
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue