mirror of
https://github.com/vector-im/element-web.git
synced 2024-09-10 07:05:14 +00:00
Merge pull request #12780 from vector-im/travis/wrapped-jitsi
Use a local widget wrapper for Jitsi calls
This commit is contained in:
commit
775e1fc4ae
8 changed files with 308 additions and 36 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = (
|
||||
<div>
|
||||
<p>
|
||||
|
@ -260,7 +234,7 @@ export async function loadApp() {
|
|||
<p>
|
||||
{_t(
|
||||
"The message from the parser is: %(message)s",
|
||||
{message: configError.err.message || _t("Invalid JSON")},
|
||||
{message: configInfo.configError.err.message || _t("Invalid JSON")},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -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(<div className="error">
|
||||
Unable to load config file: please refresh the page to try again.
|
||||
</div>, 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()}
|
||||
|
|
58
src/vector/initial-load.ts
Normal file
58
src/vector/initial-load.ts
Normal 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};
|
||||
}
|
19
src/vector/jitsi/index.html
Normal file
19
src/vector/jitsi/index.html
Normal 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>
|
75
src/vector/jitsi/index.scss
Normal file
75
src/vector/jitsi/index.scss
Normal 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
127
src/vector/jitsi/index.ts
Normal 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 = "";
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue