Skip to content

Commit

Permalink
Add protocol to open conference links with the app
Browse files Browse the repository at this point in the history
Co-authored-by: Christophe HAMERLING <chamerling@linagora.com>
Co-authored-by: Klemens Arro <klemens.arro@admcloudtech.com>
Co-authored-by: Goran Urukalo <goran.urukalo@teletrader.com>
  • Loading branch information
3 people authored Jun 10, 2020
1 parent b662c93 commit d12611d
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Desktop application for [Jitsi Meet] built with [Electron].
- Builtin auto-updates
- Remote control
- Always-On-Top window
- Support for deeplinks such as `jitsi-meet://myroom` (will open `myroom` on the configured Jitsi instance) or `jitsi-meet://jitsi.mycompany.com/myroom` (will open `myroom` on the Jitsi instance running on `jitsi.mycompany.com`)

## Installation

Expand Down
68 changes: 64 additions & 4 deletions app/features/app/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,84 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';

import React, { Component } from 'react';
import { Route, Switch } from 'react-router';
import { ConnectedRouter as Router } from 'react-router-redux';
import { connect } from 'react-redux';
import { ConnectedRouter as Router, push } from 'react-router-redux';

import { Conference } from '../../conference';
import config from '../../config';
import { history } from '../../router';
import { createConferenceObjectFromURL } from '../../utils';
import { Welcome } from '../../welcome';

