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.

RemoteVideoMenuTriggerButton.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. // @flow
  2. import React, { Component } from 'react';
  3. import { batch } from 'react-redux';
  4. import ConnectionIndicatorContent from
  5. '../../../../features/connection-indicator/components/web/ConnectionIndicatorContent';
  6. import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
  7. import { translate } from '../../../base/i18n';
  8. import { Icon, IconMenuThumb } from '../../../base/icons';
  9. import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
  10. import { Popover } from '../../../base/popover';
  11. import { connect } from '../../../base/redux';
  12. import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
  13. import { requestRemoteControl, stopController } from '../../../remote-control';
  14. import { hideToolboxOnTileView } from '../../../toolbox/actions';
  15. import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
  16. import { renderConnectionStatus } from '../../actions.web';
  17. import ConnectionStatusButton from './ConnectionStatusButton';
  18. import MuteEveryoneElseButton from './MuteEveryoneElseButton';
  19. import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
  20. import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
  21. import {
  22. GrantModeratorButton,
  23. MuteButton,
  24. MuteVideoButton,
  25. KickButton,
  26. PrivateMessageMenuButton,
  27. RemoteControlButton,
  28. VideoMenu,
  29. VolumeSlider
  30. } from './';
  31. declare var $: Object;
  32. /**
  33. * The type of the React {@code Component} props of
  34. * {@link RemoteVideoMenuTriggerButton}.
  35. */
  36. type Props = {
  37. /**
  38. * Whether or not to display the kick button.
  39. */
  40. _disableKick: boolean,
  41. /**
  42. * Whether or not to display the remote mute buttons.
  43. */
  44. _disableRemoteMute: Boolean,
  45. /**
  46. * Whether or not to display the grant moderator button.
  47. */
  48. _disableGrantModerator: Boolean,
  49. /**
  50. * Whether or not the participant is a conference moderator.
  51. */
  52. _isModerator: boolean,
  53. /**
  54. * The position relative to the trigger the remote menu should display
  55. * from. Valid values are those supported by AtlasKit
  56. * {@code InlineDialog}.
  57. */
  58. _menuPosition: string,
  59. /**
  60. * Whether to display the Popover as a drawer.
  61. */
  62. _overflowDrawer: boolean,
  63. /**
  64. * The current state of the participant's remote control session.
  65. */
  66. _remoteControlState: number,
  67. /**
  68. * The redux dispatch function.
  69. */
  70. dispatch: Function,
  71. /**
  72. * Gets a ref to the current component instance.
  73. */
  74. getRef: Function,
  75. /**
  76. * A value between 0 and 1 indicating the volume of the participant's
  77. * audio element.
  78. */
  79. initialVolumeValue: ?number,
  80. /**
  81. * Callback to invoke when changing the level of the participant's
  82. * audio element.
  83. */
  84. onVolumeChange: Function,
  85. /**
  86. * The ID for the participant on which the remote video menu will act.
  87. */
  88. participantID: string,
  89. /**
  90. * The ID for the participant on which the remote video menu will act.
  91. */
  92. _participantDisplayName: string,
  93. /**
  94. * Whether the popover should render the Connection Info stats.
  95. */
  96. _showConnectionInfo: Boolean,
  97. /**
  98. * Invoked to obtain translated strings.
  99. */
  100. t: Function
  101. };
  102. /**
  103. * React {@code Component} for displaying an icon associated with opening the
  104. * the {@code VideoMenu}.
  105. *
  106. * @extends {Component}
  107. */
  108. class RemoteVideoMenuTriggerButton extends Component<Props> {
  109. /**
  110. * Reference to the Popover instance.
  111. */
  112. popoverRef: Object;
  113. /**
  114. * Initializes a new RemoteVideoMenuTriggerButton instance.
  115. *
  116. * @param {Object} props - The read-only React Component props with which
  117. * the new instance is to be initialized.
  118. */
  119. constructor(props: Props) {
  120. super(props);
  121. this.popoverRef = React.createRef();
  122. this._onPopoverClose = this._onPopoverClose.bind(this);
  123. this._onPopoverOpen = this._onPopoverOpen.bind(this);
  124. }
  125. /**
  126. * Triggers showing the popover's context menu.
  127. *
  128. * @returns {void}
  129. */
  130. showContextMenu() {
  131. if (this.popoverRef && this.popoverRef.current) {
  132. this.popoverRef.current.showDialog();
  133. }
  134. }
  135. /**
  136. * Calls the ref(instance) getter.
  137. *
  138. * @inheritdoc
  139. * @returns {void}
  140. */
  141. componentDidMount() {
  142. if (this.props.getRef) {
  143. this.props.getRef(this);
  144. }
  145. }
  146. /**
  147. * Calls the ref(instance) getter.
  148. *
  149. * @inheritdoc
  150. * @returns {void}
  151. */
  152. componentWillUnmount() {
  153. if (this.props.getRef) {
  154. this.props.getRef(null);
  155. }
  156. }
  157. /**
  158. * Implements React's {@link Component#render()}.
  159. *
  160. * @inheritdoc
  161. * @returns {ReactElement}
  162. */
  163. render() {
  164. const { _overflowDrawer, _showConnectionInfo, _participantDisplayName, participantID } = this.props;
  165. const content = _showConnectionInfo
  166. ? <ConnectionIndicatorContent participantId = { participantID } />
  167. : this._renderRemoteVideoMenu();
  168. if (!content) {
  169. return null;
  170. }
  171. const username = _participantDisplayName;
  172. return (
  173. <Popover
  174. content = { content }
  175. onPopoverClose = { this._onPopoverClose }
  176. onPopoverOpen = { this._onPopoverOpen }
  177. overflowDrawer = { _overflowDrawer }
  178. position = { this.props._menuPosition }
  179. ref = { this.popoverRef }>
  180. {!_overflowDrawer && (
  181. <span className = 'popover-trigger remote-video-menu-trigger'>
  182. {!isMobileBrowser() && <Icon
  183. ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
  184. role = 'button'
  185. size = '1.4em'
  186. src = { IconMenuThumb }
  187. tabIndex = { 0 }
  188. title = { this.props.t('dialog.remoteUserControls', { username }) } />
  189. }
  190. </span>
  191. )}
  192. </Popover>
  193. );
  194. }
  195. _onPopoverOpen: () => void;
  196. /**
  197. * Disable and hide toolbox while context menu is open.
  198. *
  199. * @returns {void}
  200. */
  201. _onPopoverOpen() {
  202. this.props.dispatch(setParticipantContextMenuOpen(true));
  203. this.props.dispatch(hideToolboxOnTileView());
  204. }
  205. _onPopoverClose: () => void;
  206. /**
  207. * Render normal context menu next time popover dialog opens.
  208. *
  209. * @returns {void}
  210. */
  211. _onPopoverClose() {
  212. const { dispatch } = this.props;
  213. batch(() => {
  214. dispatch(setParticipantContextMenuOpen(false));
  215. dispatch(renderConnectionStatus(false));
  216. });
  217. }
  218. /**
  219. * Creates a new {@code VideoMenu} with buttons for interacting with
  220. * the remote participant.
  221. *
  222. * @private
  223. * @returns {ReactElement}
  224. */
  225. _renderRemoteVideoMenu() {
  226. const {
  227. _disableKick,
  228. _disableRemoteMute,
  229. _disableGrantModerator,
  230. _isModerator,
  231. dispatch,
  232. initialVolumeValue,
  233. onVolumeChange,
  234. _remoteControlState,
  235. participantID
  236. } = this.props;
  237. const buttons = [];
  238. const showVolumeSlider = !isIosMobileBrowser()
  239. && onVolumeChange
  240. && typeof initialVolumeValue === 'number'
  241. && !isNaN(initialVolumeValue);
  242. if (_isModerator) {
  243. if (!_disableRemoteMute) {
  244. buttons.push(
  245. <MuteButton
  246. key = 'mute'
  247. participantID = { participantID } />
  248. );
  249. buttons.push(
  250. <MuteEveryoneElseButton
  251. key = 'mute-others'
  252. participantID = { participantID } />
  253. );
  254. buttons.push(
  255. <MuteVideoButton
  256. key = 'mute-video'
  257. participantID = { participantID } />
  258. );
  259. buttons.push(
  260. <MuteEveryoneElsesVideoButton
  261. key = 'mute-others-video'
  262. participantID = { participantID } />
  263. );
  264. }
  265. if (!_disableGrantModerator) {
  266. buttons.push(
  267. <GrantModeratorButton
  268. key = 'grant-moderator'
  269. participantID = { participantID } />
  270. );
  271. }
  272. if (!_disableKick) {
  273. buttons.push(
  274. <KickButton
  275. key = 'kick'
  276. participantID = { participantID } />
  277. );
  278. }
  279. }
  280. if (_remoteControlState) {
  281. let onRemoteControlToggle = null;
  282. if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
  283. onRemoteControlToggle = () => dispatch(stopController(true));
  284. } else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
  285. onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
  286. }
  287. buttons.push(
  288. <RemoteControlButton
  289. key = 'remote-control'
  290. onClick = { onRemoteControlToggle }
  291. participantID = { participantID }
  292. remoteControlState = { _remoteControlState } />
  293. );
  294. }
  295. buttons.push(
  296. <PrivateMessageMenuButton
  297. key = 'privateMessage'
  298. participantID = { participantID } />
  299. );
  300. if (isMobileBrowser()) {
  301. buttons.push(
  302. <ConnectionStatusButton
  303. key = 'conn-status'
  304. participantId = { participantID } />
  305. );
  306. }
  307. if (showVolumeSlider) {
  308. buttons.push(
  309. <VolumeSlider
  310. initialValue = { initialVolumeValue }
  311. key = 'volume-slider'
  312. onChange = { onVolumeChange } />
  313. );
  314. }
  315. if (buttons.length > 0) {
  316. return (
  317. <VideoMenu id = { participantID }>
  318. { buttons }
  319. </VideoMenu>
  320. );
  321. }
  322. return null;
  323. }
  324. }
  325. /**
  326. * Maps (parts of) the Redux state to the associated {@code RemoteVideoMenuTriggerButton}'s props.
  327. *
  328. * @param {Object} state - The Redux state.
  329. * @param {Object} ownProps - The own props of the component.
  330. * @private
  331. * @returns {Props}
  332. */
  333. function _mapStateToProps(state, ownProps) {
  334. const { participantID } = ownProps;
  335. const localParticipant = getLocalParticipant(state);
  336. const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
  337. const { disableKick, disableGrantModerator } = remoteVideoMenu;
  338. let _remoteControlState = null;
  339. const participant = getParticipantById(state, participantID);
  340. const _participantDisplayName = participant?.name;
  341. const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
  342. const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
  343. const { active, controller } = state['features/remote-control'];
  344. const { requestedParticipant, controlled } = controller;
  345. const activeParticipant = requestedParticipant || controlled;
  346. const { overflowDrawer } = state['features/toolbox'];
  347. const { showConnectionInfo } = state['features/base/connection'];
  348. if (_supportsRemoteControl
  349. && ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
  350. if (requestedParticipant === participantID) {
  351. _remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
  352. } else if (controlled) {
  353. _remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
  354. } else {
  355. _remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
  356. }
  357. }
  358. const currentLayout = getCurrentLayout(state);
  359. let _menuPosition;
  360. switch (currentLayout) {
  361. case LAYOUTS.TILE_VIEW:
  362. _menuPosition = 'left-start';
  363. break;
  364. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  365. _menuPosition = 'left-end';
  366. break;
  367. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
  368. _menuPosition = 'top';
  369. break;
  370. default:
  371. _menuPosition = 'auto';
  372. }
  373. return {
  374. _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
  375. _disableKick: Boolean(disableKick),
  376. _disableRemoteMute: Boolean(disableRemoteMute),
  377. _remoteControlState,
  378. _menuPosition,
  379. _overflowDrawer: overflowDrawer,
  380. _participantDisplayName,
  381. _disableGrantModerator: Boolean(disableGrantModerator),
  382. _showConnectionInfo: showConnectionInfo
  383. };
  384. }
  385. export default translate(connect(_mapStateToProps)(RemoteVideoMenuTriggerButton));