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.

DesktopPicker.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. // @flow
  2. import Tabs from '@atlaskit/tabs';
  3. import PropTypes from 'prop-types';
  4. import React, { Component } from 'react';
  5. import { connect } from 'react-redux';
  6. import { Dialog, hideDialog } from '../../base/dialog';
  7. import { translate } from '../../base/i18n';
  8. import DesktopPickerPane from './DesktopPickerPane';
  9. import { obtainDesktopSources } from '../functions';
  10. /**
  11. * The size of the requested thumbnails.
  12. *
  13. * @type {Object}
  14. */
  15. const THUMBNAIL_SIZE = {
  16. height: 300,
  17. width: 300
  18. };
  19. /**
  20. * The sources polling interval in ms.
  21. *
  22. * @type {int}
  23. */
  24. const UPDATE_INTERVAL = 2000;
  25. /**
  26. * The default selected tab.
  27. *
  28. * @type {string}
  29. */
  30. const DEFAULT_TAB_TYPE = 'screen';
  31. const TAB_LABELS = {
  32. screen: 'dialog.yourEntireScreen',
  33. window: 'dialog.applicationWindow'
  34. };
  35. const VALID_TYPES = Object.keys(TAB_LABELS);
  36. /**
  37. * React component for DesktopPicker.
  38. *
  39. * @extends Component
  40. */
  41. class DesktopPicker extends Component<*, *> {
  42. /**
  43. * DesktopPicker component's property types.
  44. *
  45. * @static
  46. */
  47. static propTypes = {
  48. /**
  49. * An array with desktop sharing sources to be displayed.
  50. */
  51. desktopSharingSources: PropTypes.arrayOf(PropTypes.string),
  52. /**
  53. * Used to request DesktopCapturerSources.
  54. */
  55. dispatch: PropTypes.func,
  56. /**
  57. * The callback to be invoked when the component is closed or when
  58. * a DesktopCapturerSource has been chosen.
  59. */
  60. onSourceChoose: PropTypes.func,
  61. /**
  62. * Used to obtain translations.
  63. */
  64. t: PropTypes.func
  65. };
  66. _poller = null;
  67. state = {
  68. selectedSource: {},
  69. selectedTab: 0,
  70. sources: {},
  71. types: []
  72. };
  73. /**
  74. * Stores the type of the selected tab.
  75. *
  76. * @type {string}
  77. */
  78. _selectedTabType = DEFAULT_TAB_TYPE;
  79. /**
  80. * Initializes a new DesktopPicker instance.
  81. *
  82. * @param {Object} props - The read-only properties with which the new
  83. * instance is to be initialized.
  84. */
  85. constructor(props) {
  86. super(props);
  87. // Bind event handlers so they are only bound once per instance.
  88. this._onCloseModal = this._onCloseModal.bind(this);
  89. this._onPreviewClick = this._onPreviewClick.bind(this);
  90. this._onSubmit = this._onSubmit.bind(this);
  91. this._onTabSelected = this._onTabSelected.bind(this);
  92. this._updateSources = this._updateSources.bind(this);
  93. this.state.types
  94. = this._getValidTypes(this.props.desktopSharingSources);
  95. }
  96. /**
  97. * Starts polling.
  98. *
  99. * @inheritdoc
  100. * @returns {void}
  101. */
  102. componentDidMount() {
  103. this._startPolling();
  104. }
  105. /**
  106. * Notifies this mounted React Component that it will receive new props.
  107. * Sets a default selected source if one is not already set.
  108. *
  109. * @inheritdoc
  110. * @param {Object} nextProps - The read-only React Component props that this
  111. * instance will receive.
  112. * @returns {void}
  113. */
  114. componentWillReceiveProps(nextProps) {
  115. const { desktopSharingSources } = nextProps;
  116. /**
  117. * Do only reference check in order to not calculate the types on every
  118. * update. This is enough for our use case and we don't need value
  119. * checking because if the value is the same we won't change the
  120. * reference for the desktopSharingSources array.
  121. */
  122. if (desktopSharingSources !== this.props.desktopSharingSources) {
  123. this.setState({
  124. types: this._getValidTypes(desktopSharingSources)
  125. });
  126. }
  127. }
  128. /**
  129. * Clean up component and DesktopCapturerSource store state.
  130. *
  131. * @inheritdoc
  132. */
  133. componentWillUnmount() {
  134. this._stopPolling();
  135. }
  136. /**
  137. * Implements React's {@link Component#render()}.
  138. *
  139. * @inheritdoc
  140. */
  141. render() {
  142. return (
  143. <Dialog
  144. isModal = { false }
  145. okDisabled = { Boolean(!this.state.selectedSource.id) }
  146. okTitleKey = 'dialog.Share'
  147. onCancel = { this._onCloseModal }
  148. onSubmit = { this._onSubmit }
  149. titleKey = 'dialog.shareYourScreen'
  150. width = 'medium' >
  151. { this._renderTabs() }
  152. </Dialog>
  153. );
  154. }
  155. /**
  156. * Computates the selected source.
  157. *
  158. * @param {Object} sources - The available sources.
  159. * @returns {Object} The selectedSource value.
  160. */
  161. _getSelectedSource(sources = {}) {
  162. const { selectedSource } = this.state;
  163. /**
  164. * If there are no sources for this type (or no sources for any type)
  165. * we can't select anything.
  166. */
  167. if (!Array.isArray(sources[this._selectedTabType])
  168. || sources[this._selectedTabType].length <= 0) {
  169. return {};
  170. }
  171. /**
  172. * Select the first available source for this type in the following
  173. * scenarios:
  174. * 1) Nothing is yet selected.
  175. * 2) Tab change.
  176. * 3) The selected source is no longer available.
  177. */
  178. if (!selectedSource // scenario 1)
  179. || selectedSource.type !== this._selectedTabType // scenario 2)
  180. || !sources[this._selectedTabType].some( // scenario 3)
  181. source => source.id === selectedSource.id)) {
  182. return {
  183. id: sources[this._selectedTabType][0].id,
  184. type: this._selectedTabType
  185. };
  186. }
  187. /**
  188. * For all other scenarios don't change the selection.
  189. */
  190. return selectedSource;
  191. }
  192. /**
  193. * Extracts only the valid types from the passed {@code types}.
  194. *
  195. * @param {Array<string>} types - The types to filter.
  196. * @returns {Array<string>} The filtered types.
  197. */
  198. _getValidTypes(types = []) {
  199. return types.filter(
  200. type => VALID_TYPES.includes(type));
  201. }
  202. _onCloseModal: (?string, string) => void;
  203. /**
  204. * Dispatches an action to hide the DesktopPicker and invokes the passed in
  205. * callback with a selectedSource, if any.
  206. *
  207. * @param {string} [id] - The id of the DesktopCapturerSource to pass into
  208. * the onSourceChoose callback.
  209. * @param {string} type - The type of the DesktopCapturerSource to pass into
  210. * the onSourceChoose callback.
  211. * @returns {void}
  212. */
  213. _onCloseModal(id = '', type) {
  214. this.props.onSourceChoose(id, type);
  215. this.props.dispatch(hideDialog());
  216. }
  217. _onPreviewClick: (string, string) => void;
  218. /**
  219. * Sets the currently selected DesktopCapturerSource.
  220. *
  221. * @param {string} id - The id of DesktopCapturerSource.
  222. * @param {string} type - The type of DesktopCapturerSource.
  223. * @returns {void}
  224. */
  225. _onPreviewClick(id, type) {
  226. this.setState({
  227. selectedSource: {
  228. id,
  229. type
  230. }
  231. });
  232. }
  233. _onSubmit: () => void;
  234. /**
  235. * Request to close the modal and execute callbacks with the selected source
  236. * id.
  237. *
  238. * @returns {void}
  239. */
  240. _onSubmit() {
  241. const { id, type } = this.state.selectedSource;
  242. this._onCloseModal(id, type);
  243. }
  244. _onTabSelected: () => void;
  245. /**
  246. * Stores the selected tab and updates the selected source via
  247. * {@code _getSelectedSource}.
  248. *
  249. * @param {Object} tab - The configuration passed into atlaskit tabs to
  250. * describe how to display the selected tab.
  251. * @param {number} tabIndex - The index of the tab within the array of
  252. * displayed tabs.
  253. * @returns {void}
  254. */
  255. _onTabSelected(tab, tabIndex) { // eslint-disable-line no-unused-vars
  256. const { types, sources } = this.state;
  257. this._selectedTabType = types[tabIndex];
  258. this.setState({
  259. selectedSource: this._getSelectedSource(sources),
  260. selectedTab: tabIndex
  261. });
  262. }
  263. /**
  264. * Configures and renders the tabs for display.
  265. *
  266. * @private
  267. * @returns {ReactElement}
  268. */
  269. _renderTabs() {
  270. const { selectedSource, sources, types } = this.state;
  271. const { t } = this.props;
  272. const tabs
  273. = types.map(
  274. type => {
  275. return {
  276. content: <DesktopPickerPane
  277. key = { type }
  278. onClick = { this._onPreviewClick }
  279. onDoubleClick = { this._onCloseModal }
  280. selectedSourceId = { selectedSource.id }
  281. sources = { sources[type] }
  282. type = { type } />,
  283. label: t(TAB_LABELS[type])
  284. };
  285. });
  286. return (
  287. <Tabs
  288. onSelect = { this._onTabSelected }
  289. selected = { this.state.selectedTab }
  290. tabs = { tabs } />);
  291. }
  292. /**
  293. * Create an interval to update knwon available DesktopCapturerSources.
  294. *
  295. * @private
  296. * @returns {void}
  297. */
  298. _startPolling() {
  299. this._stopPolling();
  300. this._updateSources();
  301. this._poller = window.setInterval(this._updateSources, UPDATE_INTERVAL);
  302. }
  303. /**
  304. * Cancels the interval to update DesktopCapturerSources.
  305. *
  306. * @private
  307. * @returns {void}
  308. */
  309. _stopPolling() {
  310. window.clearInterval(this._poller);
  311. this._poller = null;
  312. }
  313. _updateSources: () => void;
  314. /**
  315. * Obtains the desktop sources and updates state with them.
  316. *
  317. * @private
  318. * @returns {void}
  319. */
  320. _updateSources() {
  321. const { types } = this.state;
  322. if (types.length > 0) {
  323. obtainDesktopSources(
  324. this.state.types,
  325. { thumbnailSize: THUMBNAIL_SIZE }
  326. )
  327. .then(sources => {
  328. const selectedSource = this._getSelectedSource(sources);
  329. // TODO: Maybe check if we have stopped the timer and unmounted
  330. // the component.
  331. this.setState({
  332. sources,
  333. selectedSource
  334. });
  335. })
  336. .catch(() => { /* ignore */ });
  337. }
  338. }
  339. }
  340. export default translate(connect()(DesktopPicker));