/**
* Main component encapsulating the entire application.
*/
export default class App extends Component<*> {
class App extends Component<*> {
/**
* Initializes a new {@code App} instance.
*
* @inheritdoc
*/
constructor() {
super();
constructor(props) {
super(props);

document.title = config.appName;

this._listenOnProtocolMessages
= this._listenOnProtocolMessages.bind(this);
}

/**
* Implements React's {@link Component#componentDidMount()}.
*
* @returns {void}
*/
componentDidMount() {
// start listening on this events
window.jitsiNodeAPI.ipc.on('protocol-data-msg', this._listenOnProtocolMessages);

// send notification to main process
window.jitsiNodeAPI.ipc.send('renderer-ready');
}

/**
* Implements React's {@link Component#componentWillUnmount()}.
*
* @returns {void}
*/
componentWillUnmount() {
// remove listening for this events
window.jitsiNodeAPI.ipc.removeListener(
'protocol-data-msg',
this._listenOnProtocolMessages
);
}

_listenOnProtocolMessages: (*) => void;

/**
* Handler when main proccess contact us.
*
* @param {Object} event - Message event.
* @param {string} inputURL - String with room name.
*
* @returns {void}
*/
_listenOnProtocolMessages(event, inputURL: string) {
// Remove trailing slash if one exists.
if (inputURL.substr(-1) === '/') {
inputURL = inputURL.substr(0, inputURL.length - 1); // eslint-disable-line no-param-reassign
}

const conference = createConferenceObjectFromURL(inputURL);

// Don't navigate if conference couldn't be created
if (!conference) {
return;
}

// change route when we are notified
this.props.dispatch(push('/conference', conference));
}

/**
Expand All @@ -50,3 +108,5 @@ export default class App extends Component<*> {
);
}
}

export default connect()(App);
6 changes: 6 additions & 0 deletions app/features/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export default {
*/
appName: 'Jitsi Meet',

/**
* The prefix for application protocol.
* You will also need to replace this in package.json.
*/
appProtocolPrefix: 'jitsi-meet',

/**
* The default server URL of Jitsi Meet Deployment that will be used.
*/
Expand Down
37 changes: 37 additions & 0 deletions app/features/utils/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,40 @@ export function normalizeServerURL(url: string) {
export function openExternalLink(link: string) {
window.jitsiNodeAPI.openExternalLink(link);
}


/**
* Get URL, extract room name from it and create a Conference object.
*
* @param {string} inputURL - Combined server url with room separated by /.
* @returns {Object}
*/
export function createConferenceObjectFromURL(inputURL: string) {
const lastIndexOfSlash = inputURL.lastIndexOf('/');
let room;
let serverURL;

if (lastIndexOfSlash === -1) {
// This must be only the room name.
room = inputURL;
} else {
// Take the substring after last slash to be the room name.
room = inputURL.substring(lastIndexOfSlash + 1);

// Take the substring before last slash to be the Server URL.
serverURL = inputURL.substring(0, lastIndexOfSlash);

// Normalize the server URL.
serverURL = normalizeServerURL(serverURL);
}

// Don't navigate if no room was specified.
if (!room) {
return;
}

return {
room,
serverURL
};
}
29 changes: 5 additions & 24 deletions app/features/welcome/components/Welcome.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { push } from 'react-router-redux';
import { Navbar } from '../../navbar';
import { Onboarding, startOnboarding } from '../../onboarding';
import { RecentList } from '../../recent-list';
import { normalizeServerURL } from '../../utils';
import { createConferenceObjectFromURL } from '../../utils';

import { Body, FieldWrapper, Form, Header, Label, Wrapper } from '../styled';

Expand Down Expand Up @@ -206,33 +206,14 @@ class Welcome extends Component<Props, State> {
*/
_onJoin() {
const inputURL = this.state.url || this.state.generatedRoomname;
const lastIndexOfSlash = inputURL.lastIndexOf('/');
let room;
let serverURL;

if (lastIndexOfSlash === -1) {
// This must be only the room name.
room = inputURL;
} else {
// Take the substring after last slash to be the room name.
room = inputURL.substring(lastIndexOfSlash + 1);

// Take the substring before last slash to be the Server URL.
serverURL = inputURL.substring(0, lastIndexOfSlash);

// Normalize the server URL.
serverURL = normalizeServerURL(serverURL);
}
const conference = createConferenceObjectFromURL(inputURL);

// Don't navigate if no room was specified.
if (!room) {
// Don't navigate if conference couldn't be created
if (!conference) {
return;
}

this.props.dispatch(push('/conference', {
room,
serverURL
}));
this.props.dispatch(push('/conference', conference));
}

_onURLChange: (*) => void;
Expand Down
30 changes: 27 additions & 3 deletions app/preload/preload.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
const createElectronStorage = require('redux-persist-electron-storage');
const { shell } = require('electron');
const { ipcRenderer, shell } = require('electron');
const os = require('os');
const url = require('url');

const jitsiMeetElectronUtils = require('jitsi-meet-electron-utils');


const protocolRegex = /^https?:/i;

/**
Expand All @@ -28,10 +27,35 @@ function openExternalLink(link) {
}
}

const whitelistedIpcChannels = [ 'protocol-data-msg', 'renderer-ready' ];

window.jitsiNodeAPI = {
createElectronStorage,
osUserInfo: os.userInfo,
openExternalLink,
jitsiMeetElectronUtils
jitsiMeetElectronUtils,
shellOpenExternal: shell.openExternal,
ipc: {
on: (channel, listener) => {
if (!whitelistedIpcChannels.includes(channel)) {
return;
}

return ipcRenderer.on(channel, listener);
},
send: channel => {
if (!whitelistedIpcChannels.includes(channel)) {
return;
}

return ipcRenderer.send(channel);
},
removeListener: (channel, listener) => {
if (!whitelistedIpcChannels.includes(channel)) {
return;
}

return ipcRenderer.removeListener(channel, listener);
}
}
};
94 changes: 93 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
BrowserWindow,
Menu,
app,
ipcMain,
shell
} = require('electron');
const contextMenu = require('electron-context-menu');
Expand Down Expand Up @@ -71,6 +72,14 @@ if (isDev) {
*/
let mainWindow = null;

/**
* Add protocol data
*/
const appProtocolSurplus = `${config.default.appProtocolPrefix}://`;
let rendererReady = false;
let protocolDataForFrontApp = null;


/**
* Sets the application menu. It is hidden on all platforms except macOS because
* otherwise copy and paste functionality is not available.
Expand Down Expand Up @@ -211,6 +220,44 @@ function createJitsiMeetWindow() {
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});

/**
* This is for windows [win32]
* so when someone tries to enter something like jitsi-meet://test
* while app is closed
* it will trigger this event below
*/
if (process.platform === 'win32') {
handleProtocolCall(process.argv.pop());
}
}

/**
* Handler for application protocol links to initiate a conference.
*/
function handleProtocolCall(fullProtocolCall) {
// don't touch when something is bad
if (
!fullProtocolCall
|| fullProtocolCall.trim() === ''
|| fullProtocolCall.indexOf(appProtocolSurplus) !== 0
) {
return;
}

const inputURL = fullProtocolCall.replace(appProtocolSurplus, '');

if (app.isReady() && mainWindow === null) {
createJitsiMeetWindow();
}

protocolDataForFrontApp = inputURL;

if (rendererReady) {
mainWindow
.webContents
.send('protocol-data-msg', inputURL);
}
}

/**
Expand Down Expand Up @@ -247,14 +294,21 @@ app.on('certificate-error',

app.on('ready', createJitsiMeetWindow);

app.on('second-instance', () => {
app.on('second-instance', (event, commandLine) => {
/**
* If someone creates second instance of the application, set focus on
* existing window.
*/
if (mainWindow) {
mainWindow.isMinimized() && mainWindow.restore();
mainWindow.focus();

/**
* This is for windows [win32]
* so when someone tries to enter something like jitsi-meet://test
* while app is opened it will trigger protocol handler.
*/
handleProtocolCall(commandLine.pop());
}
});

Expand All @@ -264,3 +318,41 @@ app.on('window-all-closed', () => {
app.quit();
}
});

// remove so we can register each time as we run the app.
app.removeAsDefaultProtocolClient(config.default.appProtocolPrefix);

// If we are running a non-packaged version of the app && on windows
if (isDev && process.platform === 'win32') {
// Set the path of electron.exe and your app.
// These two additional parameters are only available on windows.
app.setAsDefaultProtocolClient(
config.default.appProtocolPrefix,
process.execPath,
[ path.resolve(process.argv[1]) ]
);
} else {
app.setAsDefaultProtocolClient(config.default.appProtocolPrefix);
}

/**
* This is for mac [darwin]
* so when someone tries to enter something like jitsi-meet://test
* it will trigger this event below
*/
app.on('open-url', (event, data) => {
event.preventDefault();
handleProtocolCall(data);
});

/**
* This is to notify main.js [this] that front app is ready to receive messages.
*/
ipcMain.on('renderer-ready', () => {
rendererReady = true;
if (protocolDataForFrontApp) {
mainWindow
.webContents
.send('protocol-data-msg', protocolDataForFrontApp);
}
});
Loading

0 comments on commit d12611d

Please sign in to comment.