Add protocol to open conference links with the app

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>
This commit is contained in:
Christophe Hamerling 2020-06-10 14:21:13 +02:00 committed by GitHub
parent b662c93ac7
commit d12611d79c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 243 additions and 33 deletions

View File

@ -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

View File

@ -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));
}
/**
@ -50,3 +108,5 @@ export default class App extends Component<*> {
);
}
}
export default connect()(App);

View File

@ -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.
*/

View File

@ -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
};
}

View File

@ -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';
@ -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;
const conference = createConferenceObjectFromURL(inputURL);
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) {
// 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;

View File

@ -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;
/**
@ -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
main.js
View File

@ -4,6 +4,7 @@ const {
BrowserWindow,
Menu,
app,
ipcMain,
shell
} = require('electron');
const contextMenu = require('electron-context-menu');
@ -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.
@ -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);
}
}
/**
@ -247,7 +294,7 @@ 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.
@ -255,6 +302,13 @@ app.on('second-instance', () => {
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());
}
});
@ -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);
}
});

View File

@ -63,7 +63,16 @@
},
"directories": {
"buildResources": "resources"
}
},
"protocols": [
{
"name": "jitsi-protocol",
"role": "Viewer",
"schemes": [
"jitsi-meet"
]
}
]
},
"pre-commit": [
"lint"