diff --git a/CHANGELOG.md b/CHANGELOG.md index 272d0f08319..9c28ccda9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,15 @@ Release date: TBD ### Improvements +#### Windows + - Added the feature to open the application via `mattermost://` link. + [#616](https://github.com/mattermost/desktop/pull/616) + #### Mac - Added `Ctrl+Tab` and `Ctrl+Shift+Tab` shortcuts to switch tabs [#512](https://github.com/mattermost/desktop/issues/512) + - Added the feature to open the application via `mattermost://` link. + [#616](https://github.com/mattermost/desktop/pull/616) ### Bug Fixes diff --git a/electron-builder.json b/electron-builder.json index 6ab6896c962..899d1a4ee85 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -20,6 +20,12 @@ ] } ], + "protocols": [ + { + "name": "Mattermost", + "schemes": ["mattermost"] + } + ], "deb": { "synopsis": "Mattermost" }, diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index 965066a5092..4786919ed93 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -14,6 +14,8 @@ const HoveringURL = require('./HoveringURL.jsx'); const NewTeamModal = require('./NewTeamModal.jsx'); +const Utils = require('../../utils/util.js'); + const MainPage = createReactClass({ propTypes: { onUnreadCountChange: PropTypes.func.isRequired, @@ -21,12 +23,22 @@ const MainPage = createReactClass({ onTeamConfigChange: PropTypes.func.isRequired, initialIndex: PropTypes.number.isRequired, useSpellChecker: PropTypes.bool.isRequired, - onSelectSpellCheckerLocale: PropTypes.func.isRequired + onSelectSpellCheckerLocale: PropTypes.func.isRequired, + deeplinkingUrl: PropTypes.string }, getInitialState() { + let key = this.props.initialIndex; + if (this.props.deeplinkingUrl !== null) { + for (var i = 0; i < this.props.teams.length; i++) { + if (this.props.deeplinkingUrl.includes(this.props.teams[i].url)) { + key = i; + break; + } + } + } return { - key: this.props.initialIndex, + key, unreadCounts: new Array(this.props.teams.length), mentionCounts: new Array(this.props.teams.length), unreadAtActive: new Array(this.props.teams.length), @@ -108,6 +120,19 @@ const MainPage = createReactClass({ ipcRenderer.on('focus-on-webview', () => { this.focusOnWebView(); }); + + ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => { + const lastUrlDomain = Utils.getDomain(deepLinkUrl); + for (var i = 0; i < this.props.teams.length; i++) { + if (lastUrlDomain === Utils.getDomain(self.refs[`mattermostView${i}`].getSrc())) { + if (this.state.key !== i) { + this.handleSelect(i); + } + self.refs[`mattermostView${i}`].handleDeepLink(deepLinkUrl.replace(lastUrlDomain, '')); + break; + } + } + }); }, componentDidUpdate(prevProps, prevState) { if (prevState.key !== this.state.key) { // i.e. When tab has been changed @@ -247,6 +272,13 @@ const MainPage = createReactClass({ } var id = 'mattermostView' + index; var isActive = self.state.key === index; + + let teamUrl = team.url; + const deeplinkingUrl = this.props.deeplinkingUrl; + if (deeplinkingUrl !== null && deeplinkingUrl.includes(teamUrl)) { + teamUrl = deeplinkingUrl; + } + return ( 1} useSpellChecker={this.props.useSpellChecker} onSelectSpellCheckerLocale={this.props.onSelectSpellCheckerLocale} - src={team.url} + src={teamUrl} name={team.name} onTargetURLChange={self.handleTargetURLChange} onUnreadCountChange={handleUnreadCountChange} diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 22d55b145cb..3bb97a478a1 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -205,6 +205,21 @@ const MattermostView = createReactClass({ webview.getWebContents().goForward(); }, + getSrc() { + const webview = findDOMNode(this.refs.webview); + return webview.src; + }, + + handleDeepLink(relativeUrl) { + const webview = findDOMNode(this.refs.webview); + webview.executeJavaScript( + 'history.pushState(null, null, "' + relativeUrl + '");' + ); + webview.executeJavaScript( + 'dispatchEvent(new PopStateEvent("popstate", null));' + ); + }, + render() { const errorView = this.state.errorInfo ? ( { errorView } diff --git a/src/browser/index.jsx b/src/browser/index.jsx index b31b1dadb8e..7191af4482e 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -102,6 +102,11 @@ function handleSelectSpellCheckerLocale(locale) { const parsedURL = url.parse(window.location.href, true); const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0; +let deeplinkingUrl = null; +if (!parsedURL.query.index || parsedURL.query.index === null) { + deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl; +} + ReactDOM.render( , document.getElementById('content') ); diff --git a/src/main.js b/src/main.js index 65d3d31e6f2..2c6e25b2477 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,8 @@ const isDev = require('electron-is-dev'); const installExtension = require('electron-devtools-installer'); const squirrelStartup = require('./main/squirrelStartup'); +const protocols = require('../electron-builder.json').protocols; + process.on('uncaughtException', (error) => { console.error(error); }); @@ -44,6 +46,7 @@ const assetsDir = path.resolve(app.getAppPath(), 'assets'); // be closed automatically when the JavaScript object is garbage collected. var mainWindow = null; let spellChecker = null; +let scheme = null; var argv = require('yargs').parse(process.argv.slice(1)); @@ -148,7 +151,19 @@ const trayImages = (() => { })(); // If there is already an instance, activate the window in the existing instace and quit this one -if (app.makeSingleInstance((/*commandLine, workingDirectory*/) => { +if (app.makeSingleInstance((commandLine/*, workingDirectory*/) => { + // Protocol handler for win32 + // argv: An array of the second instance’s (command line / deep linked) arguments + if (process.platform === 'win32') { + // Keep only command line / deep linked arguments + if (Array.isArray(commandLine.slice(1)) && commandLine.slice(1).length > 0) { + const deeplinkingUrl = getDeeplinkingUrl(commandLine.slice(1)[0]); + if (deeplinkingUrl) { + mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl); + } + } + } + // Someone tried to run a second instance, we should focus our window. if (mainWindow) { if (mainWindow.isMinimized()) { @@ -319,6 +334,32 @@ ipcMain.on('download-url', (event, URL) => { }); }); +if (isDev) { + console.log('In development mode, deeplinking is disabled'); +} else if (protocols && protocols[0] && + protocols[0].schemes && protocols[0].schemes[0] +) { + scheme = protocols[0].schemes[0]; + app.setAsDefaultProtocolClient(scheme); +} + +function getDeeplinkingUrl(url) { + if (scheme) { + return url.replace(new RegExp('^' + scheme), 'https'); + } + return null; +} + +// Protocol handler for osx +app.on('open-url', (event, url) => { + event.preventDefault(); + const deeplinkingUrl = getDeeplinkingUrl(url); + if (deeplinkingUrl) { + mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl); + } + mainWindow.show(); +}); + // This method will be called when Electron has finished // initialization and is ready to create browser windows. app.on('ready', () => { @@ -331,10 +372,26 @@ app.on('ready', () => { catch((err) => console.log('An error occurred: ', err)); } + let deeplinkingUrl = null; + + // Protocol handler for win32 + if (process.platform === 'win32') { + // Keep only command line / deep linked argument. Make sure it's not squirrel command + const tmpArgs = process.argv.slice(1); + if ( + Array.isArray(tmpArgs) && tmpArgs.length > 0 && + tmpArgs[0].match(/^--squirrel-/) === null + ) { + deeplinkingUrl = getDeeplinkingUrl(tmpArgs[0]); + } + } + mainWindow = createMainWindow(config, { hideOnStartup, - linuxAppIcon: path.join(assetsDir, 'appicon.png') + linuxAppIcon: path.join(assetsDir, 'appicon.png'), + deeplinkingUrl }); + mainWindow.on('closed', () => { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time diff --git a/src/main/mainWindow.js b/src/main/mainWindow.js index 89bb623476f..ae62f3a66f8 100644 --- a/src/main/mainWindow.js +++ b/src/main/mainWindow.js @@ -42,6 +42,7 @@ function createMainWindow(config, options) { }); const mainWindow = new BrowserWindow(windowOptions); + mainWindow.deeplinkingUrl = options.deeplinkingUrl; const indexURL = global.isDev ? 'http://localhost:8080/browser/index.html' : `file://${app.getAppPath()}/browser/index.html`; mainWindow.loadURL(indexURL); diff --git a/src/utils/util.js b/src/utils/util.js new file mode 100644 index 00000000000..6475bcfe962 --- /dev/null +++ b/src/utils/util.js @@ -0,0 +1,10 @@ +const {URL} = require('url'); + +export function getDomain(url) { + try { + const objectUrl = new URL(url); + return objectUrl.origin; + } catch (e) { + return null; + } +}