Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol deep linking #616

Merged
merged 9 commits into from
Oct 24, 2017
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
]
}
],
"protocols": [
{
"name": "Mattermost",
"schemes": ["mattermost"]
}
],
"deb": {
"synopsis": "Mattermost"
},
Expand Down
38 changes: 35 additions & 3 deletions src/browser/components/MainPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,31 @@ const HoveringURL = require('./HoveringURL.jsx');

const NewTeamModal = require('./NewTeamModal.jsx');

const Utils = require('../../utils/util.js');

const MainPage = createReactClass({
propTypes: {
onUnreadCountChange: PropTypes.func.isRequired,
teams: PropTypes.array.isRequired,
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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -247,14 +272,21 @@ 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 (
<MattermostView
key={id}
id={id}
withTab={this.props.teams.length > 1}
useSpellChecker={this.props.useSpellChecker}
onSelectSpellCheckerLocale={this.props.onSelectSpellCheckerLocale}
src={team.url}
src={teamUrl}
name={team.name}
onTargetURLChange={self.handleTargetURLChange}
onUnreadCountChange={handleUnreadCountChange}
Expand Down
16 changes: 16 additions & 0 deletions src/browser/components/MattermostView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -223,6 +238,7 @@ const MattermostView = createReactClass({
if (!this.props.active) {
classNames.push('mattermostView-hidden');
}

return (
<div>
{ errorView }
Expand Down
6 changes: 6 additions & 0 deletions src/browser/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MainPage
teams={AppConfig.data.teams}
Expand All @@ -110,6 +115,7 @@ ReactDOM.render(
onTeamConfigChange={teamConfigChange}
useSpellChecker={AppConfig.data.useSpellChecker}
onSelectSpellCheckerLocale={handleSelectSpellCheckerLocale}
deeplinkingUrl={deeplinkingUrl}
/>,
document.getElementById('content')
);
Expand Down
61 changes: 59 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app needs to do not anything other than installation or related works when --squirrel-* command line flags exist. They are --squirrel-install, --squirrel-uninstall, --squirrel-updated and --squirrel-obsolete. https://github.com/electron/windows-installer#handling-squirrel-events

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()) {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/mainWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/utils/util.js
Original file line number Diff line number Diff line change
@@ -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;
}
}