2021-04-21 13:48:05 +00:00
|
|
|
// @flow
|
|
|
|
|
|
|
|
import React, { useCallback, useRef, useState } from 'react';
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2021-08-24 18:50:13 +00:00
|
|
|
import { useDispatch } from 'react-redux';
|
2021-04-21 13:48:05 +00:00
|
|
|
|
2021-08-31 08:24:47 +00:00
|
|
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
2021-07-12 15:14:38 +00:00
|
|
|
import { openDialog } from '../../../base/dialog';
|
2021-07-09 12:36:19 +00:00
|
|
|
import {
|
|
|
|
getParticipantCountWithFake,
|
2021-08-18 11:29:41 +00:00
|
|
|
getSortedParticipantIds
|
2021-07-12 15:14:38 +00:00
|
|
|
} from '../../../base/participants';
|
2021-08-24 18:50:13 +00:00
|
|
|
import { connect } from '../../../base/redux';
|
2021-07-12 15:14:38 +00:00
|
|
|
import MuteRemoteParticipantDialog from '../../../video-menu/components/web/MuteRemoteParticipantDialog';
|
|
|
|
import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
|
2021-04-21 13:48:05 +00:00
|
|
|
|
|
|
|
import { InviteButton } from './InviteButton';
|
2021-07-09 12:36:19 +00:00
|
|
|
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
|
|
|
import MeetingParticipantItem from './MeetingParticipantItem';
|
2021-04-21 13:48:05 +00:00
|
|
|
import { Heading, ParticipantContainer } from './styled';
|
|
|
|
|
|
|
|
type NullProto = {
|
|
|
|
[key: string]: any,
|
|
|
|
__proto__: null
|
|
|
|
};
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
type RaiseContext = NullProto | {|
|
2021-04-21 13:48:05 +00:00
|
|
|
|
|
|
|
/**
|
2021-08-24 18:50:13 +00:00
|
|
|
* Target elements against which positioning calculations are made.
|
2021-04-21 13:48:05 +00:00
|
|
|
*/
|
|
|
|
offsetTarget?: HTMLElement,
|
|
|
|
|
|
|
|
/**
|
2021-07-09 12:36:19 +00:00
|
|
|
* The ID of the participant.
|
2021-04-21 13:48:05 +00:00
|
|
|
*/
|
2021-07-09 12:36:19 +00:00
|
|
|
participantID?: String,
|
|
|
|
|};
|
2021-04-21 13:48:05 +00:00
|
|
|
|
|
|
|
const initialState = Object.freeze(Object.create(null));
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
/**
|
|
|
|
* Renders the MeetingParticipantList component.
|
2021-08-24 18:50:13 +00:00
|
|
|
* NOTE: This component is not using useSelector on purpose. The child components MeetingParticipantItem
|
|
|
|
* and MeetingParticipantContextMenu are using connect. Having those mixed leads to problems.
|
|
|
|
* When this one was using useSelector and the other two were not -the other two were re-rendered before this one was
|
|
|
|
* re-rendered, so when participant is leaving, we first re-render the item and menu components,
|
|
|
|
* throwing errors (closing the page) before removing those components for the participant that left.
|
2021-07-09 12:36:19 +00:00
|
|
|
*
|
|
|
|
* @returns {ReactNode} - The component.
|
|
|
|
*/
|
2021-08-24 18:50:13 +00:00
|
|
|
function MeetingParticipantList({ participantsCount, showInviteButton, sortedParticipantIds = [] }) {
|
2021-06-23 11:23:44 +00:00
|
|
|
const dispatch = useDispatch();
|
2021-04-21 13:48:05 +00:00
|
|
|
const isMouseOverMenu = useRef(false);
|
2021-07-09 12:36:19 +00:00
|
|
|
|
2021-04-21 13:48:05 +00:00
|
|
|
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
const lowerMenu = useCallback(() => {
|
|
|
|
/**
|
|
|
|
* We are tracking mouse movement over the active participant item and
|
|
|
|
* the context menu. Due to the order of enter/leave events, we need to
|
|
|
|
* defer checking if the mouse is over the context menu with
|
|
|
|
* queueMicrotask
|
|
|
|
*/
|
|
|
|
window.queueMicrotask(() => {
|
|
|
|
if (isMouseOverMenu.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (raiseContext !== initialState) {
|
|
|
|
setRaiseContext(initialState);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, [ raiseContext ]);
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
const raiseMenu = useCallback((participantID, target) => {
|
2021-04-21 13:48:05 +00:00
|
|
|
setRaiseContext({
|
2021-07-09 12:36:19 +00:00
|
|
|
participantID,
|
2021-04-21 13:48:05 +00:00
|
|
|
offsetTarget: findStyledAncestor(target, ParticipantContainer)
|
|
|
|
});
|
|
|
|
}, [ raiseContext ]);
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
const toggleMenu = useCallback(participantID => e => {
|
|
|
|
const { participantID: raisedParticipant } = raiseContext;
|
2021-04-21 13:48:05 +00:00
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
if (raisedParticipant && raisedParticipant === participantID) {
|
2021-04-21 13:48:05 +00:00
|
|
|
lowerMenu();
|
|
|
|
} else {
|
2021-07-09 12:36:19 +00:00
|
|
|
raiseMenu(participantID, e.target);
|
2021-04-21 13:48:05 +00:00
|
|
|
}
|
|
|
|
}, [ raiseContext ]);
|
|
|
|
|
|
|
|
const menuEnter = useCallback(() => {
|
|
|
|
isMouseOverMenu.current = true;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const menuLeave = useCallback(() => {
|
|
|
|
isMouseOverMenu.current = false;
|
|
|
|
lowerMenu();
|
|
|
|
}, [ lowerMenu ]);
|
|
|
|
|
2021-06-23 11:23:44 +00:00
|
|
|
const muteAudio = useCallback(id => () => {
|
|
|
|
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
|
|
|
|
});
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
// FIXME:
|
|
|
|
// It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
|
|
|
|
// taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
|
|
|
|
// solution!!!
|
|
|
|
// One potential proper fix would be to use react-window component in order to lower the number of components
|
|
|
|
// mounted.
|
|
|
|
const participantActionEllipsisLabel = t('MeetingParticipantItem.ParticipantActionEllipsis.options');
|
|
|
|
const youText = t('chat.you');
|
|
|
|
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
|
|
|
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
|
|
|
|
|
|
|
const renderParticipant = id => (
|
|
|
|
<MeetingParticipantItem
|
|
|
|
askUnmuteText = { askUnmuteText }
|
|
|
|
isHighlighted = { raiseContext.participantID === id }
|
|
|
|
key = { id }
|
|
|
|
muteAudio = { muteAudio }
|
|
|
|
muteParticipantButtonText = { muteParticipantButtonText }
|
|
|
|
onContextMenu = { toggleMenu(id) }
|
|
|
|
onLeave = { lowerMenu }
|
|
|
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
|
|
|
participantID = { id }
|
|
|
|
youText = { youText } />
|
|
|
|
);
|
|
|
|
|
2021-04-21 13:48:05 +00:00
|
|
|
return (
|
|
|
|
<>
|
2021-07-09 12:36:19 +00:00
|
|
|
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
2021-06-02 16:49:44 +00:00
|
|
|
{showInviteButton && <InviteButton />}
|
2021-04-21 13:48:05 +00:00
|
|
|
<div>
|
2021-08-18 11:29:41 +00:00
|
|
|
{sortedParticipantIds.map(renderParticipant)}
|
2021-04-21 13:48:05 +00:00
|
|
|
</div>
|
|
|
|
<MeetingParticipantContextMenu
|
2021-06-23 11:23:44 +00:00
|
|
|
muteAudio = { muteAudio }
|
2021-04-21 13:48:05 +00:00
|
|
|
onEnter = { menuEnter }
|
|
|
|
onLeave = { menuLeave }
|
|
|
|
onSelect = { lowerMenu }
|
|
|
|
{ ...raiseContext } />
|
|
|
|
</>
|
|
|
|
);
|
2021-07-09 12:36:19 +00:00
|
|
|
}
|
2021-08-24 18:50:13 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps (parts of) the redux state to the associated props for this component.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The Redux state.
|
|
|
|
* @param {Object} ownProps - The own props of the component.
|
|
|
|
* @private
|
|
|
|
* @returns {Props}
|
|
|
|
*/
|
|
|
|
function _mapStateToProps(state): Object {
|
|
|
|
const sortedParticipantIds = getSortedParticipantIds(state);
|
|
|
|
|
|
|
|
// This is very important as getRemoteParticipants is not changing its reference object
|
|
|
|
// and we will not re-render on change, but if count changes we will do
|
|
|
|
const participantsCount = getParticipantCountWithFake(state);
|
|
|
|
|
2021-08-31 08:24:47 +00:00
|
|
|
const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
|
2021-08-24 18:50:13 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
sortedParticipantIds,
|
|
|
|
participantsCount,
|
|
|
|
showInviteButton
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export default connect(_mapStateToProps)(MeetingParticipantList);
|