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

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