Merge pull request #12780 from vector-im/travis/wrapped-jitsi

Use a local widget wrapper for Jitsi calls
This commit is contained in:
Travis Ralston 2020-03-19 11:52:22 -06:00 committed by GitHub
commit 775e1fc4ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 308 additions and 36 deletions

View file

@ -22,7 +22,6 @@
"https://scalar-staging.vector.im/api", "https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api" "https://scalar-staging.riot.im/scalar/api"
], ],
"integrations_jitsi_widget_url": "https://scalar.vector.im/api/widgets/jitsi.html",
"bug_report_endpoint_url": "https://riot.im/bugreports/submit", "bug_report_endpoint_url": "https://riot.im/bugreports/submit",
"defaultCountryCode": "GB", "defaultCountryCode": "GB",
"showLabsSettings": false, "showLabsSettings": false,
@ -52,5 +51,9 @@
}, },
"settingDefaults": { "settingDefaults": {
"breadcrumbs": true "breadcrumbs": true
},
"jitsi": {
"preferredDomain": "jitsi.riot.im",
"externalApiUrl": "https://jitsi.riot.im/libs/external_api.min.js"
} }
} }

View file

@ -84,6 +84,13 @@ For a good example, see https://riot.im/develop/config.json.
By default, this is "https://matrix.to" to generate matrix.to (spec) permalinks. By default, this is "https://matrix.to" to generate matrix.to (spec) permalinks.
Set this to your Riot instance URL if you run an unfederated server (eg: Set this to your Riot instance URL if you run an unfederated server (eg:
"https://riot.example.org"). "https://riot.example.org").
1. `jitsi`: Used to change the default conference options.
1. `preferredDomain`: The domain name of the preferred Jitsi instance. Defaults
to `jitsi.riot.im`. This is used whenever a user clicks on the voice/video
call buttons - integration managers may use a different domain.
1. `externalApiUrl`: The URL to the Jitsi Meet API script. This is required
for showing any Jitsi widgets, no matter the source. Defaults to
`https://jitsi.riot.im/libs/external_api.min.js`.
Note that `index.html` also has an og:image meta tag that is set to an image Note that `index.html` also has an og:image meta tag that is set to an image
hosted on riot.im. This is the image used if links to your copy of Riot hosted on riot.im. This is the image used if links to your copy of Riot

View file

