Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

DesktopPicker.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import React, { PureComponent } from 'react';
  2. import { WithTranslation } from 'react-i18next';
  3. import { connect } from 'react-redux';
  4. import { IStore } from '../../app/types';
  5. import { hideDialog } from '../../base/dialog/actions';
  6. import { translate } from '../../base/i18n/functions';
  7. import { DesktopSharingSourceType } from '../../base/tracks/types';
  8. import Dialog from '../../base/ui/components/web/Dialog';
  9. import Tabs from '../../base/ui/components/web/Tabs';
  10. import { THUMBNAIL_SIZE } from '../constants';
  11. import { obtainDesktopSources } from '../functions';
  12. import logger from '../logger';
  13. import DesktopPickerPane from './DesktopPickerPane';
  14. /**
  15. * The sources polling interval in ms.
  16. *
  17. * @type {int}
  18. */
  19. const UPDATE_INTERVAL = 2000;
  20. /**
  21. * The default selected tab.
  22. *
  23. * @type {string}
  24. */
  25. const DEFAULT_TAB_TYPE = 'screen';
  26. const TAB_LABELS = {
  27. screen: 'dialog.yourEntireScreen',
  28. window: 'dialog.applicationWindow'
  29. };
  30. const VALID_TYPES = Object.keys(TAB_LABELS) as DesktopSharingSourceType[];
  31. /**
  32. * The type of the React {@code Component} props of {@link DesktopPicker}.
  33. */
  34. interface IProps extends WithTranslation {
  35. /**
  36. * An array with desktop sharing sources to be displayed.
  37. */
  38. desktopSharingSources: Array<DesktopSharingSourceType>;
  39. /**
  40. * Used to request DesktopCapturerSources.
  41. */
  42. dispatch: IStore['dispatch'];
  43. /**
  44. * The callback to be invoked when the component is closed or when a
  45. * DesktopCapturerSource has been chosen.
  46. */
  47. onSourceChoose: Function;
  48. }
  49. /**
  50. * The type of the React {@code Component} state of {@link DesktopPicker}.
  51. */
  52. interface IState {
  53. /**
  54. * The state of the audio screen share checkbox.
  55. */
  56. screenShareAudio: boolean;
  57. /**
  58. * The currently highlighted DesktopCapturerSource.
  59. */
  60. selectedSource: any;
  61. /**
  62. * The desktop source type currently being displayed.
  63. */
  64. selectedTab: string;
  65. /**
  66. * An object containing all the DesktopCapturerSources.
  67. */
  68. sources: any;
  69. /**
  70. * The desktop source types to fetch previews for.
  71. */
  72. types: Array<DesktopSharingSourceType>;
  73. }
  74. /**
  75. * React component for DesktopPicker.
  76. *
  77. * @augments Component
  78. */
  79. class DesktopPicker extends PureComponent<IProps, IState> {
  80. /**
  81. * Implements React's {@link Component#getDerivedStateFromProps()}.
  82. *
  83. * @inheritdoc
  84. */
  85. static getDerivedStateFromProps(props: IProps) {
  86. return {
  87. types: DesktopPicker._getValidTypes(props.desktopSharingSources)
  88. };
  89. }
  90. /**
  91. * Extracts only the valid types from the passed {@code types}.
  92. *
  93. * @param {Array<string>} types - The types to filter.
  94. * @private
  95. * @returns {Array<string>} The filtered types.
  96. */
  97. static _getValidTypes(types: DesktopSharingSourceType[] = []) {
  98. return types.filter(
  99. type => VALID_TYPES.includes(type));
  100. }
  101. _poller: any = null;
  102. state: IState = {
  103. screenShareAudio: false,
  104. selectedSource: {},
  105. selectedTab: DEFAULT_TAB_TYPE,
  106. sources: {},
  107. types: []
  108. };
  109. /**
  110. * Initializes a new DesktopPicker instance.
  111. *
  112. * @param {Object} props - The read-only properties with which the new
  113. * instance is to be initialized.
  114. */
  115. constructor(props: IProps) {
  116. super(props);
  117. // Bind event handlers so they are only bound once per instance.
  118. this._onCloseModal = this._onCloseModal.bind(this);
  119. this._onPreviewClick = this._onPreviewClick.bind(this);
  120. this._onShareAudioChecked = this._onShareAudioChecked.bind(this);
  121. this._onSubmit = this._onSubmit.bind(this);
  122. this._onTabSelected = this._onTabSelected.bind(this);
  123. this._updateSources = this._updateSources.bind(this);
  124. this.state.types
  125. = DesktopPicker._getValidTypes(this.props.desktopSharingSources);
  126. }
  127. /**
  128. * Starts polling.
  129. *
  130. * @inheritdoc
  131. * @returns {void}
  132. */
  133. componentDidMount() {
  134. this._startPolling();
  135. }
  136. /**
  137. * Clean up component and DesktopCapturerSource store state.
  138. *
  139. * @inheritdoc
  140. */
  141. componentWillUnmount() {
  142. this._stopPolling();
  143. }
  144. /**
  145. * Implements React's {@link Component#render()}.
  146. *
  147. * @inheritdoc
  148. */
  149. render() {
  150. const { selectedTab, selectedSource, sources, types } = this.state;
  151. return (
  152. <Dialog
  153. ok = {{
  154. disabled: Boolean(!this.state.selectedSource.id),
  155. translationKey: 'dialog.Share'
  156. }}
  157. onCancel = { this._onCloseModal }
  158. onSubmit = { this._onSubmit }
  159. size = 'large'
  160. titleKey = 'dialog.shareYourScreen'>
  161. { this._renderTabs() }
  162. {types.map(type => (
  163. <div
  164. aria-labelledby = { `${type}-button` }
  165. className = { selectedTab === type ? undefined : 'hide' }
  166. id = { `${type}-panel` }
  167. key = { type }
  168. role = 'tabpanel'
  169. tabIndex = { 0 }>
  170. {selectedTab === type && (
  171. <DesktopPickerPane
  172. key = { selectedTab }
  173. onClick = { this._onPreviewClick }
  174. onDoubleClick = { this._onSubmit }
  175. onShareAudioChecked = { this._onShareAudioChecked }
  176. selectedSourceId = { selectedSource.id }
  177. sources = { sources[selectedTab as keyof typeof sources] }
  178. type = { selectedTab } />
  179. )}
  180. </div>
  181. ))}
  182. </Dialog>
  183. );
  184. }
  185. /**
  186. * Computes the selected source.
  187. *
  188. * @param {Object} sources - The available sources.
  189. * @param {string} selectedTab - The selected tab.
  190. * @returns {Object} The selectedSource value.
  191. */
  192. _getSelectedSource(sources: any = {}, selectedTab?: string) {
  193. const { selectedSource } = this.state;
  194. const currentSelectedTab = selectedTab ?? this.state.selectedTab;
  195. /**
  196. * If there are no sources for this type (or no sources for any type)
  197. * we can't select anything.
  198. */
  199. if (!Array.isArray(sources[currentSelectedTab as keyof typeof sources])
  200. || sources[currentSelectedTab as keyof typeof sources].length <= 0) {
  201. return {};
  202. }
  203. /**
  204. * Select the first available source for this type in the following
  205. * scenarios:
  206. * 1) Nothing is yet selected.
  207. * 2) Tab change.
  208. * 3) The selected source is no longer available.
  209. */
  210. if (!selectedSource // scenario 1)
  211. || selectedSource.type !== currentSelectedTab // scenario 2)
  212. || !sources[currentSelectedTab].some( // scenario 3)
  213. (source: any) => source.id === selectedSource.id)) {
  214. return {
  215. id: sources[currentSelectedTab][0].id,
  216. type: currentSelectedTab
  217. };
  218. }
  219. /**
  220. * For all other scenarios don't change the selection.
  221. */
  222. return selectedSource;
  223. }
  224. /**
  225. * Dispatches an action to hide the DesktopPicker and invokes the passed in
  226. * callback with a selectedSource, if any.
  227. *
  228. * @param {string} [id] - The id of the DesktopCapturerSource to pass into
  229. * the onSourceChoose callback.
  230. * @param {string} type - The type of the DesktopCapturerSource to pass into
  231. * the onSourceChoose callback.
  232. * @param {boolean} screenShareAudio - Whether or not to add system audio to
  233. * screen sharing session.
  234. * @returns {void}
  235. */
  236. _onCloseModal(id = '', type?: string, screenShareAudio = false) {
  237. this.props.onSourceChoose(id, type, screenShareAudio);
  238. this.props.dispatch(hideDialog());
  239. }
  240. /**
  241. * Sets the currently selected DesktopCapturerSource.
  242. *
  243. * @param {string} id - The id of DesktopCapturerSource.
  244. * @param {string} type - The type of DesktopCapturerSource.
  245. * @returns {void}
  246. */
  247. _onPreviewClick(id: string, type: string) {
  248. this.setState({
  249. selectedSource: {
  250. id,
  251. type
  252. }
  253. });
  254. }
  255. /**
  256. * Request to close the modal and execute callbacks with the selected source
  257. * id.
  258. *
  259. * @returns {void}
  260. */
  261. _onSubmit() {
  262. const { selectedSource: { id, type }, screenShareAudio } = this.state;
  263. this._onCloseModal(id, type, screenShareAudio);
  264. }
  265. /**
  266. * Stores the selected tab and updates the selected source via
  267. * {@code _getSelectedSource}.
  268. *
  269. * @param {string} id - The id of the newly selected tab.
  270. * @returns {void}
  271. */
  272. _onTabSelected(id: string) {
  273. const { sources } = this.state;
  274. // When we change tabs also reset the screenShareAudio state so we don't
  275. // use the option from one tab when sharing from another.
  276. this.setState({
  277. screenShareAudio: false,
  278. selectedSource: this._getSelectedSource(sources, id),
  279. // select type `window` or `screen` from id
  280. selectedTab: id
  281. });
  282. }
  283. /**
  284. * Set the screenSharingAudio state indicating whether or not to also share
  285. * system audio.
  286. *
  287. * @param {boolean} checked - Share audio or not.
  288. * @returns {void}
  289. */
  290. _onShareAudioChecked(checked: boolean) {
  291. this.setState({ screenShareAudio: checked });
  292. }
  293. /**
  294. * Configures and renders the tabs for display.
  295. *
  296. * @private
  297. * @returns {ReactElement}
  298. */
  299. _renderTabs() {
  300. const { types } = this.state;
  301. const { t } = this.props;
  302. const tabs
  303. = types.map(
  304. type => {
  305. return {
  306. accessibilityLabel: t(TAB_LABELS[type]),
  307. id: `${type}`,
  308. controlsId: `${type}-panel`,
  309. label: t(TAB_LABELS[type])
  310. };
  311. });
  312. return (
  313. <Tabs
  314. accessibilityLabel = { t('dialog.sharingTabs') }
  315. className = 'desktop-picker-tabs-container'
  316. onChange = { this._onTabSelected }
  317. selected = { `${this.state.selectedTab}` }
  318. tabs = { tabs } />
  319. );
  320. }
  321. /**
  322. * Create an interval to update known available DesktopCapturerSources.
  323. *
  324. * @private
  325. * @returns {void}
  326. */
  327. _startPolling() {
  328. this._stopPolling();
  329. this._updateSources();
  330. this._poller = window.setInterval(this._updateSources, UPDATE_INTERVAL);
  331. }
  332. /**
  333. * Cancels the interval to update DesktopCapturerSources.
  334. *
  335. * @private
  336. * @returns {void}
  337. */
  338. _stopPolling() {
  339. window.clearInterval(this._poller);
  340. this._poller = null;
  341. }
  342. /**
  343. * Obtains the desktop sources and updates state with them.
  344. *
  345. * @private
  346. * @returns {void}
  347. */
  348. _updateSources() {
  349. const { types } = this.state;
  350. const options = {
  351. types,
  352. thumbnailSize: THUMBNAIL_SIZE
  353. };
  354. if (types.length > 0) {
  355. obtainDesktopSources(options)
  356. .then((sources: any) => {
  357. const selectedSource = this._getSelectedSource(sources);
  358. this.setState({
  359. selectedSource,
  360. sources
  361. });
  362. })
  363. .catch((error: any) => logger.log(error));
  364. }
  365. }
  366. }
  367. export default translate(connect()(DesktopPicker));