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.

MeetingParticipantContextMenu.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. // @flow
  2. import { withStyles } from '@material-ui/core/styles';
  3. import React, { Component } from 'react';
  4. import { Avatar } from '../../../base/avatar';
  5. import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
  6. import { openDialog } from '../../../base/dialog';
  7. import { translate } from '../../../base/i18n';
  8. import {
  9. IconCloseCircle,
  10. IconCrown,
  11. IconMessage,
  12. IconMicDisabled,
  13. IconMuteEveryoneElse,
  14. IconShareVideo,
  15. IconVideoOff
  16. } from '../../../base/icons';
  17. import {
  18. getLocalParticipant,
  19. getParticipantByIdOrUndefined,
  20. isLocalParticipantModerator,
  21. isParticipantModerator
  22. } from '../../../base/participants';
  23. import { connect } from '../../../base/redux';
  24. import { withPixelLineHeight } from '../../../base/styles/functions.web';
  25. import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
  26. import { openChatById } from '../../../chat/actions';
  27. import { setVolume } from '../../../filmstrip/actions.web';
  28. import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
  29. import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
  30. import { VolumeSlider } from '../../../video-menu/components/web';
  31. import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
  32. import { getComputedOuterHeight } from '../../functions';
  33. import {
  34. ContextMenu,
  35. ContextMenuIcon,
  36. ContextMenuItem,
  37. ContextMenuItemGroup,
  38. ignoredChildClassName
  39. } from './styled';
  40. type Props = {
  41. /**
  42. * True if the local participant is moderator and false otherwise.
  43. */
  44. _isLocalModerator: boolean,
  45. /**
  46. * True if the chat button is enabled and false otherwise.
  47. */
  48. _isChatButtonEnabled: boolean,
  49. /**
  50. * True if the participant is moderator and false otherwise.
  51. */
  52. _isParticipantModerator: boolean,
  53. /**
  54. * True if the participant is video muted and false otherwise.
  55. */
  56. _isParticipantVideoMuted: boolean,
  57. /**
  58. * True if the participant is audio muted and false otherwise.
  59. */
  60. _isParticipantAudioMuted: boolean,
  61. /**
  62. * Shared video local participant owner.
  63. */
  64. _localVideoOwner: boolean,
  65. /**
  66. * Participant reference
  67. */
  68. _participant: Object,
  69. /**
  70. * A value between 0 and 1 indicating the volume of the participant's
  71. * audio element.
  72. */
  73. _volume: ?number,
  74. /**
  75. * Closes a drawer if open.
  76. */
  77. closeDrawer: Function,
  78. /**
  79. * An object containing the CSS classes.
  80. */
  81. classes?: {[ key: string]: string},
  82. /**
  83. * The dispatch function from redux.
  84. */
  85. dispatch: Function,
  86. /**
  87. * The participant for which the drawer is open.
  88. * It contains the displayName & participantID.
  89. */
  90. drawerParticipant: Object,
  91. /**
  92. * Callback used to open a confirmation dialog for audio muting.
  93. */
  94. muteAudio: Function,
  95. /**
  96. * Target elements against which positioning calculations are made
  97. */
  98. offsetTarget: HTMLElement,
  99. /**
  100. * Callback for the mouse entering the component
  101. */
  102. onEnter: Function,
  103. /**
  104. * Callback for the mouse leaving the component
  105. */
  106. onLeave: Function,
  107. /**
  108. * Callback for making a selection in the menu
  109. */
  110. onSelect: Function,
  111. /**
  112. * The ID of the participant.
  113. */
  114. participantID: string,
  115. /**
  116. * True if an overflow drawer should be displayed.
  117. */
  118. overflowDrawer: boolean,
  119. /**
  120. * The translate function.
  121. */
  122. t: Function
  123. };
  124. type State = {
  125. /**
  126. * If true the context menu will be hidden.
  127. */
  128. isHidden: boolean
  129. };
  130. const styles = theme => {
  131. return {
  132. drawer: {
  133. '& > div': {
  134. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
  135. lineHeight: '32px',
  136. '& svg': {
  137. fill: theme.palette.icon01
  138. }
  139. },
  140. '&:first-child': {
  141. marginTop: 15
  142. }
  143. }
  144. };
  145. };
  146. /**
  147. * Implements the MeetingParticipantContextMenu component.
  148. */
  149. class MeetingParticipantContextMenu extends Component<Props, State> {
  150. /**
  151. * Reference to the context menu container div.
  152. */
  153. _containerRef: Object;
  154. /**
  155. * Creates new instance of MeetingParticipantContextMenu.
  156. *
  157. * @param {Props} props - The props.
  158. */
  159. constructor(props: Props) {
  160. super(props);
  161. this.state = {
  162. isHidden: true
  163. };
  164. this._containerRef = React.createRef();
  165. this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
  166. this._onGrantModerator = this._onGrantModerator.bind(this);
  167. this._onKick = this._onKick.bind(this);
  168. this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
  169. this._onMuteVideo = this._onMuteVideo.bind(this);
  170. this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
  171. this._position = this._position.bind(this);
  172. this._onVolumeChange = this._onVolumeChange.bind(this);
  173. }
  174. _getCurrentParticipantId: () => string;
  175. /**
  176. * Returns the participant id for the item we want to operate.
  177. *
  178. * @returns {void}
  179. */
  180. _getCurrentParticipantId() {
  181. const { _participant, drawerParticipant, overflowDrawer } = this.props;
  182. return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
  183. }
  184. _onGrantModerator: () => void;
  185. /**
  186. * Grant moderator permissions.
  187. *
  188. * @returns {void}
  189. */
  190. _onGrantModerator() {
  191. this.props.dispatch(openDialog(GrantModeratorDialog, {
  192. participantID: this._getCurrentParticipantId()
  193. }));
  194. }
  195. _onKick: () => void;
  196. /**
  197. * Kicks the participant.
  198. *
  199. * @returns {void}
  200. */
  201. _onKick() {
  202. this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
  203. participantID: this._getCurrentParticipantId()
  204. }));
  205. }
  206. _onStopSharedVideo: () => void;
  207. /**
  208. * Stops shared video.
  209. *
  210. * @returns {void}
  211. */
  212. _onStopSharedVideo() {
  213. const { dispatch } = this.props;
  214. dispatch(this._onStopSharedVideo());
  215. }
  216. _onMuteEveryoneElse: () => void;
  217. /**
  218. * Mutes everyone else.
  219. *
  220. * @returns {void}
  221. */
  222. _onMuteEveryoneElse() {
  223. this.props.dispatch(openDialog(MuteEveryoneDialog, {
  224. exclude: [ this._getCurrentParticipantId() ]
  225. }));
  226. }
  227. _onMuteVideo: () => void;
  228. /**
  229. * Mutes the video of the selected participant.
  230. *
  231. * @returns {void}
  232. */
  233. _onMuteVideo() {
  234. this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
  235. participantID: this._getCurrentParticipantId()
  236. }));
  237. }
  238. _onSendPrivateMessage: () => void;
  239. /**
  240. * Sends private message.
  241. *
  242. * @returns {void}
  243. */
  244. _onSendPrivateMessage() {
  245. const { closeDrawer, dispatch, overflowDrawer } = this.props;
  246. dispatch(openChatById(this._getCurrentParticipantId()));
  247. overflowDrawer && closeDrawer();
  248. }
  249. _position: () => void;
  250. /**
  251. * Positions the context menu.
  252. *
  253. * @returns {void}
  254. */
  255. _position() {
  256. const { _participant, offsetTarget } = this.props;
  257. if (_participant
  258. && this._containerRef.current
  259. && offsetTarget?.offsetParent
  260. && offsetTarget.offsetParent instanceof HTMLElement
  261. ) {
  262. const { current: container } = this._containerRef;
  263. const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
  264. const outerHeight = getComputedOuterHeight(container);
  265. container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
  266. ? offsetTop - outerHeight
  267. : offsetTop;
  268. this.setState({ isHidden: false });
  269. } else {
  270. this.setState({ isHidden: true });
  271. }
  272. }
  273. _onVolumeChange: (number) => void;
  274. /**
  275. * Handles volume changes.
  276. *
  277. * @param {number} value - The new value for the volume.
  278. * @returns {void}
  279. */
  280. _onVolumeChange(value) {
  281. const { _participant, dispatch } = this.props;
  282. const { id } = _participant;
  283. dispatch(setVolume(id, value));
  284. }
  285. /**
  286. * Implements React Component's componentDidMount.
  287. *
  288. * @inheritdoc
  289. * @returns {void}
  290. */
  291. componentDidMount() {
  292. this._position();
  293. }
  294. /**
  295. * Implements React Component's componentDidUpdate.
  296. *
  297. * @inheritdoc
  298. */
  299. componentDidUpdate(prevProps: Props) {
  300. if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) {
  301. this._position();
  302. }
  303. }
  304. /**
  305. * Implements React's {@link Component#render()}.
  306. *
  307. * @inheritdoc
  308. * @returns {ReactElement}
  309. */
  310. render() {
  311. const {
  312. _isLocalModerator,
  313. _isChatButtonEnabled,
  314. _isParticipantModerator,
  315. _isParticipantVideoMuted,
  316. _isParticipantAudioMuted,
  317. _localVideoOwner,
  318. _participant,
  319. _volume = 1,
  320. classes,
  321. closeDrawer,
  322. drawerParticipant,
  323. onEnter,
  324. onLeave,
  325. onSelect,
  326. overflowDrawer,
  327. muteAudio,
  328. t
  329. } = this.props;
  330. if (!_participant) {
  331. return null;
  332. }
  333. const actions
  334. = _participant?.isFakeParticipant ? (
  335. <>
  336. {_localVideoOwner && (
  337. <ContextMenuItem onClick = { this._onStopSharedVideo }>
  338. <ContextMenuIcon src = { IconShareVideo } />
  339. <span>{t('toolbar.stopSharedVideo')}</span>
  340. </ContextMenuItem>
  341. )}
  342. </>
  343. ) : (
  344. <>
  345. {_isLocalModerator && (
  346. <ContextMenuItemGroup>
  347. <>
  348. {
  349. !_isParticipantAudioMuted && overflowDrawer
  350. && <ContextMenuItem onClick = { muteAudio(_participant) }>
  351. <ContextMenuIcon src = { IconMicDisabled } />
  352. <span>{t('dialog.muteParticipantButton')}</span>
  353. </ContextMenuItem>
  354. }
  355. <ContextMenuItem onClick = { this._onMuteEveryoneElse }>
  356. <ContextMenuIcon src = { IconMuteEveryoneElse } />
  357. <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
  358. </ContextMenuItem>
  359. </>
  360. {
  361. _isParticipantVideoMuted || (
  362. <ContextMenuItem onClick = { this._onMuteVideo }>
  363. <ContextMenuIcon src = { IconVideoOff } />
  364. <span>{t('participantsPane.actions.stopVideo')}</span>
  365. </ContextMenuItem>
  366. )
  367. }
  368. </ContextMenuItemGroup>
  369. )}
  370. <ContextMenuItemGroup>
  371. {
  372. _isLocalModerator && (
  373. <>
  374. {
  375. !_isParticipantModerator && (
  376. <ContextMenuItem onClick = { this._onGrantModerator }>
  377. <ContextMenuIcon src = { IconCrown } />
  378. <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
  379. </ContextMenuItem>
  380. )
  381. }
  382. <ContextMenuItem onClick = { this._onKick }>
  383. <ContextMenuIcon src = { IconCloseCircle } />
  384. <span>{ t('videothumbnail.kick') }</span>
  385. </ContextMenuItem>
  386. </>
  387. )
  388. }
  389. {
  390. _isChatButtonEnabled && (
  391. <ContextMenuItem onClick = { this._onSendPrivateMessage }>
  392. <ContextMenuIcon src = { IconMessage } />
  393. <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
  394. </ContextMenuItem>
  395. )
  396. }
  397. </ContextMenuItemGroup>
  398. { overflowDrawer && typeof _volume === 'number' && !isNaN(_volume)
  399. && <ContextMenuItemGroup>
  400. <VolumeSlider
  401. initialValue = { _volume }
  402. key = 'volume-slider'
  403. onChange = { this._onVolumeChange } />
  404. </ContextMenuItemGroup>
  405. }
  406. </>
  407. );
  408. return (
  409. <>
  410. { !overflowDrawer
  411. && <ContextMenu
  412. className = { ignoredChildClassName }
  413. innerRef = { this._containerRef }
  414. isHidden = { this.state.isHidden }
  415. onClick = { onSelect }
  416. onMouseEnter = { onEnter }
  417. onMouseLeave = { onLeave }>
  418. { actions }
  419. </ContextMenu>}
  420. <DrawerPortal>
  421. <Drawer
  422. isOpen = { drawerParticipant && overflowDrawer }
  423. onClose = { closeDrawer }>
  424. <div className = { classes && classes.drawer }>
  425. <ContextMenuItemGroup>
  426. <ContextMenuItem>
  427. <Avatar
  428. participantId = { drawerParticipant && drawerParticipant.participantID }
  429. size = { 20 } />
  430. <span>{ drawerParticipant && drawerParticipant.displayName }</span>
  431. </ContextMenuItem>
  432. </ContextMenuItemGroup>
  433. { actions }
  434. </div>
  435. </Drawer>
  436. </DrawerPortal>
  437. </>
  438. );
  439. }
  440. }
  441. /**
  442. * Maps (parts of) the redux state to the associated props for this component.
  443. *
  444. * @param {Object} state - The Redux state.
  445. * @param {Object} ownProps - The own props of the component.
  446. * @private
  447. * @returns {Props}
  448. */
  449. function _mapStateToProps(state, ownProps): Object {
  450. const { participantID, overflowDrawer, drawerParticipant } = ownProps;
  451. const { ownerId } = state['features/shared-video'];
  452. const localParticipantId = getLocalParticipant(state).id;
  453. const participant = getParticipantByIdOrUndefined(state,
  454. overflowDrawer ? drawerParticipant?.participantID : participantID);
  455. const _isLocalModerator = isLocalParticipantModerator(state);
  456. const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
  457. const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
  458. const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
  459. const _isParticipantModerator = isParticipantModerator(participant);
  460. const { participantsVolume } = state['features/filmstrip'];
  461. const id = participant?.id;
  462. const isLocal = participant?.local ?? true;
  463. return {
  464. _isLocalModerator,
  465. _isChatButtonEnabled,
  466. _isParticipantModerator,
  467. _isParticipantVideoMuted,
  468. _isParticipantAudioMuted,
  469. _localVideoOwner: Boolean(ownerId === localParticipantId),
  470. _participant: participant,
  471. _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
  472. };
  473. }
  474. export default withStyles(styles)(translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)));