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

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