feat(breakout-rooms) add context menu to participants in other rooms

This commit is contained in:
Robert Pintilii 2022-06-23 08:40:11 +01:00 committed by GitHub
parent 7dca91a50a
commit ddce2e6bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 432 additions and 19 deletions

58
package-lock.json generated
View File

@ -142,7 +142,9 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",
"@types/uuid": "8.3.4",
"babel-loader": "8.2.3",
"babel-plugin-optional-require": "0.3.1",
@ -5383,6 +5385,16 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@ -5479,9 +5491,9 @@
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz",
"integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==",
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -5497,6 +5509,18 @@
"@types/react": "*"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
@ -24022,6 +24046,16 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@ -24118,9 +24152,9 @@
"dev": true
},
"@types/react": {
"version": "17.0.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz",
"integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==",
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -24136,6 +24170,18 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",

View File

@ -147,7 +147,9 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",
"@types/uuid": "8.3.4",
"babel-loader": "8.2.3",
"babel-plugin-optional-require": "0.3.1",
@ -190,5 +192,9 @@
"postinstall": "patch-package --error-on-fail && jetify",
"validate": "npm ls",
"start": "make dev"
},
"resolutions": {
"@types/react": "17.0.14",
"@types/react-dom": "17.0.14"
}
}

View File

@ -11,6 +11,7 @@ import { SET_VOLUME } from './actionTypes';
import {
ContextMenuLobbyParticipantReject
} from './components/native';
import RoomParticipantMenu from './components/native/RoomParticipantMenu';
export * from './actions.any';
/**
@ -81,3 +82,17 @@ export function setVolume(participantId: string, volume: number) {
volume
};
}
/**
* Displays the breakout room participant menu.
*
* @param {Object} room - The room the participant is in.
* @param {string} participantJid - The jid of the participant.
* @param {string} participantName - The display name of the participant.
* @returns {Function}
*/
export function showRoomParticipantMenu(room: Object, participantJid: string, participantName: string) {
return openSheet(RoomParticipantMenu, { room,
participantJid,
participantName });
}

View File