@ -39,9 +39,6 @@ import url from 'url';
import {parseQs, parseQsFromFragment} from './url_utils'; import {parseQs, parseQsFromFragment} from './url_utils';
import ElectronPlatform from './platform/ElectronPlatform';
import WebPlatform from './platform/WebPlatform';
import {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg'; import {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg';
import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
@ -50,6 +47,7 @@ import {setTheme} from "matrix-react-sdk/src/theme";
import Olm from 'olm'; import Olm from 'olm';
import CallHandler from 'matrix-react-sdk/src/CallHandler'; import CallHandler from 'matrix-react-sdk/src/CallHandler';
import {loadConfig, preparePlatform} from "./initial-load";
let lastLocationHashSet = null; let lastLocationHashSet = null;
@ -191,35 +189,11 @@ export async function loadApp() {
await loadOlm(); await loadOlm();
// set the platform for react sdk // set the platform for react sdk
if (window.ipcRenderer) { preparePlatform();
console.log("Using Electron platform");
const plaf = new ElectronPlatform();
PlatformPeg.set(plaf);
} else {
console.log("Using Web platform");
PlatformPeg.set(new WebPlatform());
}
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
let configJson; // Load the config from the platform
let configError; const configInfo = await loadConfig();
let configSyntaxError = false;
try {
configJson = await platform.getConfig();
} catch (e) {
configError = e;
if (e && e.err && e.err instanceof SyntaxError) {
console.error("SyntaxError loading config:", e);
configSyntaxError = true;
configJson = {}; // to prevent errors between here and loading CSS for the error box
}
}
// XXX: We call this twice, once here and once in MatrixChat as a prop. We call it here to ensure
// granular settings are loaded correctly and to avoid duplicating the override logic for the theme.
SdkConfig.put(configJson);
// Load language after loading config.json so that settingsDefaults.language can be applied // Load language after loading config.json so that settingsDefaults.language can be applied
await loadLanguage(); await loadLanguage();
@ -248,7 +222,7 @@ export async function loadApp() {
await setTheme(); await setTheme();
// Now that we've loaded the theme (CSS), display the config syntax error if needed. // Now that we've loaded the theme (CSS), display the config syntax error if needed.
if (configSyntaxError) { if (configInfo.configSyntaxError) {
const errorMessage = ( const errorMessage = (
<div> <div>
<p> <p>
@ -260,7 +234,7 @@ export async function loadApp() {
<p> <p>
{_t( {_t(
"The message from the parser is: %(message)s", "The message from the parser is: %(message)s",
{message: configError.err.message || _t("Invalid JSON")}, {message: configInfo.configError.err.message || _t("Invalid JSON")},
)} )}
</p> </p>
</div> </div>
@ -280,7 +254,7 @@ export async function loadApp() {
const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname; const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname;
console.log("Vector starting at " + urlWithoutQuery); console.log("Vector starting at " + urlWithoutQuery);
if (configError) { if (configInfo.configError) {
window.matrixChat = ReactDOM.render(<div className="error"> window.matrixChat = ReactDOM.render(<div className="error">
Unable to load config file: please refresh the page to try again. Unable to load config file: please refresh the page to try again.
</div>, document.getElementById('matrixchat')); </div>, document.getElementById('matrixchat'));
@ -298,7 +272,7 @@ export async function loadApp() {
config={newConfig} config={newConfig}
realQueryParams={params} realQueryParams={params}
startingFragmentQueryParams={fragparts.params} startingFragmentQueryParams={fragparts.params}
enableGuest={!configJson.disable_guests} enableGuest={!SdkConfig.get().disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted} onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)} initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()} defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}

View file

@ -0,0 +1,58 @@
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import ElectronPlatform from './platform/ElectronPlatform';
import WebPlatform from './platform/WebPlatform';
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg';
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
export function preparePlatform() {
if ((<any>window).ipcRenderer) {
console.log("Using Electron platform");
const plaf = new ElectronPlatform();
PlatformPeg.set(plaf);
} else {
console.log("Using Web platform");
PlatformPeg.set(new WebPlatform());
}
}
export async function loadConfig(): Promise<{configError?: Error, configSyntaxError: boolean}> {
const platform = PlatformPeg.get();
let configJson;
let configError;
let configSyntaxError = false;
try {
configJson = await platform.getConfig();
} catch (e) {
configError = e;
if (e && e.err && e.err instanceof SyntaxError) {
console.error("SyntaxError loading config:", e);
configSyntaxError = true;
configJson = {}; // to prevent errors between here and loading CSS for the error box
}
}
// XXX: We call this twice, once here and once in MatrixChat as a prop. We call it here to ensure
// granular settings are loaded correctly and to avoid duplicating the override logic for the theme.
//
// Note: this isn't called twice for some wrappers, like the Jitsi wrapper.
SdkConfig.put(configJson);
return {configError, configSyntaxError};
}

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Jitsi Widget</title>
</head>
<body>
<div id="jitsiContainer"><!-- the js will put the conference here --></div>
<div id="joinButtonContainer">
<div class="joinConferenceFloating">
<div class="joinConferencePrompt">
<!-- TODO: i18n -->
<h2>Jitsi Video Conference</h2>
<button type="button" id="joinButton">Join Conference</button>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,75 @@
/*
Copyright 2020 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Match the user's theme: https://github.com/vector-im/riot-web/issues/12794
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: url('~matrix-react-sdk/res/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
}
body {
font-family: Nunito, Arial, Helvetica, sans-serif;
background-color: #181b21;
color: #edf3ff;
}
body, html {
padding: 0;
margin: 0;
}
#jitsiContainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
#joinButtonContainer {
display: table;
position: absolute;
height: 100%;
width: 100%;
}
.joinConferenceFloating {
display: table-cell;
vertical-align: middle;
}
.joinConferencePrompt {
margin-left: auto;
margin-right: auto;
width: 90%;
text-align: center;
}
#joinButton {
// A mix of AccessibleButton styles
cursor: pointer;
padding: 7px 18px;
text-align: center;
border-radius: 4px;
display: inline-block;
font-size: 14px;
color: #ffffff;
background-color: #03b381;
border: 0;
}

127
src/vector/jitsi/index.ts Normal file
View file

@ -0,0 +1,127 @@
/*
Copyright 2020 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// We have to trick webpack into loading our CSS for us.
require("./index.scss");
import * as qs from 'querystring';
import { Capability, WidgetApi } from "matrix-react-sdk/src/widgets/WidgetApi";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { loadConfig, preparePlatform } from "../initial-load";
// Dev note: we use raw JS without many dependencies to reduce bundle size.
// We do not need all of React to render a Jitsi conference.
declare var JitsiMeetExternalAPI: any;
let inConference = false;
// Jitsi params
let jitsiDomain: string;
let conferenceId: string;
let displayName: string;
let avatarUrl: string;
let userId: string;
let widgetApi: WidgetApi;
(async function () {
try {
// The widget's options are encoded into the fragment to avoid leaking info to the server. The widget
// spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
const widgetQuery = qs.parse(window.location.hash.substring(1));
const query = Object.assign({}, qs.parse(window.location.search.substring(1)), widgetQuery);
const qsParam = (name: string, optional = false): string => {
if (!optional && (!query[name] || typeof (query[name]) !== 'string')) {
throw new Error(`Expected singular ${name} in query string`);
}
return <string>query[name];
};
// Set this up as early as possible because Riot will be hitting it almost immediately.
widgetApi = new WidgetApi(qsParam('parentUrl'), qsParam('widgetId'), [
Capability.AlwaysOnScreen,
]);
widgetApi.waitReady().then(async () => {
// Start off by ensuring we're not stuck on screen
await widgetApi.setAlwaysOnScreen(false);
});
// Bootstrap ourselves for loading the script and such
preparePlatform();
await loadConfig();
// Populate the Jitsi params now
jitsiDomain = qsParam('conferenceDomain', true) || SdkConfig.get()['jitsi']['preferredDomain'];
conferenceId = qsParam('conferenceId');
displayName = qsParam('displayName', true);
avatarUrl = qsParam('avatarUrl', true); // http not mxc
userId = qsParam('userId');
// Get the Jitsi Meet API loaded up as fast as possible, but ensure that the widget's postMessage
// receiver (WidgetApi) is up and running first.
const scriptTag = document.createElement("script");
scriptTag.src = SdkConfig.get()['jitsi']['externalApiUrl'];
document.body.appendChild(scriptTag);
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795)
document.getElementById("joinButton").onclick = () => joinConference();
} catch (e) {
console.error("Error setting up Jitsi widget", e);
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget";
switchVisibleContainers();
}
})();
function switchVisibleContainers() {
inConference = !inConference;
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset';
}
function joinConference() { // event handler bound in HTML
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, {
width: "100%",
height: "100%",
parentNode: document.querySelector("#jitsiContainer"),
roomName: conferenceId,
interfaceConfigOverwrite: {
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
MAIN_TOOLBAR_BUTTONS: [],
VIDEO_LAYOUT_FIT: "height",
},
});
if (displayName) meetApi.executeCommand("displayName", displayName);
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
if (userId) meetApi.executeCommand("email", userId);
meetApi.on("readyToClose", () => {
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works
document.getElementById("jitsiContainer").innerHTML = "";
});
}

View file

@ -34,6 +34,7 @@ module.exports = (env, argv) => {
"bundle": "./src/vector/index.js", "bundle": "./src/vector/index.js",
"indexeddb-worker": "./src/vector/indexeddb-worker.js", "indexeddb-worker": "./src/vector/indexeddb-worker.js",
"mobileguide": "./src/vector/mobile_guide/index.js", "mobileguide": "./src/vector/mobile_guide/index.js",
"jitsi": "./src/vector/jitsi/index.ts",
"usercontent": "./node_modules/matrix-react-sdk/src/usercontent/index.js", "usercontent": "./node_modules/matrix-react-sdk/src/usercontent/index.js",
// CSS themes // CSS themes
@ -303,13 +304,21 @@ module.exports = (env, argv) => {
// HtmlWebpackPlugin will screw up our formatting like the names // HtmlWebpackPlugin will screw up our formatting like the names
// of the themes and which chunks we actually care about. // of the themes and which chunks we actually care about.
inject: false, inject: false,
excludeChunks: ['mobileguide', 'usercontent'], excludeChunks: ['mobileguide', 'usercontent', 'jitsi'],
minify: argv.mode === 'production', minify: argv.mode === 'production',
vars: { vars: {
og_image_url: og_image_url, og_image_url: og_image_url,
}, },
}), }),
// This is the jitsi widget wrapper (embedded, so isolated stack)
new HtmlWebpackPlugin({
template: './src/vector/jitsi/index.html',
filename: 'jitsi.html',
minify: argv.mode === 'production',
chunks: ['jitsi'],
}),
// This is the mobile guide's entry point (separate for faster mobile loading) // This is the mobile guide's entry point (separate for faster mobile loading)
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './src/vector/mobile_guide/index.html', template: './src/vector/mobile_guide/index.html',