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 (
+ );
+ }
+
+ /**
+ * 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: