您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

MeetingParticipantList.js 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // @flow
  2. import React, { useCallback, useRef, useState } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { useSelector, useDispatch } from 'react-redux';
  5. import { openDialog } from '../../base/dialog';
  6. import {
  7. getLocalParticipant,
  8. getParticipantCountWithFake,
  9. getRemoteParticipants
  10. } from '../../base/participants';
  11. import MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
  12. import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
  13. import { InviteButton } from './InviteButton';
  14. import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
  15. import MeetingParticipantItem from './MeetingParticipantItem';
  16. import { Heading, ParticipantContainer } from './styled';
  17. type NullProto = {
  18. [key: string]: any,
  19. __proto__: null
  20. };
  21. type RaiseContext = NullProto | {|
  22. /**
  23. * Target elements against which positioning calculations are made
  24. */
  25. offsetTarget?: HTMLElement,
  26. /**
  27. * The ID of the participant.
  28. */
  29. participantID?: String,
  30. |};
  31. const initialState = Object.freeze(Object.create(null));
  32. /**
  33. * Renders the MeetingParticipantList component.
  34. *
  35. * @returns {ReactNode} - The component.
  36. */
  37. export function MeetingParticipantList() {
  38. const dispatch = useDispatch();
  39. const isMouseOverMenu = useRef(false);
  40. const participants = useSelector(getRemoteParticipants);
  41. const localParticipant = useSelector(getLocalParticipant);
  42. // This is very important as getRemoteParticipants is not changing its reference object
  43. // and we will not re-render on change, but if count changes we will do
  44. const participantsCount = useSelector(getParticipantCountWithFake);
  45. const showInviteButton = useSelector(shouldRenderInviteButton);
  46. const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
  47. const { t } = useTranslation();
  48. const lowerMenu = useCallback(() => {
  49. /**
  50. * We are tracking mouse movement over the active participant item and
  51. * the context menu. Due to the order of enter/leave events, we need to
  52. * defer checking if the mouse is over the context menu with
  53. * queueMicrotask
  54. */
  55. window.queueMicrotask(() => {
  56. if (isMouseOverMenu.current) {
  57. return;
  58. }
  59. if (raiseContext !== initialState) {
  60. setRaiseContext(initialState);
  61. }
  62. });
  63. }, [ raiseContext ]);
  64. const raiseMenu = useCallback((participantID, target) => {
  65. setRaiseContext({
  66. participantID,
  67. offsetTarget: findStyledAncestor(target, ParticipantContainer)
  68. });
  69. }, [ raiseContext ]);
  70. const toggleMenu = useCallback(participantID => e => {
  71. const { participantID: raisedParticipant } = raiseContext;
  72. if (raisedParticipant && raisedParticipant === participantID) {
  73. lowerMenu();
  74. } else {
  75. raiseMenu(participantID, e.target);
  76. }
  77. }, [ raiseContext ]);
  78. const menuEnter = useCallback(() => {
  79. isMouseOverMenu.current = true;
  80. }, []);
  81. const menuLeave = useCallback(() => {
  82. isMouseOverMenu.current = false;
  83. lowerMenu();
  84. }, [ lowerMenu ]);
  85. const muteAudio = useCallback(id => () => {
  86. dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
  87. });
  88. // FIXME:
  89. // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
  90. // taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
  91. // solution!!!
  92. // One potential proper fix would be to use react-window component in order to lower the number of components
  93. // mounted.
  94. const participantActionEllipsisLabel = t('MeetingParticipantItem.ParticipantActionEllipsis.options');
  95. const youText = t('chat.you');
  96. const askUnmuteText = t('participantsPane.actions.askUnmute');
  97. const muteParticipantButtonText = t('dialog.muteParticipantButton');
  98. const renderParticipant = id => (
  99. <MeetingParticipantItem
  100. askUnmuteText = { askUnmuteText }
  101. isHighlighted = { raiseContext.participantID === id }
  102. key = { id }
  103. muteAudio = { muteAudio }
  104. muteParticipantButtonText = { muteParticipantButtonText }
  105. onContextMenu = { toggleMenu(id) }
  106. onLeave = { lowerMenu }
  107. participantActionEllipsisLabel = { participantActionEllipsisLabel }
  108. participantID = { id }
  109. youText = { youText } />
  110. );
  111. const items = [];
  112. localParticipant && items.push(renderParticipant(localParticipant?.id));
  113. participants.forEach(p => {
  114. items.push(renderParticipant(p?.id));
  115. });
  116. return (
  117. <>
  118. <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
  119. {showInviteButton && <InviteButton />}
  120. <div>
  121. { items }
  122. </div>
  123. <MeetingParticipantContextMenu
  124. muteAudio = { muteAudio }
  125. onEnter = { menuEnter }
  126. onLeave = { menuLeave }
  127. onSelect = { lowerMenu }
  128. { ...raiseContext } />
  129. </>
  130. );
  131. }