diff --git a/config.sample.json b/config.sample.json
index 69dc396884..b8dc2fbb4c 100644
--- a/config.sample.json
+++ b/config.sample.json
@@ -22,7 +22,6 @@
"https://scalar-staging.vector.im/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",
"defaultCountryCode": "GB",
"showLabsSettings": false,
@@ -52,5 +51,9 @@
},
"settingDefaults": {
"breadcrumbs": true
+ },
+ "jitsi": {
+ "preferredDomain": "jitsi.riot.im",
+ "externalApiUrl": "https://jitsi.riot.im/libs/external_api.min.js"
}
}
diff --git a/docs/config.md b/docs/config.md
index d11d8638cd..40e71361bb 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -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.
Set this to your Riot instance URL if you run an unfederated server (eg:
"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
hosted on riot.im. This is the image used if links to your copy of Riot
diff --git a/src/vector/app.js b/src/vector/app.js
index da46dffd76..6a1635dd50 100644
--- a/src/vector/app.js
+++ b/src/vector/app.js
@@ -39,9 +39,6 @@ import url from 'url';
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 SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
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 CallHandler from 'matrix-react-sdk/src/CallHandler';
+import {loadConfig, preparePlatform} from "./initial-load";
let lastLocationHashSet = null;
@@ -191,35 +189,11 @@ export async function loadApp() {
await loadOlm();
// set the platform for react sdk
- if (window.ipcRenderer) {
- console.log("Using Electron platform");
- const plaf = new ElectronPlatform();
- PlatformPeg.set(plaf);
- } else {
- console.log("Using Web platform");
- PlatformPeg.set(new WebPlatform());
- }
-
+ preparePlatform();
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.
- SdkConfig.put(configJson);
+ // Load the config from the platform
+ const configInfo = await loadConfig();
// Load language after loading config.json so that settingsDefaults.language can be applied
await loadLanguage();
@@ -248,7 +222,7 @@ export async function loadApp() {
await setTheme();
// Now that we've loaded the theme (CSS), display the config syntax error if needed.
- if (configSyntaxError) {
+ if (configInfo.configSyntaxError) {
const errorMessage = (
@@ -260,7 +234,7 @@ export async function loadApp() {
{_t(
"The message from the parser is: %(message)s",
- {message: configError.err.message || _t("Invalid JSON")},
+ {message: configInfo.configError.err.message || _t("Invalid JSON")},
)}
@@ -280,7 +254,7 @@ export async function loadApp() {
const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname;
console.log("Vector starting at " + urlWithoutQuery);
- if (configError) {
+ if (configInfo.configError) {
window.matrixChat = ReactDOM.render(
Unable to load config file: please refresh the page to try again.
, document.getElementById('matrixchat'));
@@ -298,7 +272,7 @@ export async function loadApp() {
config={newConfig}
realQueryParams={params}
startingFragmentQueryParams={fragparts.params}
- enableGuest={!configJson.disable_guests}
+ enableGuest={!SdkConfig.get().disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
diff --git a/src/vector/initial-load.ts b/src/vector/initial-load.ts
new file mode 100644
index 0000000000..7bd73c5a4d
--- /dev/null
+++ b/src/vector/initial-load.ts
@@ -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 ((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};
+}
diff --git a/src/vector/jitsi/index.html b/src/vector/jitsi/index.html
new file mode 100644
index 0000000000..58184e0a5d
--- /dev/null
+++ b/src/vector/jitsi/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Jitsi Widget
+
+
+
+
+
+
diff --git a/src/vector/jitsi/index.scss b/src/vector/jitsi/index.scss
new file mode 100644
index 0000000000..fc33c9d8d0
--- /dev/null
+++ b/src/vector/jitsi/index.scss
@@ -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;
+}
diff --git a/src/vector/jitsi/index.ts b/src/vector/jitsi/index.ts
new file mode 100644
index 0000000000..5ca99dbd3f
--- /dev/null
+++ b/src/vector/jitsi/index.ts
@@ -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 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 = "";
+ });
+}
diff --git a/webpack.config.js b/webpack.config.js
index fbee94766d..f54655b5bd 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -34,6 +34,7 @@ module.exports = (env, argv) => {
"bundle": "./src/vector/index.js",
"indexeddb-worker": "./src/vector/indexeddb-worker.js",
"mobileguide": "./src/vector/mobile_guide/index.js",
+ "jitsi": "./src/vector/jitsi/index.ts",
"usercontent": "./node_modules/matrix-react-sdk/src/usercontent/index.js",
// CSS themes
@@ -303,13 +304,21 @@ module.exports = (env, argv) => {
// HtmlWebpackPlugin will screw up our formatting like the names
// of the themes and which chunks we actually care about.
inject: false,
- excludeChunks: ['mobileguide', 'usercontent'],
+ excludeChunks: ['mobileguide', 'usercontent', 'jitsi'],
minify: argv.mode === 'production',
vars: {
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)
new HtmlWebpackPlugin({
template: './src/vector/mobile_guide/index.html',