You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MeetingParticipants.js 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. // @flow
  2. import React, { useCallback, useRef, useState } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { useDispatch } from 'react-redux';
  5. import { rejectParticipantAudio } from '../../../av-moderation/actions';
  6. import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
  7. import { MEDIA_TYPE } from '../../../base/media';
  8. import {
  9. getParticipantCountWithFake
  10. } from '../../../base/participants';
  11. import { connect } from '../../../base/redux';
  12. import { normalizeAccents } from '../../../base/util/strings';
  13. import { showOverflowDrawer } from '../../../toolbox/functions';
  14. import { muteRemote } from '../../../video-menu/actions.any';
  15. import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
  16. import { useParticipantDrawer } from '../../hooks';
  17. import ClearableInput from './ClearableInput';
  18. import { InviteButton } from './InviteButton';
  19. import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
  20. import MeetingParticipantItems from './MeetingParticipantItems';
  21. import { Heading, ParticipantContainer } from './styled';
  22. type NullProto = {
  23. [key: string]: any,
  24. __proto__: null
  25. };
  26. type RaiseContext = NullProto | {|
  27. /**
  28. * Target elements against which positioning calculations are made.
  29. */
  30. offsetTarget?: HTMLElement,
  31. /**
  32. * The ID of the participant.
  33. */
  34. participantID ?: string,
  35. |};
  36. const initialState = Object.freeze(Object.create(null));
  37. /**
  38. * Renders the MeetingParticipantList component.
  39. * NOTE: This component is not using useSelector on purpose. The child components MeetingParticipantItem
  40. * and MeetingParticipantContextMenu are using connect. Having those mixed leads to problems.
  41. * When this one was using useSelector and the other two were not -the other two were re-rendered before this one was
  42. * re-rendered, so when participant is leaving, we first re-render the item and menu components,
  43. * throwing errors (closing the page) before removing those components for the participant that left.
  44. *
  45. * @returns {ReactNode} - The component.
  46. */
  47. function MeetingParticipants({ participantsCount, showInviteButton, overflowDrawer, sortedParticipantIds = [] }) {
  48. const dispatch = useDispatch();
  49. const isMouseOverMenu = useRef(false);
  50. const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
  51. const [ searchString, setSearchString ] = useState('');
  52. const { t } = useTranslation();
  53. const lowerMenu = useCallback(() => {
  54. /**
  55. * We are tracking mouse movement over the active participant item and
  56. * the context menu. Due to the order of enter/leave events, we need to
  57. * defer checking if the mouse is over the context menu with
  58. * queueMicrotask
  59. */
  60. window.queueMicrotask(() => {
  61. if (isMouseOverMenu.current) {
  62. return;
  63. }
  64. if (raiseContext !== initialState) {
  65. setRaiseContext(initialState);
  66. }
  67. });
  68. }, [ raiseContext ]);
  69. const raiseMenu = useCallback((participantID, target) => {
  70. setRaiseContext({
  71. participantID,
  72. offsetTarget: findStyledAncestor(target, ParticipantContainer)
  73. });
  74. }, [ raiseContext ]);
  75. const toggleMenu = useCallback(participantID => e => {
  76. const { participantID: raisedParticipant } = raiseContext;
  77. if (raisedParticipant && raisedParticipant === participantID) {
  78. lowerMenu();
  79. } else {
  80. raiseMenu(participantID, e.target);
  81. }
  82. }, [ raiseContext ]);
  83. const menuEnter = useCallback(() => {
  84. isMouseOverMenu.current = true;
  85. }, []);
  86. const menuLeave = useCallback(() => {
  87. isMouseOverMenu.current = false;
  88. lowerMenu();
  89. }, [ lowerMenu ]);
  90. const muteAudio = useCallback(id => () => {
  91. dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
  92. dispatch(rejectParticipantAudio(id));
  93. }, [ dispatch ]);
  94. const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
  95. // FIXME:
  96. // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
  97. // taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
  98. // solution!!!
  99. // One potential proper fix would be to use react-window component in order to lower the number of components
  100. // mounted.
  101. const participantActionEllipsisLabel = t('MeetingParticipantItem.ParticipantActionEllipsis.options');
  102. const youText = t('chat.you');
  103. const askUnmuteText = t('participantsPane.actions.askUnmute');
  104. const muteParticipantButtonText = t('dialog.muteParticipantButton');
  105. return (
  106. <>
  107. <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
  108. {showInviteButton && <InviteButton />}
  109. <ClearableInput
  110. onChange = { setSearchString }
  111. placeholder = { t('participantsPane.search') } />
  112. <div>
  113. <MeetingParticipantItems
  114. askUnmuteText = { askUnmuteText }
  115. lowerMenu = { lowerMenu }
  116. muteAudio = { muteAudio }
  117. muteParticipantButtonText = { muteParticipantButtonText }
  118. openDrawerForParticipant = { openDrawerForParticipant }
  119. overflowDrawer = { overflowDrawer }
  120. participantActionEllipsisLabel = { participantActionEllipsisLabel }
  121. participantIds = { sortedParticipantIds }
  122. participantsCount = { participantsCount }
  123. raiseContextId = { raiseContext.participantID }
  124. searchString = { normalizeAccents(searchString) }
  125. toggleMenu = { toggleMenu }
  126. youText = { youText } />
  127. </div>
  128. <MeetingParticipantContextMenu
  129. closeDrawer = { closeDrawer }
  130. drawerParticipant = { drawerParticipant }
  131. muteAudio = { muteAudio }
  132. onEnter = { menuEnter }
  133. onLeave = { menuLeave }
  134. onSelect = { lowerMenu }
  135. overflowDrawer = { overflowDrawer }
  136. { ...raiseContext } />
  137. </>
  138. );
  139. }
  140. /**
  141. * Maps (parts of) the redux state to the associated props for this component.
  142. *
  143. * @param {Object} state - The Redux state.
  144. * @param {Object} ownProps - The own props of the component.
  145. * @private
  146. * @returns {Props}
  147. */
  148. function _mapStateToProps(state): Object {
  149. const sortedParticipantIds = getSortedParticipantIds(state);
  150. // This is very important as getRemoteParticipants is not changing its reference object
  151. // and we will not re-render on change, but if count changes we will do
  152. const participantsCount = getParticipantCountWithFake(state);
  153. const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
  154. const overflowDrawer = showOverflowDrawer(state);
  155. return {
  156. sortedParticipantIds,
  157. participantsCount,
  158. showInviteButton,
  159. overflowDrawer
  160. };
  161. }
  162. export default connect(_mapStateToProps)(MeetingParticipants);