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

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