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.

ParticipantContextMenu.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // @flow
  2. import { makeStyles } from '@material-ui/styles';
  3. import React, { useCallback } from 'react';
  4. import { useTranslation } from 'react-i18next';
  5. import { useDispatch, useSelector } from 'react-redux';
  6. import { isSupported as isAvModerationSupported } from '../../../av-moderation/functions';
  7. import { Avatar } from '../../../base/avatar';
  8. import ContextMenu from '../../../base/components/context-menu/ContextMenu';
  9. import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
  10. import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
  11. import { IconShareVideo } from '../../../base/icons';
  12. import { MEDIA_TYPE } from '../../../base/media';
  13. import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
  14. import { isParticipantAudioMuted } from '../../../base/tracks';
  15. import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
  16. import { setVolume } from '../../../filmstrip/actions.web';
  17. import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
  18. import { isForceMuted } from '../../../participants-pane/functions';
  19. import { requestRemoteControl, stopController } from '../../../remote-control';
  20. import { stopSharedVideo } from '../../../shared-video/actions.any';
  21. import { showOverflowDrawer } from '../../../toolbox/functions.web';
  22. import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
  23. import SendToRoomButton from './SendToRoomButton';
  24. import {
  25. AskToUnmuteButton,
  26. ConnectionStatusButton,
  27. GrantModeratorButton,
  28. MuteButton,
  29. MuteEveryoneElseButton,
  30. MuteEveryoneElsesVideoButton,
  31. MuteVideoButton,
  32. KickButton,
  33. PrivateMessageMenuButton,
  34. RemoteControlButton,
  35. TogglePinToStageButton,
  36. VolumeSlider
  37. } from './';
  38. type Props = {
  39. /**
  40. * Class name for the context menu.
  41. */
  42. className?: string,
  43. /**
  44. * Closes a drawer if open.
  45. */
  46. closeDrawer?: Function,
  47. /**
  48. * The participant for which the drawer is open.
  49. * It contains the displayName & participantID.
  50. */
  51. drawerParticipant?: Object,
  52. /**
  53. * Shared video local participant owner.
  54. */
  55. localVideoOwner?: boolean,
  56. /**
  57. * Target elements against which positioning calculations are made.
  58. */
  59. offsetTarget?: HTMLElement,
  60. /**
  61. * Callback for the mouse entering the component.
  62. */
  63. onEnter?: Function,
  64. /**
  65. * Callback for the mouse leaving the component.
  66. */
  67. onLeave?: Function,
  68. /**
  69. * Callback for making a selection in the menu.
  70. */
  71. onSelect: Function,
  72. /**
  73. * Participant reference.
  74. */
  75. participant: Object,
  76. /**
  77. * The current state of the participant's remote control session.
  78. */
  79. remoteControlState?: number,
  80. /**
  81. * Whether or not the menu is displayed in the thumbnail remote video menu.
  82. */
  83. thumbnailMenu: ?boolean
  84. }
  85. const useStyles = makeStyles(theme => {
  86. return {
  87. text: {
  88. color: theme.palette.text02,
  89. padding: '10px 16px',
  90. height: '40px',
  91. overflow: 'hidden',
  92. display: 'flex',
  93. alignItems: 'center',
  94. boxSizing: 'border-box'
  95. }
  96. };
  97. });
  98. const ParticipantContextMenu = ({
  99. className,
  100. closeDrawer,
  101. drawerParticipant,
  102. localVideoOwner,
  103. offsetTarget,
  104. onEnter,
  105. onLeave,
  106. onSelect,
  107. participant,
  108. remoteControlState,
  109. thumbnailMenu
  110. }: Props) => {
  111. const dispatch = useDispatch();
  112. const { t } = useTranslation();
  113. const styles = useStyles();
  114. const localParticipant = useSelector(getLocalParticipant);
  115. const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
  116. const _isAudioForceMuted = useSelector(state =>
  117. isForceMuted(participant, MEDIA_TYPE.AUDIO, state));
  118. const _isVideoForceMuted = useSelector(state =>
  119. isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
  120. const _isAudioMuted = useSelector(state => isParticipantAudioMuted(participant, state));
  121. const _overflowDrawer = useSelector(showOverflowDrawer);
  122. const { remoteVideoMenu = {}, disableRemoteMute, startSilent }
  123. = useSelector(state => state['features/base/config']);
  124. const { disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
  125. const { participantsVolume } = useSelector(state => state['features/filmstrip']);
  126. const _volume = (participant?.local ?? true ? undefined
  127. : participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
  128. const isBreakoutRoom = useSelector(isInBreakoutRoom);
  129. const isModerationSupported = useSelector(isAvModerationSupported());
  130. const stageFilmstrip = useSelector(isStageFilmstripEnabled);
  131. const _currentRoomId = useSelector(getCurrentRoomId);
  132. const _rooms = Object.values(useSelector(getBreakoutRooms));
  133. const _onVolumeChange = useCallback(value => {
  134. dispatch(setVolume(participant.id, value));
  135. }, [ setVolume, dispatch ]);
  136. const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
  137. const _onStopSharedVideo = useCallback(() => {
  138. clickHandler();
  139. dispatch(stopSharedVideo());
  140. }, [ stopSharedVideo ]);
  141. const _getCurrentParticipantId = useCallback(() => {
  142. const drawer = _overflowDrawer && !thumbnailMenu;
  143. return (drawer ? drawerParticipant?.participantID : participant?.id) ?? '';
  144. }
  145. , [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
  146. const buttons = [];
  147. const buttons2 = [];
  148. const showVolumeSlider = !startSilent
  149. && !isIosMobileBrowser()
  150. && (_overflowDrawer || thumbnailMenu)
  151. && typeof _volume === 'number'
  152. && !isNaN(_volume);
  153. const fakeParticipantActions = [ {
  154. accessibilityLabel: t('toolbar.stopSharedVideo'),
  155. icon: IconShareVideo,
  156. onClick: _onStopSharedVideo,
  157. text: t('toolbar.stopSharedVideo')
  158. } ];
  159. if (_isModerator) {
  160. if ((thumbnailMenu || _overflowDrawer) && isModerationSupported && _isAudioMuted) {
  161. buttons.push(<AskToUnmuteButton
  162. isAudioForceMuted = { _isAudioForceMuted }
  163. isVideoForceMuted = { _isVideoForceMuted }
  164. key = 'ask-unmute'
  165. participantID = { _getCurrentParticipantId() } />
  166. );
  167. }
  168. if (!disableRemoteMute) {
  169. buttons.push(
  170. <MuteButton
  171. key = 'mute'
  172. participantID = { _getCurrentParticipantId() } />
  173. );
  174. buttons.push(
  175. <MuteEveryoneElseButton
  176. key = 'mute-others'
  177. participantID = { _getCurrentParticipantId() } />
  178. );
  179. buttons.push(
  180. <MuteVideoButton
  181. key = 'mute-video'
  182. participantID = { _getCurrentParticipantId() } />
  183. );
  184. buttons.push(
  185. <MuteEveryoneElsesVideoButton
  186. key = 'mute-others-video'
  187. participantID = { _getCurrentParticipantId() } />
  188. );
  189. }
  190. if (!disableGrantModerator && !isBreakoutRoom) {
  191. buttons2.push(
  192. <GrantModeratorButton
  193. key = 'grant-moderator'
  194. participantID = { _getCurrentParticipantId() } />
  195. );
  196. }
  197. if (!disableKick) {
  198. buttons2.push(
  199. <KickButton
  200. key = 'kick'
  201. participantID = { _getCurrentParticipantId() } />
  202. );
  203. }
  204. }
  205. if (stageFilmstrip) {
  206. buttons2.push(<TogglePinToStageButton
  207. key = 'pinToStage'
  208. participantID = { _getCurrentParticipantId() } />);
  209. }
  210. if (!disablePrivateChat) {
  211. buttons2.push(<PrivateMessageMenuButton
  212. key = 'privateMessage'
  213. participantID = { _getCurrentParticipantId() } />
  214. );
  215. }
  216. if (thumbnailMenu && isMobileBrowser()) {
  217. buttons2.push(
  218. <ConnectionStatusButton
  219. key = 'conn-status'
  220. participantId = { _getCurrentParticipantId() } />
  221. );
  222. }
  223. if (thumbnailMenu && remoteControlState) {
  224. let onRemoteControlToggle = null;
  225. if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
  226. onRemoteControlToggle = () => dispatch(stopController(true));
  227. } else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
  228. onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId()));
  229. }
  230. buttons2.push(
  231. <RemoteControlButton
  232. key = 'remote-control'
  233. onClick = { onRemoteControlToggle }
  234. participantID = { _getCurrentParticipantId() }
  235. remoteControlState = { remoteControlState } />
  236. );
  237. }
  238. const breakoutRoomsButtons = [];
  239. if (!thumbnailMenu && _isModerator) {
  240. _rooms.forEach((room: Object) => {
  241. if (room.id !== _currentRoomId) {
  242. breakoutRoomsButtons.push(
  243. <SendToRoomButton
  244. key = { room.id }
  245. onClick = { clickHandler }
  246. participantID = { _getCurrentParticipantId() }
  247. room = { room } />
  248. );
  249. }
  250. });
  251. }
  252. return (
  253. <ContextMenu
  254. className = { className }
  255. entity = { participant }
  256. hidden = { thumbnailMenu ? false : undefined }
  257. inDrawer = { thumbnailMenu && _overflowDrawer }
  258. isDrawerOpen = { drawerParticipant }
  259. offsetTarget = { offsetTarget }
  260. onClick = { onSelect }
  261. onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
  262. onMouseEnter = { onEnter }
  263. onMouseLeave = { onLeave }>
  264. {!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
  265. actions = { [ {
  266. accessibilityLabel: drawerParticipant.displayName,
  267. customIcon: <Avatar
  268. participantId = { drawerParticipant.participantID }
  269. size = { 20 } />,
  270. text: drawerParticipant.displayName
  271. } ] } />}
  272. {participant?.isFakeParticipant ? localVideoOwner && (
  273. <ContextMenuItemGroup
  274. actions = { fakeParticipantActions } />
  275. ) : (
  276. <>
  277. {buttons.length > 0 && (
  278. <ContextMenuItemGroup>
  279. {buttons}
  280. </ContextMenuItemGroup>
  281. )}
  282. <ContextMenuItemGroup>
  283. {buttons2}
  284. </ContextMenuItemGroup>
  285. {showVolumeSlider && (
  286. <ContextMenuItemGroup>
  287. <VolumeSlider
  288. initialValue = { _volume }
  289. key = 'volume-slider'
  290. onChange = { _onVolumeChange } />
  291. </ContextMenuItemGroup>
  292. )}
  293. {breakoutRoomsButtons.length > 0 && (
  294. <ContextMenuItemGroup>
  295. <div className = { styles.text }>
  296. {t('breakoutRooms.actions.sendToBreakoutRoom')}
  297. </div>
  298. {breakoutRoomsButtons}
  299. </ContextMenuItemGroup>
  300. )}
  301. </>
  302. )}
  303. </ContextMenu>
  304. );
  305. };
  306. export default ParticipantContextMenu;