@ -1,9 +1,10 @@
// @flow
import React from 'react';
import { useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { isParticipantModerator } from '../../../../../base/participants';
import { isLocalParticipantModerator, isParticipantModerator } from '../../../../../base/participants';
import { showRoomParticipantMenu } from '../../../../actions.native';
import ParticipantItem from '../../../native/ParticipantItem';
type Props = {
@ -11,11 +12,23 @@ type Props = {
/**
* Participant to be displayed.
*/
item: Object
item: Object,
/**
* The room the participant is in.
*/
room: Object
};
const BreakoutRoomParticipantItem = ({ item }: Props) => {
const BreakoutRoomParticipantItem = ({ item, room }: Props) => {
const { defaultRemoteDisplayName } = useSelector(state => state['features/base/config']);
const moderator = useSelector(isLocalParticipantModerator);
const dispatch = useDispatch();
const onPress = useCallback(() => {
if (moderator) {
dispatch(showRoomParticipantMenu(room, item.jid, item.displayName));
}
}, [ moderator, room, item ]);
return (
<ParticipantItem
@ -23,6 +36,7 @@ const BreakoutRoomParticipantItem = ({ item }: Props) => {
isKnockingParticipant = { false }
isModerator = { isParticipantModerator(item) }
key = { item.jid }
onPress = { onPress }
participantID = { item.jid } />
);
};

View File

@ -64,7 +64,9 @@ export const CollapsibleRoom = ({ room, searchString }: Props) => {
keyExtractor = { _keyExtractor }
// eslint-disable-next-line react/jsx-no-bind
renderItem = { ({ item: participant }) => participantMatchesSearch(participant, searchString)
&& <BreakoutRoomParticipantItem item = { participant } /> }
&& <BreakoutRoomParticipantItem
item = { participant }
room = { room } /> }
scrollEnabled = { true }
showsHorizontalScrollIndicator = { false }
windowSize = { 2 } />

View File

@ -8,8 +8,11 @@ import { useSelector } from 'react-redux';
import { ListItem } from '../../../../../base/components';
import { Icon, IconArrowDown, IconArrowUp } from '../../../../../base/icons';
import { isLocalParticipantModerator } from '../../../../../base/participants';
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
import { ACTION_TRIGGER } from '../../../../constants';
import { participantMatchesSearch } from '../../../../functions';
import ParticipantActionEllipsis from '../../../web/ParticipantActionEllipsis';
import ParticipantItem from '../../../web/ParticipantItem';
type Props = {
@ -39,6 +42,16 @@ type Props = {
*/
onLeave?: Function,
/**
* The raise context for the participant menu.
*/
participantContextEntity: ?Object,
/**
* Callback to raise participant context menu.
*/
raiseParticipantContextMenu: Function,
/**
* Room reference.
*/
@ -47,7 +60,12 @@ type Props = {
/**
* Participants search string.
*/
searchString: string
searchString: string,
/**
* Toggles the room participant context menu.
*/
toggleParticipantMenu: Function
}
const useStyles = makeStyles(theme => {
@ -84,8 +102,11 @@ export const CollapsibleRoom = ({
isHighlighted,
onRaiseMenu,
onLeave,
participantContextEntity,
raiseParticipantContextMenu,
room,
searchString
searchString,
toggleParticipantMenu
}: Props) => {
const { t } = useTranslation();
const styles = useStyles();
@ -97,6 +118,8 @@ export const CollapsibleRoom = ({
onRaiseMenu(target);
}, [ onRaiseMenu ]);
const { defaultRemoteDisplayName } = useSelector(state => state['features/base/config']);
const overflowDrawer = useSelector(showOverflowDrawer);
const moderator = useSelector(isLocalParticipantModerator);
const arrow = (<div className = { styles.arrowContainer }>
<Icon
@ -109,6 +132,13 @@ export const CollapsibleRoom = ({
|| {}).length})`}
</span>);
const raiseParticipantMenu = useCallback(({ participantID, displayName }) => moderator
&& raiseParticipantContextMenu({
room,
jid: participantID,
participantName: displayName
}), [ room, moderator ]);
return (
<>
<ListItem
@ -126,10 +156,21 @@ export const CollapsibleRoom = ({
&& Object.values(room?.participants || {}).map((p: Object) =>
participantMatchesSearch(p, searchString) && (
<ParticipantItem
actionsTrigger = { ACTION_TRIGGER.HOVER }
displayName = { p.displayName || defaultRemoteDisplayName }
isHighlighted = { participantContextEntity?.jid === p.jid }
key = { p.jid }
local = { false }
participantID = { p.jid } />
openDrawerForParticipant = { raiseParticipantMenu }
overflowDrawer = { overflowDrawer }
participantID = { p.jid }>
{!overflowDrawer && moderator && (
<ParticipantActionEllipsis
onClick = { toggleParticipantMenu({ room,
jid: p.jid,
participantName: p.displayName }) } />
)}
</ParticipantItem>
))
}
</>

View File

@ -21,6 +21,7 @@ import JoinActionButton from './JoinQuickActionButton';
import { LeaveButton } from './LeaveButton';
import RoomActionEllipsis from './RoomActionEllipsis';
import { RoomContextMenu } from './RoomContextMenu';
import { RoomParticipantContextMenu } from './RoomParticipantContextMenu';
type Props = {
@ -41,7 +42,8 @@ export const RoomList = ({ searchString }: Props) => {
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
const _overflowDrawer = useSelector(showOverflowDrawer);
const [ lowerMenu, raiseMenu, toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
const [ lowerParticipantMenu, raiseParticipantMenu, toggleParticipantMenu,
participantMenuEnter, participantMenuLeave, raiseParticipantContext ] = useContextMenu();
const onRaiseMenu = useCallback(room => target => raiseMenu(room, target), [ raiseMenu ]);
return (
@ -55,8 +57,11 @@ export const RoomList = ({ searchString }: Props) => {
isHighlighted = { raiseContext.entity === room }
onLeave = { lowerMenu }
onRaiseMenu = { onRaiseMenu(room) }
participantContextEntity = { raiseParticipantContext.entity }
raiseParticipantContextMenu = { raiseParticipantMenu }
room = { room }
searchString = { searchString }>
searchString = { searchString }
toggleParticipantMenu = { toggleParticipantMenu }>
{!_overflowDrawer && <>
{!hideJoinRoomButton && <JoinActionButton room = { room } />}
{isLocalModerator && !room.isMainRoom
@ -71,6 +76,11 @@ export const RoomList = ({ searchString }: Props) => {
onLeave = { menuLeave }
onSelect = { lowerMenu }
{ ...raiseContext } />
<RoomParticipantContextMenu
onEnter = { participantMenuEnter }
onLeave = { participantMenuLeave }
onSelect = { lowerParticipantMenu }
{ ...raiseParticipantContext } />
</>
);
};

View File

@ -0,0 +1,118 @@
import { makeStyles } from '@material-ui/core';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
// @ts-ignore
import { Avatar } from '../../../../../base/avatar';
// @ts-ignore
import { ContextMenu, ContextMenuItemGroup } from '../../../../../base/components';
// @ts-ignore
import { isLocalParticipantModerator } from '../../../../../base/participants';
// @ts-ignore
import { getBreakoutRooms } from '../../../../../breakout-rooms/functions';
// @ts-ignore
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
// @ts-ignore
import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton';
// @ts-ignore
import { AVATAR_SIZE } from '../../../../constants';
type Props = {
/**
* Room and participant jid reference.
*/
entity: {
room: any,
jid: string,
participantName: string
},
/**
* Target elements against which positioning calculations are made.
*/
offsetTarget: HTMLElement|undefined,
/**
* Callback for the mouse entering the component.
*/
onEnter: Function,
/**
* Callback for the mouse leaving the component.
*/
onLeave: Function,
/**
* Callback for making a selection in the menu.
*/
onSelect: Function
};
const useStyles = makeStyles((theme:any) => {
return {
text: {
color: theme.palette.text02,
padding: '10px 16px',
height: '40px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box'
}
};
});
export const RoomParticipantContextMenu = ({
entity,
offsetTarget,
onEnter,
onLeave,
onSelect
}: Props) => {
const styles = useStyles();
const { t } = useTranslation();
const isLocalModerator = useSelector(isLocalParticipantModerator);
const lowerMenu = useCallback(() => onSelect(true), [onSelect]);
const rooms: Object = useSelector(getBreakoutRooms);
const overflowDrawer = useSelector(showOverflowDrawer);
const breakoutRoomsButtons = useMemo(() => Object.values(rooms || {}).map((room: any) => {
if (room.id !== entity?.room?.id) {
return (<SendToRoomButton
key = { room.id }
onClick = { lowerMenu }
participantID = { entity?.jid }
room = { room } />);
}
return null;
}).filter(Boolean), [ entity, rooms ]);
return isLocalModerator && (
<ContextMenu
entity = { entity }
isDrawerOpen = { Boolean(entity) }
offsetTarget = { offsetTarget }
onClick = { lowerMenu }
onDrawerClose = { onSelect }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
{overflowDrawer && entity?.jid && <ContextMenuItemGroup
actions = { [ {
accessibilityLabel: entity?.participantName,
customIcon: <Avatar
displayName = { entity?.participantName }
size = { AVATAR_SIZE } />,
text: entity?.participantName
} ] } />}
<ContextMenuItemGroup>
<div className = { styles.text }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</div>
{breakoutRoomsButtons}
</ContextMenuItemGroup>
</ContextMenu>
);
};

View File

@ -0,0 +1,147 @@
import React, { PureComponent } from 'react';
import { Text, View } from 'react-native';
// @ts-ignore
import { Avatar } from '../../../base/avatar';
// @ts-ignore
import { BottomSheet, hideSheet } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { getBreakoutRooms } from '../../../breakout-rooms/functions';
import SendToBreakoutRoom from '../../../video-menu/components/native/SendToBreakoutRoom';
import styles from '../../../video-menu/components/native/styles';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 24;
type Props = {
/**
* The list of all breakout rooms.
*/
_rooms: Array<any>,
/**
* The room the participant is in.
*/
room: any,
/**
* The jid of the selected participant.
*/
participantJid: string,
/**
* The display name of the selected participant.
*/
participantName: string,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Translation function.
*/
t: Function
}
/**
* Class to implement a popup menu that opens upon long pressing a thumbnail.
*/
class RoomParticipantMenu extends PureComponent<Props> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { _rooms, participantJid, room, t } = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: participantJid,
styles: bottomSheetStyles.buttons
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { true }>
<View style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemText }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</Text>
</View>
{_rooms.map(r => room.id !== r.id && (<SendToBreakoutRoom
key = { r.id }
room = { r }
{ ...buttonProps } />))}
</BottomSheet>
);
}
/**
* Callback to hide the {@code RemoteVideoMenu}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantName } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] }>
<Avatar
displayName = { participantName }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }>
{ participantName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_rooms: Object.values(getBreakoutRooms(state))
};
}
export default translate(connect(_mapStateToProps)(RoomParticipantMenu));

View File

@ -107,3 +107,8 @@ export const VideoStateIcons = {
),
[MEDIA_STATE.NONE]: null
};
/**
* Mobile web context menu avatar size.
*/
export const AVATAR_SIZE = 20;

View File

@ -9,7 +9,8 @@ import { KICK_OUT_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import {
getParticipantById,
getParticipantDisplayName
getParticipantDisplayName,
isLocalParticipantModerator
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
@ -76,6 +77,11 @@ type Props = {
*/
_isParticipantAvailable?: boolean,
/**
* Whether the local participant is moderator or not.
*/
_moderator: boolean,
/**
* Display name of the participant retrieved from Redux.
*/
@ -120,6 +126,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
_disableRemoteMute,
_disableGrantModerator,
_isParticipantAvailable,
_moderator,
_rooms,
_currentRoomId,
participantId,
@ -148,7 +155,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
<ConnectionStatusButton
{ ...buttonProps }
afterClick = { undefined } />
{_rooms.length > 1 && <>
{_moderator && _rooms.length > 1 && <>
<Divider style = { styles.divider } />
<View style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemText }>
@ -216,6 +223,7 @@ function _mapStateToProps(state, ownProps) {
const _rooms = Object.values(getBreakoutRooms(state));
const _currentRoomId = getCurrentRoomId(state);
const shouldDisableKick = disableKick || !kickOutEnabled;
const moderator = isLocalParticipantModerator(state);
return {
_currentRoomId,
@ -223,6 +231,7 @@ function _mapStateToProps(state, ownProps) {
_disableRemoteMute: Boolean(disableRemoteMute),
_disablePrivateChat: Boolean(disablePrivateChat),
_isParticipantAvailable: Boolean(isParticipantAvailable),
_moderator: moderator,
_participantDisplayName: getParticipantDisplayName(state, participantId),
_rooms
};