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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. // @flow
  2. import React, { Component } from 'react';
  3. import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
  4. import { openDialog } from '../../../base/dialog';
  5. import { translate } from '../../../base/i18n';
  6. import {
  7. IconCloseCircle,
  8. IconCrown,
  9. IconMessage,
  10. IconMicDisabled,
  11. IconMuteEveryoneElse,
  12. IconShareVideo,
  13. IconVideoOff
  14. } from '../../../base/icons';
  15. import {
  16. getLocalParticipant,
  17. getParticipantByIdOrUndefined,
  18. isLocalParticipantModerator,
  19. isParticipantModerator
  20. } from '../../../base/participants';
  21. import { connect } from '../../../base/redux';
  22. import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
  23. import { openChat } from '../../../chat/actions';
  24. import { stopSharedVideo } from '../../../shared-video/actions.any';
  25. import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
  26. import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
  27. import { getComputedOuterHeight } from '../../functions';
  28. import {
  29. ContextMenu,
  30. ContextMenuIcon,
  31. ContextMenuItem,
  32. ContextMenuItemGroup,
  33. ignoredChildClassName
  34. } from './styled';
  35. type Props = {
  36. /**
  37. * True if the local participant is moderator and false otherwise.
  38. */
  39. _isLocalModerator: boolean,
  40. /**
  41. * True if the chat button is enabled and false otherwise.
  42. */
  43. _isChatButtonEnabled: boolean,
  44. /**
  45. * True if the participant is moderator and false otherwise.
  46. */
  47. _isParticipantModerator: boolean,
  48. /**
  49. * True if the participant is video muted and false otherwise.
  50. */
  51. _isParticipantVideoMuted: boolean,
  52. /**
  53. * True if the participant is audio muted and false otherwise.
  54. */
  55. _isParticipantAudioMuted: boolean,
  56. /**
  57. * Shared video local participant owner.
  58. */
  59. _localVideoOwner: boolean,
  60. /**
  61. * Participant reference
  62. */
  63. _participant: Object,
  64. /**
  65. * The dispatch function from redux.
  66. */
  67. dispatch: Function,
  68. /**
  69. * Callback used to open a confirmation dialog for audio muting.
  70. */
  71. muteAudio: Function,
  72. /**
  73. * Target elements against which positioning calculations are made
  74. */
  75. offsetTarget: HTMLElement,
  76. /**
  77. * Callback for the mouse entering the component
  78. */
  79. onEnter: Function,
  80. /**
  81. * Callback for the mouse leaving the component
  82. */
  83. onLeave: Function,
  84. /**
  85. * Callback for making a selection in the menu
  86. */
  87. onSelect: Function,
  88. /**
  89. * The ID of the participant.
  90. */
  91. participantID: string,
  92. /**
  93. * The translate function.
  94. */
  95. t: Function
  96. };
  97. type State = {
  98. /**
  99. * If true the context menu will be hidden.
  100. */
  101. isHidden: boolean
  102. };
  103. /**
  104. * Implements the MeetingParticipantContextMenu component.
  105. */
  106. class MeetingParticipantContextMenu extends Component<Props, State> {
  107. /**
  108. * Reference to the context menu container div.
  109. */
  110. _containerRef: Object;
  111. /**
  112. * Creates new instance of MeetingParticipantContextMenu.
  113. *
  114. * @param {Props} props - The props.
  115. */
  116. constructor(props: Props) {
  117. super(props);
  118. this.state = {
  119. isHidden: true
  120. };
  121. this._containerRef = React.createRef();
  122. this._onGrantModerator = this._onGrantModerator.bind(this);
  123. this._onKick = this._onKick.bind(this);
  124. this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
  125. this._onMuteVideo = this._onMuteVideo.bind(this);
  126. this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
  127. this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
  128. this._position = this._position.bind(this);
  129. }
  130. _onGrantModerator: () => void;
  131. /**
  132. * Grant moderator permissions.
  133. *
  134. * @returns {void}
  135. */
  136. _onGrantModerator() {
  137. const { _participant, dispatch } = this.props;
  138. dispatch(openDialog(GrantModeratorDialog, {
  139. participantID: _participant?.id
  140. }));
  141. }
  142. _onKick: () => void;
  143. /**
  144. * Kicks the participant.
  145. *
  146. * @returns {void}
  147. */
  148. _onKick() {
  149. const { _participant, dispatch } = this.props;
  150. dispatch(openDialog(KickRemoteParticipantDialog, {
  151. participantID: _participant?.id
  152. }));
  153. }
  154. _onStopSharedVideo: () => void;
  155. /**
  156. * Stops shared video.
  157. *
  158. * @returns {void}
  159. */
  160. _onStopSharedVideo() {
  161. const { dispatch } = this.props;
  162. dispatch(stopSharedVideo());
  163. }
  164. _onMuteEveryoneElse: () => void;
  165. /**
  166. * Mutes everyone else.
  167. *
  168. * @returns {void}
  169. */
  170. _onMuteEveryoneElse() {
  171. const { _participant, dispatch } = this.props;
  172. dispatch(openDialog(MuteEveryoneDialog, {
  173. exclude: [ _participant?.id ]
  174. }));
  175. }
  176. _onMuteVideo: () => void;
  177. /**
  178. * Mutes the video of the selected participant.
  179. *
  180. * @returns {void}
  181. */
  182. _onMuteVideo() {
  183. const { _participant, dispatch } = this.props;
  184. dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
  185. participantID: _participant?.id
  186. }));
  187. }
  188. _onSendPrivateMessage: () => void;
  189. /**
  190. * Sends private message.
  191. *
  192. * @returns {void}
  193. */
  194. _onSendPrivateMessage() {
  195. const { _participant, dispatch } = this.props;
  196. dispatch(openChat(_participant));
  197. }
  198. _position: () => void;
  199. /**
  200. * Positions the context menu.
  201. *
  202. * @returns {void}
  203. */
  204. _position() {
  205. const { _participant, offsetTarget } = this.props;
  206. if (_participant
  207. && this._containerRef.current
  208. && offsetTarget?.offsetParent
  209. && offsetTarget.offsetParent instanceof HTMLElement
  210. ) {
  211. const { current: container } = this._containerRef;
  212. const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
  213. const outerHeight = getComputedOuterHeight(container);
  214. container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
  215. ? offsetTop - outerHeight
  216. : offsetTop;
  217. this.setState({ isHidden: false });
  218. } else {
  219. this.setState({ isHidden: true });
  220. }
  221. }
  222. /**
  223. * Implements React Component's componentDidMount.
  224. *
  225. * @inheritdoc
  226. * @returns {void}
  227. */
  228. componentDidMount() {
  229. this._position();
  230. }
  231. /**
  232. * Implements React Component's componentDidUpdate.
  233. *
  234. * @inheritdoc
  235. */
  236. componentDidUpdate(prevProps: Props) {
  237. if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) {
  238. this._position();
  239. }
  240. }
  241. /**
  242. * Implements React's {@link Component#render()}.
  243. *
  244. * @inheritdoc
  245. * @returns {ReactElement}
  246. */
  247. render() {
  248. const {
  249. _isLocalModerator,
  250. _isChatButtonEnabled,
  251. _isParticipantModerator,
  252. _isParticipantVideoMuted,
  253. _isParticipantAudioMuted,
  254. _localVideoOwner,
  255. _participant,
  256. onEnter,
  257. onLeave,
  258. onSelect,
  259. muteAudio,
  260. t
  261. } = this.props;
  262. if (!_participant) {
  263. return null;
  264. }
  265. return (
  266. <ContextMenu
  267. className = { ignoredChildClassName }
  268. innerRef = { this._containerRef }
  269. isHidden = { this.state.isHidden }
  270. onClick = { onSelect }
  271. onMouseEnter = { onEnter }
  272. onMouseLeave = { onLeave }>
  273. {
  274. !_participant?.isFakeParticipant && (
  275. <>
  276. <ContextMenuItemGroup>
  277. {
  278. _isLocalModerator && (
  279. <>
  280. {
  281. !_isParticipantAudioMuted
  282. && <ContextMenuItem onClick = { muteAudio(_participant) }>
  283. <ContextMenuIcon src = { IconMicDisabled } />
  284. <span>{t('dialog.muteParticipantButton')}</span>
  285. </ContextMenuItem>
  286. }
  287. <ContextMenuItem onClick = { this._onMuteEveryoneElse }>
  288. <ContextMenuIcon src = { IconMuteEveryoneElse } />
  289. <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
  290. </ContextMenuItem>
  291. </>
  292. )
  293. }
  294. {
  295. _isLocalModerator && (
  296. _isParticipantVideoMuted || (
  297. <ContextMenuItem onClick = { this._onMuteVideo }>
  298. <ContextMenuIcon src = { IconVideoOff } />
  299. <span>{t('participantsPane.actions.stopVideo')}</span>
  300. </ContextMenuItem>
  301. )
  302. )
  303. }
  304. </ContextMenuItemGroup>
  305. <ContextMenuItemGroup>
  306. {
  307. _isLocalModerator && (
  308. <>
  309. {
  310. !_isParticipantModerator && (
  311. <ContextMenuItem onClick = { this._onGrantModerator }>
  312. <ContextMenuIcon src = { IconCrown } />
  313. <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
  314. </ContextMenuItem>
  315. )
  316. }
  317. <ContextMenuItem onClick = { this._onKick }>
  318. <ContextMenuIcon src = { IconCloseCircle } />
  319. <span>{ t('videothumbnail.kick') }</span>
  320. </ContextMenuItem>
  321. </>
  322. )
  323. }
  324. {
  325. _isChatButtonEnabled && (
  326. <ContextMenuItem onClick = { this._onSendPrivateMessage }>
  327. <ContextMenuIcon src = { IconMessage } />
  328. <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
  329. </ContextMenuItem>
  330. )
  331. }
  332. </ContextMenuItemGroup>
  333. </>
  334. )
  335. }
  336. {
  337. _participant?.isFakeParticipant && _localVideoOwner && (
  338. <ContextMenuItem onClick = { this._onStopSharedVideo }>
  339. <ContextMenuIcon src = { IconShareVideo } />
  340. <span>{t('toolbar.stopSharedVideo')}</span>
  341. </ContextMenuItem>
  342. )
  343. }
  344. </ContextMenu>
  345. );
  346. }
  347. }
  348. /**
  349. * Maps (parts of) the redux state to the associated props for this component.
  350. *
  351. * @param {Object} state - The Redux state.
  352. * @param {Object} ownProps - The own props of the component.
  353. * @private
  354. * @returns {Props}
  355. */
  356. function _mapStateToProps(state, ownProps): Object {
  357. const { participantID } = ownProps;
  358. const { ownerId } = state['features/shared-video'];
  359. const localParticipantId = getLocalParticipant(state).id;
  360. const participant = getParticipantByIdOrUndefined(state, participantID);
  361. const _isLocalModerator = isLocalParticipantModerator(state);
  362. const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
  363. const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
  364. const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
  365. const _isParticipantModerator = isParticipantModerator(participant);
  366. return {
  367. _isLocalModerator,
  368. _isChatButtonEnabled,
  369. _isParticipantModerator,
  370. _isParticipantVideoMuted,
  371. _isParticipantAudioMuted,
  372. _localVideoOwner: Boolean(ownerId === localParticipantId),
  373. _participant: participant
  374. };
  375. }
  376. export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));