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.tsx 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import React, { useCallback } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { connect, useDispatch, useSelector } from 'react-redux';
  4. import { makeStyles } from 'tss-react/mui';
  5. import { IReduxState } from '../../../app/types';
  6. import { rejectParticipantAudio, rejectParticipantVideo } from '../../../av-moderation/actions';
  7. import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
  8. import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
  9. import { MEDIA_TYPE } from '../../../base/media/constants';
  10. import { getParticipantById, isScreenShareParticipant } from '../../../base/participants/functions';
  11. import { withPixelLineHeight } from '../../../base/styles/functions.web';
  12. import Input from '../../../base/ui/components/web/Input';
  13. import useContextMenu from '../../../base/ui/hooks/useContextMenu.web';
  14. import { normalizeAccents } from '../../../base/util/strings.web';
  15. import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
  16. import { showOverflowDrawer } from '../../../toolbox/functions.web';
  17. import { muteRemote } from '../../../video-menu/actions.web';
  18. import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
  19. import { useParticipantDrawer } from '../../hooks';
  20. import RenameButton from '../breakout-rooms/components/web/RenameButton';
  21. import { InviteButton } from './InviteButton';
  22. import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
  23. import MeetingParticipantItems from './MeetingParticipantItems';
  24. const useStyles = makeStyles()(theme => {
  25. return {
  26. headingW: {
  27. color: theme.palette.warning02
  28. },
  29. heading: {
  30. color: theme.palette.text02,
  31. ...withPixelLineHeight(theme.typography.bodyShortBold),
  32. marginBottom: theme.spacing(3),
  33. [`@media(max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: {
  34. ...withPixelLineHeight(theme.typography.bodyShortBoldLarge)
  35. }
  36. },
  37. search: {
  38. margin: `${theme.spacing(3)} 0`,
  39. '& input': {
  40. textAlign: 'center',
  41. paddingRight: '16px'
  42. }
  43. }
  44. };
  45. });
  46. interface IProps {
  47. currentRoom?: {
  48. jid: string;
  49. name: string;
  50. };
  51. overflowDrawer?: boolean;
  52. participantsCount?: number;
  53. searchString: string;
  54. setSearchString: (newValue: string) => void;
  55. showInviteButton?: boolean;
  56. sortedParticipantIds?: Array<string>;
  57. }
  58. /**
  59. * Renders the MeetingParticipantList component.
  60. * NOTE: This component is not using useSelector on purpose. The child components MeetingParticipantItem
  61. * and MeetingParticipantContextMenu are using connect. Having those mixed leads to problems.
  62. * When this one was using useSelector and the other two were not -the other two were re-rendered before this one was
  63. * re-rendered, so when participant is leaving, we first re-render the item and menu components,
  64. * throwing errors (closing the page) before removing those components for the participant that left.
  65. *
  66. * @returns {ReactNode} - The component.
  67. */
  68. function MeetingParticipants({
  69. currentRoom,
  70. overflowDrawer,
  71. participantsCount,
  72. searchString,
  73. setSearchString,
  74. showInviteButton,
  75. sortedParticipantIds = []
  76. }: IProps) {
  77. const dispatch = useDispatch();
  78. const { t } = useTranslation();
  79. const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu<string>();
  80. const muteAudio = useCallback(id => () => {
  81. dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
  82. dispatch(rejectParticipantAudio(id));
  83. }, [ dispatch ]);
  84. const stopVideo = useCallback(id => () => {
  85. dispatch(muteRemote(id, MEDIA_TYPE.VIDEO));
  86. dispatch(rejectParticipantVideo(id));
  87. }, [ dispatch ]);
  88. const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
  89. // FIXME:
  90. // It seems that useTranslation is not very scalable. Unmount 500 components that have the useTranslation hook is
  91. // taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
  92. // solution!!!
  93. // One potential proper fix would be to use react-window component in order to lower the number of components
  94. // mounted.
  95. const participantActionEllipsisLabel = t('participantsPane.actions.moreParticipantOptions');
  96. const youText = t('chat.you');
  97. const isBreakoutRoom = useSelector(isInBreakoutRoom);
  98. const _isCurrentRoomRenamable = useSelector(isCurrentRoomRenamable);
  99. const { classes: styles } = useStyles();
  100. return (
  101. <>
  102. <span
  103. aria-level = { 1 }
  104. className = 'sr-only'
  105. role = 'heading'>
  106. { t('participantsPane.title') }
  107. </span>
  108. <div className = { styles.heading }>
  109. {currentRoom?.name
  110. ? `${currentRoom.name} (${participantsCount})`
  111. : t('participantsPane.headings.participantsList', { count: participantsCount })}
  112. { currentRoom?.name && _isCurrentRoomRenamable
  113. && <RenameButton
  114. breakoutRoomJid = { currentRoom?.jid }
  115. name = { currentRoom?.name } /> }
  116. </div>
  117. {showInviteButton && <InviteButton />}
  118. <Input
  119. accessibilityLabel = { t('participantsPane.search') }
  120. className = { styles.search }
  121. clearable = { true }
  122. id = 'participants-search-input'
  123. onChange = { setSearchString }
  124. placeholder = { t('participantsPane.search') }
  125. value = { searchString } />
  126. <div>
  127. <MeetingParticipantItems
  128. isInBreakoutRoom = { isBreakoutRoom }
  129. lowerMenu = { lowerMenu }
  130. muteAudio = { muteAudio }
  131. openDrawerForParticipant = { openDrawerForParticipant }
  132. overflowDrawer = { overflowDrawer }
  133. participantActionEllipsisLabel = { participantActionEllipsisLabel }
  134. participantIds = { sortedParticipantIds }
  135. raiseContextId = { raiseContext.entity }
  136. searchString = { normalizeAccents(searchString) }
  137. stopVideo = { stopVideo }
  138. toggleMenu = { toggleMenu }
  139. youText = { youText } />
  140. </div>
  141. <MeetingParticipantContextMenu
  142. closeDrawer = { closeDrawer }
  143. drawerParticipant = { drawerParticipant }
  144. muteAudio = { muteAudio }
  145. offsetTarget = { raiseContext?.offsetTarget }
  146. onEnter = { menuEnter }
  147. onLeave = { menuLeave }
  148. onSelect = { lowerMenu }
  149. overflowDrawer = { overflowDrawer }
  150. participantID = { raiseContext?.entity } />
  151. </>
  152. );
  153. }
  154. /**
  155. * Maps (parts of) the redux state to the associated props for this component.
  156. *
  157. * @param {Object} state - The Redux state.
  158. * @param {Object} ownProps - The own props of the component.
  159. * @private
  160. * @returns {IProps}
  161. */
  162. function _mapStateToProps(state: IReduxState) {
  163. let sortedParticipantIds: any = getSortedParticipantIds(state);
  164. // Filter out the virtual screenshare participants since we do not want them to be displayed as separate
  165. // participants in the participants pane.
  166. sortedParticipantIds = sortedParticipantIds.filter((id: any) => {
  167. const participant = getParticipantById(state, id);
  168. return !isScreenShareParticipant(participant);
  169. });
  170. const participantsCount = sortedParticipantIds.length;
  171. const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
  172. const overflowDrawer = showOverflowDrawer(state);
  173. const currentRoomId = getCurrentRoomId(state);
  174. const currentRoom = getBreakoutRooms(state)[currentRoomId];
  175. return {
  176. currentRoom,
  177. overflowDrawer,
  178. participantsCount,
  179. showInviteButton,
  180. sortedParticipantIds
  181. };
  182. }
  183. export default connect(_mapStateToProps)(MeetingParticipants);