From 24ee8eb16a8f5c16a23dc2fe6a87c7743a3dcb6e Mon Sep 17 00:00:00 2001 From: virtuacoplenny Date: Thu, 30 Mar 2017 09:58:31 -0700 Subject: [PATCH] electron: add desktop picker #1411 --- conference.js | 13 + css/_chat.scss | 10 +- css/main.scss | 1 + .../desktop-picker/_desktop-picker.scss | 59 ++++ lang/main.json | 5 +- package.json | 1 + react/features/desktop-picker/actionTypes.js | 20 ++ react/features/desktop-picker/actions.js | 87 ++++++ .../components/DesktopPicker.js | 264 ++++++++++++++++++ .../components/DesktopPickerPane.js | 69 +++++ .../components/DesktopSourcePreview.js | 97 +++++++ .../desktop-picker/components/index.js | 1 + react/features/desktop-picker/index.js | 5 + react/features/desktop-picker/reducer.js | 59 ++++ 14 files changed, 685 insertions(+), 6 deletions(-) create mode 100644 css/modals/desktop-picker/_desktop-picker.scss create mode 100644 react/features/desktop-picker/actionTypes.js create mode 100644 react/features/desktop-picker/actions.js create mode 100644 react/features/desktop-picker/components/DesktopPicker.js create mode 100644 react/features/desktop-picker/components/DesktopPickerPane.js create mode 100644 react/features/desktop-picker/components/DesktopSourcePreview.js create mode 100644 react/features/desktop-picker/components/index.js create mode 100644 react/features/desktop-picker/index.js create mode 100644 react/features/desktop-picker/reducer.js diff --git a/conference.js b/conference.js index 8c7b4ac11..1e5e62085 100644 --- a/conference.js +++ b/conference.js @@ -39,6 +39,9 @@ import { participantLeft, participantRoleChanged } from './react/features/base/participants'; +import { + showDesktopPicker +} from './react/features/desktop-picker'; import { mediaPermissionPromptVisibilityChanged, suspendDetected @@ -66,6 +69,16 @@ let DSExternalInstallationInProgress = false; import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer"; +/* + * Logic to open a desktop picker put on the window global for + * lib-jitsi-meet to detect and invoke + */ +window.JitsiMeetScreenObtainer = { + openDesktopPicker(onSourceChoose) { + APP.store.dispatch(showDesktopPicker(onSourceChoose)); + } +}; + /** * Known custom conference commands. */ diff --git a/css/_chat.scss b/css/_chat.scss index b518bd856..a6114db4f 100644 --- a/css/_chat.scss +++ b/css/_chat.scss @@ -212,24 +212,24 @@ line-height: 30px; } -::-webkit-scrollbar { +:not(.default-scrollbar)::-webkit-scrollbar { background: #06a5df; width: 7px; } -::-webkit-scrollbar-button { +:not(.default-scrollbar)::-webkit-scrollbar-button { display: none; } -::-webkit-scrollbar-track { +:not(.default-scrollbar)::-webkit-scrollbar-track { background: black; } -::-webkit-scrollbar-track-piece { +:not(.default-scrollbar)::-webkit-scrollbar-track-piece { background: black; } -::-webkit-scrollbar-thumb { +:not(.default-scrollbar)::-webkit-scrollbar-thumb { background: #06a5df; border-radius: 4px; } diff --git a/css/main.scss b/css/main.scss index 6bee2ce99..dc1cafff2 100644 --- a/css/main.scss +++ b/css/main.scss @@ -37,6 +37,7 @@ @import 'overlay/overlay'; @import 'inlay'; @import 'reload_overlay/reload_overlay'; +@import 'modals/desktop-picker/desktop-picker'; @import 'modals/dialog'; @import 'modals/feedback/feedback'; @import 'modals/speaker_stats/speaker_stats'; diff --git a/css/modals/desktop-picker/_desktop-picker.scss b/css/modals/desktop-picker/_desktop-picker.scss new file mode 100644 index 000000000..7ca455880 --- /dev/null +++ b/css/modals/desktop-picker/_desktop-picker.scss @@ -0,0 +1,59 @@ +.desktop-picker-pane { + height: 320px; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + + &.source-type-screen { + .desktop-picker-source { + margin-left: auto; + margin-right: auto; + width: 50%; + } + + .desktop-source-preview-thumbnail { + width: 100%; + } + + .desktop-source-preview-label { + display: none; + } + } + + &.source-type-window { + .desktop-picker-source { + display: inline-block; + width: 30%; + } + } +} + +.desktop-picker-source { + color: $defaultDarkFontColor; + margin-top: 10px; + text-align: center; + + &.is-selected { + .desktop-source-preview-image-container { + background: rgba(0, 0, 0, 0.1); + border-radius: $borderRadius; + } + } +} + +.desktop-source-preview-label { + margin-top: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.desktop-source-preview-thumbnail { + box-shadow: 5px 5px 5px grey; + height: auto; + max-width: 100%; +} + +.desktop-source-preview-image-container { + padding: 10px; +} diff --git a/lang/main.json b/lang/main.json index abfa901c4..29ed660a5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -339,7 +339,10 @@ "remoteControlAllowedMessage": "__user__ accepted your remote control request!", "remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!", "remoteControlStopMessage": "The remote control session ended!", - "close": "Close" + "close": "Close", + "shareYourScreen": "Share your screen", + "yourEntireScreen": "Your entire screen", + "applicationWindow": "Application window" }, "email": { diff --git a/package.json b/package.json index 68ad61f79..004fb471f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@atlaskit/button": "1.0.3", "@atlaskit/button-group": "1.0.0", "@atlaskit/modal-dialog": "1.2.4", + "@atlaskit/tabs": "1.2.5", "async": "0.9.0", "autosize": "1.18.13", "bootstrap": "3.1.1", diff --git a/react/features/desktop-picker/actionTypes.js b/react/features/desktop-picker/actionTypes.js new file mode 100644 index 000000000..722dd5188 --- /dev/null +++ b/react/features/desktop-picker/actionTypes.js @@ -0,0 +1,20 @@ +import { Symbol } from '../base/react'; + +/** + * Action to remove known DesktopCapturerSources. + * + * { + * type: RESET_DESKTOP_SOURCES, + * } + */ +export const RESET_DESKTOP_SOURCES = Symbol('RESET_DESKTOP_SOURCES'); + +/** + * Action to replace stored DesktopCapturerSources with new sources. + * + * { + * type: UPDATE_DESKTOP_SOURCES, + * sources: {Array} + * } + */ +export const UPDATE_DESKTOP_SOURCES = Symbol('UPDATE_DESKTOP_SOURCES'); diff --git a/react/features/desktop-picker/actions.js b/react/features/desktop-picker/actions.js new file mode 100644 index 000000000..8a4d473d3 --- /dev/null +++ b/react/features/desktop-picker/actions.js @@ -0,0 +1,87 @@ +import { getLogger } from 'jitsi-meet-logger'; + +import { openDialog } from '../base/dialog'; +import { + RESET_DESKTOP_SOURCES, + UPDATE_DESKTOP_SOURCES +} from './actionTypes'; +import { DesktopPicker } from './components'; + +const logger = getLogger(__filename); + +/** + * Signals to remove all stored DesktopCapturerSources. + * + * @returns {{ + * type: RESET_DESKTOP_SOURCES + * }} + */ +export function resetDesktopSources() { + return { + type: RESET_DESKTOP_SOURCES + }; +} + +/** + * Begins a request to get available DesktopCapturerSources. + * + * @param {Array} types - An array with DesktopCapturerSource type strings. + * @param {Object} options - Additional configuration for getting a list + * of sources. + * @param {Object} options.thumbnailSize - The desired height and width + * of the return native image object used for the preview image of the source. + * @returns {Function} + */ +export function obtainDesktopSources(types, options = {}) { + const capturerOptions = { + types + }; + + if (options.thumbnailSize) { + capturerOptions.thumbnailSize = options.thumbnailSize; + } + + return dispatch => { + if (window.JitsiMeetElectron + && window.JitsiMeetElectron.obtainDesktopStreams) { + window.JitsiMeetElectron.obtainDesktopStreams( + sources => dispatch(updateDesktopSources(sources)), + error => logger.error( + `Error while obtaining desktop sources: ${error}`), + capturerOptions + ); + } else { + logger.error('Called JitsiMeetElectron.obtainDesktopStreams ' + + 'but it is not defined'); + } + }; +} + +/** + * Signals to open a dialog with the DesktopPicker component. + * + * @param {Function} onSourceChoose - The callback to invoke when + * a DesktopCapturerSource has been chosen. + * @returns {Object} + */ +export function showDesktopPicker(onSourceChoose) { + return openDialog(DesktopPicker, { + onSourceChoose + }); +} + +/** + * Signals new DesktopCapturerSources have been received. + * + * @param {Object} sources - Arrays with DesktopCapturerSources. + * @returns {{ + * type: UPDATE_DESKTOP_SOURCES, + * sources: Array + * }} + */ +export function updateDesktopSources(sources) { + return { + type: UPDATE_DESKTOP_SOURCES, + sources + }; +} diff --git a/react/features/desktop-picker/components/DesktopPicker.js b/react/features/desktop-picker/components/DesktopPicker.js new file mode 100644 index 000000000..7f938ddde --- /dev/null +++ b/react/features/desktop-picker/components/DesktopPicker.js @@ -0,0 +1,264 @@ +/* global config */ + +import Tabs from '@atlaskit/tabs'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { Dialog, hideDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { + resetDesktopSources, + obtainDesktopSources +} from '../actions'; +import DesktopPickerPane from './DesktopPickerPane'; + +const updateInterval = 1000; +const thumbnailSize = { + height: 300, + width: 300 +}; +const tabConfigurations = [ + { + label: 'dialog.yourEntireScreen', + type: 'screen', + isDefault: true + }, + { + label: 'dialog.applicationWindow', + type: 'window' + } +]; + +const validTypes = tabConfigurations.map(configuration => configuration.type); +const configuredTypes = config.desktopSharingChromeSources || []; + +const tabsToPopulate = tabConfigurations.filter(configuration => + configuredTypes.includes(configuration.type) + && validTypes.includes(configuration.type) +); +const typesToFetch = tabsToPopulate.map(configuration => configuration.type); + +/** + * React component for DesktopPicker. + * + * @extends Component + */ +class DesktopPicker extends Component { + /** + * DesktopPicker component's property types. + * + * @static + */ + static propTypes = { + /** + * Used to request DesktopCapturerSources. + */ + dispatch: React.PropTypes.func, + + /** + * The callback to be invoked when the component is closed or + * when a DesktopCapturerSource has been chosen. + */ + onSourceChoose: React.PropTypes.func, + + /** + * An object with arrays of DesktopCapturerSources. The key + * should be the source type. + */ + sources: React.PropTypes.object, + + /** + * Used to obtain translations. + */ + t: React.PropTypes.func + } + + /** + * Initializes a new DesktopPicker instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this.state = { + selectedSourceId: '' + }; + + this._poller = null; + this._onCloseModal = this._onCloseModal.bind(this); + this._onPreviewClick = this._onPreviewClick.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._updateSources = this._updateSources.bind(this); + } + + /** + * Perform an immediate update request for DesktopCapturerSources and + * begin requesting updates at an interval. + * + * @inheritdoc + */ + componentWillMount() { + this._updateSources(); + this._startPolling(); + } + + /** + * Clean up component and DesktopCapturerSource store state. + * + * @inheritdoc + */ + componentWillUnmount() { + this._stopPolling(); + this.props.dispatch(resetDesktopSources()); + } + + /** + * Notifies this mounted React Component that it will receive new props. + * Sets a default selected source if one is not already set. + * + * @inheritdoc + * @param {Object} nextProps - The read-only React Component props that this + * instance will receive. + * @returns {void} + */ + componentWillReceiveProps(nextProps) { + if (!this.state.selectedSourceId + && nextProps.sources.screen.length) { + this.setState({ selectedSourceId: nextProps.sources.screen[0].id }); + } + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + return ( + + { this._renderTabs() } + ); + } + + /** + * Dispatches an action to get currently available DesktopCapturerSources. + * + * @private + * @returns {void} + */ + _updateSources() { + this.props.dispatch(obtainDesktopSources( + typesToFetch, + { + thumbnailSize + } + )); + } + + /** + * Create an interval to update knwon available DesktopCapturerSources. + * + * @private + * @returns {void} + */ + _startPolling() { + this._stopPolling(); + this._poller = window.setInterval(this._updateSources, + updateInterval); + } + + /** + * Cancels the interval to update DesktopCapturerSources. + * + * @private + * @returns {void} + */ + _stopPolling() { + window.clearInterval(this._poller); + this._poller = null; + } + + /** + * Sets the currently selected DesktopCapturerSource. + * + * @param {string} id - The id of DesktopCapturerSource. + * @returns {void} + */ + _onPreviewClick(id) { + this.setState({ selectedSourceId: id }); + } + + /** + * Request to close the modal and execute callbacks + * with the selected source id. + * + * @returns {void} + */ + _onSubmit() { + this._onCloseModal(this.state.selectedSourceId); + } + + /** + * Dispatches an action to hide the DesktopPicker and invokes + * the passed in callback with a selectedSourceId, if any. + * + * @param {string} id - The id of the DesktopCapturerSource to pass into + * the onSourceChoose callback. + * @returns {void} + */ + _onCloseModal(id = '') { + this.props.onSourceChoose(id); + this.props.dispatch(hideDialog()); + } + + /** + * Configures and renders the tabs for display. + * + * @returns {ReactElement} + * @private + */ + _renderTabs() { + const tabs = tabsToPopulate.map(tabConfig => { + const type = tabConfig.type; + + return { + label: this.props.t(tabConfig.label), + defaultSelected: tabConfig.isDefault, + content: + }; + }); + + return ; + } +} + +/** + * Maps (parts of) the Redux state to the associated DesktopPicker's props. + * + * @param {Object} state - Redux state. + * @protected + * @returns {{ + * sources: Object + * }} + */ +function mapStateToProps(state) { + return { + sources: state['features/desktop-picker/sources'] + }; +} + +export default translate(connect(mapStateToProps)(DesktopPicker)); diff --git a/react/features/desktop-picker/components/DesktopPickerPane.js b/react/features/desktop-picker/components/DesktopPickerPane.js new file mode 100644 index 000000000..592cec5ec --- /dev/null +++ b/react/features/desktop-picker/components/DesktopPickerPane.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import DesktopSourcePreview from './DesktopSourcePreview'; + +/** + * React component for showing a grid of DesktopSourcePreviews. + * + * @extends Component + */ +class DesktopPickerPane extends Component { + /** + * DesktopPickerPane component's property types. + * + * @static + */ + static propTypes = { + /** + * The handler to be invoked when a DesktopSourcePreview is clicked. + */ + onClick: React.PropTypes.func, + + /** + * The handler to be invoked when a DesktopSourcePreview is + * double clicked. + */ + onDoubleClick: React.PropTypes.func, + + /** + * The id of the DesktopCapturerSource that is currently selected. + */ + selectedSourceId: React.PropTypes.string, + + /** + * An array of DesktopCapturerSources. + */ + sources: React.PropTypes.array, + + /** + * The source type of the DesktopCapturerSources to display. + */ + type: React.PropTypes.string + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const previews = this.props.sources.map(source => + + ); + const classnames = 'desktop-picker-pane default-scrollbar ' + + `source-type-${this.props.type}`; + + return ( +
+ { previews } +
+ ); + } +} + +export default DesktopPickerPane; diff --git a/react/features/desktop-picker/components/DesktopSourcePreview.js b/react/features/desktop-picker/components/DesktopSourcePreview.js new file mode 100644 index 000000000..612ec00d5 --- /dev/null +++ b/react/features/desktop-picker/components/DesktopSourcePreview.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; + +/** + * React component for displaying a preview of a DesktopCapturerSource. + * + * @extends Component + */ +class DesktopSourcePreview extends Component { + /** + * DesktopSourcePreview component's property types. + * + * @static + */ + static propTypes = { + /** + * If true the 'is-selected' class will be added to the component. + */ + isSelected: React.PropTypes.bool, + + /** + * The callback to invoke when the component is clicked. + * The id of the DesktopCapturerSource will be passed in. + */ + onClick: React.PropTypes.func, + + /** + * The callback to invoke when the component is double clicked. + * The id of the DesktopCapturerSource will be passed in. + */ + onDoubleClick: React.PropTypes.func, + + /** + * The DesktopCapturerSource to display. + */ + source: React.PropTypes.object + } + + /** + * Initializes a new DesktopSourcePreview instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this._onClick = this._onClick.bind(this); + this._onDoubleClick = this._onDoubleClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const isSelectedClass = this.props.isSelected ? 'is-selected' : ''; + const displayClasses = `desktop-picker-source ${isSelectedClass}`; + + return ( +
+
+ +
+
+ { this.props.source.name } +
+
+ ); + } + + /** + * Invokes the passed in onClick callback. + * + * @returns {void} + */ + _onClick() { + this.props.onClick(this.props.source.id); + } + + /** + * Invokes the passed in onDoubleClick callback. + * + * @returns {void} + */ + _onDoubleClick() { + this.props.onDoubleClick(this.props.source.id); + } +} + +export default DesktopSourcePreview; diff --git a/react/features/desktop-picker/components/index.js b/react/features/desktop-picker/components/index.js new file mode 100644 index 000000000..12a0635bf --- /dev/null +++ b/react/features/desktop-picker/components/index.js @@ -0,0 +1 @@ +export { default as DesktopPicker } from './DesktopPicker'; diff --git a/react/features/desktop-picker/index.js b/react/features/desktop-picker/index.js new file mode 100644 index 000000000..51710a731 --- /dev/null +++ b/react/features/desktop-picker/index.js @@ -0,0 +1,5 @@ +export * from './actionTypes'; +export * from './actions'; +export * from './components'; + +import './reducer'; diff --git a/react/features/desktop-picker/reducer.js b/react/features/desktop-picker/reducer.js new file mode 100644 index 000000000..8ed4770db --- /dev/null +++ b/react/features/desktop-picker/reducer.js @@ -0,0 +1,59 @@ +import { ReducerRegistry } from '../base/redux'; +import { + RESET_DESKTOP_SOURCES, + UPDATE_DESKTOP_SOURCES +} from './actionTypes'; + +const defaultState = { + screen: [], + window: [] +}; + +/** + * Listen for actions that mutate the known available DesktopCapturerSources. + * + * @param {Object[]} state - Current state. + * @param {Object} action - Action object. + * @param {string} action.type - Type of action. + * @param {Array} action.sources - DesktopCapturerSources. + * @returns {Object} + */ +ReducerRegistry.register( + 'features/desktop-picker/sources', + (state = defaultState, action) => { + switch (action.type) { + case RESET_DESKTOP_SOURCES: + return { ...defaultState }; + case UPDATE_DESKTOP_SOURCES: + return seperateSourcesByType(action.sources); + default: + return state; + } + }); + +/** + * Converts an array of DesktopCapturerSources to an object with types + * for keys and values being an array with sources of the key's type. + * + * @param {Array} sources - DesktopCapturerSources. + * @returns {Object} An object with the sources split into seperate arrays + * based on source type. + * @private + */ +function seperateSourcesByType(sources = []) { + const sourcesByType = { + screen: [], + window: [] + }; + + sources.forEach(source => { + const sourceIdParts = source.id.split(':'); + const sourceType = sourceIdParts[0]; + + if (sourcesByType[sourceType]) { + sourcesByType[sourceType].push(source); + } + }); + + return sourcesByType; +}