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

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