Merge branch 'develop' of github.com:vector-im/element-web into t3chguy/querystring
Conflicts: package.json src/@types/global.d.ts src/vector/app.tsx src/vector/jitsi/index.ts src/vector/platform/WebPlatform.ts yarn.lock
This commit is contained in:
commit
b03b4582c0
266 changed files with 11675 additions and 10340 deletions
|
@ -9,9 +9,12 @@
|
|||
<div id="joinButtonContainer">
|
||||
<div class="joinConferenceFloating">
|
||||
<div class="joinConferencePrompt">
|
||||
<span class="icon"><!-- managed by CSS --></span>
|
||||
<!-- TODO: i18n -->
|
||||
<h2>Jitsi Video Conference</h2>
|
||||
<button type="button" id="joinButton">Join Conference</button>
|
||||
<div id="widgetActionContainer">
|
||||
<button type="button" id="joinButton">Join Conference</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ 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
|
||||
// TODO: Match the user's theme: https://github.com/vector-im/element-web/issues/12794
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
|
@ -23,10 +23,19 @@ limitations under the License.
|
|||
src: url('~matrix-react-sdk/res/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
$dark-fg: #edf3ff;
|
||||
$dark-bg: #363c43;
|
||||
$light-fg: #2e2f32;
|
||||
$light-bg: #fff;
|
||||
body {
|
||||
font-family: Nunito, Arial, Helvetica, sans-serif;
|
||||
background-color: #181b21;
|
||||
color: #edf3ff;
|
||||
background-color: $dark-bg;
|
||||
color: $dark-fg;
|
||||
}
|
||||
|
||||
body.theme-light {
|
||||
background-color: $light-bg;
|
||||
color: $light-fg;
|
||||
}
|
||||
|
||||
body, html {
|
||||
|
@ -73,3 +82,26 @@ body, html {
|
|||
background-color: #03b381;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
$icon-size: 42px;
|
||||
margin-top: -$icon-size; // to visually center the form
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
background-size: contain;
|
||||
background-color: $dark-fg;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: url("~matrix-react-sdk/res/img/element-icons/call/video-call.svg");
|
||||
mask-size: $icon-size;
|
||||
display: block;
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
margin: 0 auto; // center
|
||||
}
|
||||
}
|
||||
|
||||
body.theme-light .icon::before {
|
||||
background-color: $light-fg;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,21 @@ limitations under the License.
|
|||
require("./index.scss");
|
||||
|
||||
import { parseQs, parseQsFromFragment } from "../url_utils";
|
||||
import { Capability, WidgetApi } from "matrix-react-sdk/src/widgets/WidgetApi";
|
||||
import { KJUR } from 'jsrsasign';
|
||||
import {
|
||||
IOpenIDCredentials,
|
||||
IWidgetApiRequest,
|
||||
VideoConferenceCapabilities,
|
||||
WidgetApi,
|
||||
} from "matrix-widget-api";
|
||||
import { ElementWidgetActions } from "matrix-react-sdk/src/stores/widgets/ElementWidgetActions";
|
||||
|
||||
const JITSI_OPENIDTOKEN_JWT_AUTH = 'openidtoken-jwt';
|
||||
|
||||
// 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;
|
||||
declare let JitsiMeetExternalAPI: any;
|
||||
|
||||
let inConference = false;
|
||||
|
||||
|
@ -33,10 +42,15 @@ let conferenceId: string;
|
|||
let displayName: string;
|
||||
let avatarUrl: string;
|
||||
let userId: string;
|
||||
let jitsiAuth: string;
|
||||
let roomId: string;
|
||||
let openIdToken: IOpenIDCredentials;
|
||||
let roomName: string;
|
||||
|
||||
let widgetApi: WidgetApi;
|
||||
let meetApi: any; // JitsiMeetExternalAPI
|
||||
|
||||
(async function () {
|
||||
(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.
|
||||
|
@ -54,13 +68,33 @@ let widgetApi: WidgetApi;
|
|||
// out into a browser.
|
||||
const parentUrl = qsParam('parentUrl', true);
|
||||
const widgetId = qsParam('widgetId', true);
|
||||
const theme = qsParam('theme', true);
|
||||
|
||||
// Set this up as early as possible because Riot will be hitting it almost immediately.
|
||||
if (theme) {
|
||||
document.body.classList.add(`theme-${theme.replace(" ", "_")}`);
|
||||
}
|
||||
|
||||
// Set this up as early as possible because Element will be hitting it almost immediately.
|
||||
let readyPromise: Promise<[void, void]>;
|
||||
if (parentUrl && widgetId) {
|
||||
widgetApi = new WidgetApi(qsParam('parentUrl'), qsParam('widgetId'), [
|
||||
Capability.AlwaysOnScreen,
|
||||
const parentOrigin = new URL(qsParam('parentUrl')).origin;
|
||||
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
|
||||
widgetApi.requestCapabilities(VideoConferenceCapabilities);
|
||||
readyPromise = Promise.all([
|
||||
new Promise<void>(resolve => {
|
||||
widgetApi.once(`action:${ElementWidgetActions.ClientReady}`, ev => {
|
||||
ev.preventDefault();
|
||||
widgetApi.transport.reply(ev.detail, {});
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
new Promise<void>(resolve => {
|
||||
widgetApi.once("ready", () => resolve());
|
||||
}),
|
||||
]);
|
||||
widgetApi.expectingExplicitReady = true;
|
||||
widgetApi.start();
|
||||
} else {
|
||||
console.warn("No parent URL or no widget ID - assuming no widget API is available");
|
||||
}
|
||||
|
||||
// Populate the Jitsi params now
|
||||
|
@ -69,40 +103,130 @@ let widgetApi: WidgetApi;
|
|||
displayName = qsParam('displayName', true);
|
||||
avatarUrl = qsParam('avatarUrl', true); // http not mxc
|
||||
userId = qsParam('userId');
|
||||
jitsiAuth = qsParam('auth', true);
|
||||
roomId = qsParam('roomId', true);
|
||||
roomName = qsParam('roomName', true);
|
||||
|
||||
if (widgetApi) {
|
||||
await widgetApi.waitReady();
|
||||
await readyPromise;
|
||||
await widgetApi.setAlwaysOnScreen(false); // start off as detachable from the screen
|
||||
|
||||
// See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
|
||||
// Request credentials, give callback to continue when received
|
||||
openIdToken = await widgetApi.requestOpenIDConnectToken();
|
||||
console.log("Got OpenID Connect token");
|
||||
}
|
||||
|
||||
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/element-web/issues/12795)
|
||||
|
||||
widgetApi.on(`action:${ElementWidgetActions.HangupCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
if (meetApi) meetApi.executeCommand('hangup');
|
||||
widgetApi.transport.reply(ev.detail, {}); // ack
|
||||
},
|
||||
);
|
||||
widgetApi.on(`action:${ElementWidgetActions.StartLiveStream}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
if (meetApi) {
|
||||
meetApi.executeCommand('startRecording', {
|
||||
mode: 'stream',
|
||||
// this looks like it should be rtmpStreamKey but we may be on too old
|
||||
// a version of jitsi meet
|
||||
//rtmpStreamKey: ev.detail.data.rtmpStreamKey,
|
||||
youtubeStreamKey: ev.detail.data.rtmpStreamKey,
|
||||
});
|
||||
widgetApi.transport.reply(ev.detail, {}); // ack
|
||||
} else {
|
||||
widgetApi.transport.reply(ev.detail, { error: { message: "Conference not joined" } });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795)
|
||||
|
||||
document.getElementById("joinButton").onclick = () => joinConference();
|
||||
enableJoinButton(); // always enable the button
|
||||
} catch (e) {
|
||||
console.error("Error setting up Jitsi widget", e);
|
||||
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget";
|
||||
switchVisibleContainers();
|
||||
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
|
||||
}
|
||||
})();
|
||||
|
||||
function enableJoinButton() {
|
||||
document.getElementById("joinButton").onclick = () => joinConference();
|
||||
}
|
||||
|
||||
function switchVisibleContainers() {
|
||||
inConference = !inConference;
|
||||
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
|
||||
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWT token fot jitsi openidtoken-jwt auth
|
||||
*
|
||||
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||
*/
|
||||
function createJWTToken() {
|
||||
// Header
|
||||
const header = { alg: 'HS256', typ: 'JWT' };
|
||||
// Payload
|
||||
const payload = {
|
||||
// As per Jitsi token auth, `iss` needs to be set to something agreed between
|
||||
// JWT generating side and Prosody config. Since we have no configuration for
|
||||
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
|
||||
iss: jitsiDomain,
|
||||
sub: jitsiDomain,
|
||||
aud: `https://${jitsiDomain}`,
|
||||
room: "*",
|
||||
context: {
|
||||
matrix: {
|
||||
token: openIdToken.access_token,
|
||||
room_id: roomId,
|
||||
server_name: openIdToken.matrix_server_name,
|
||||
},
|
||||
user: {
|
||||
avatar: avatarUrl,
|
||||
name: displayName,
|
||||
},
|
||||
},
|
||||
};
|
||||
// Sign JWT
|
||||
// The secret string here is irrelevant, we're only using the JWT
|
||||
// to transport data to Prosody in the Jitsi stack.
|
||||
return KJUR.jws.JWS.sign(
|
||||
'HS256',
|
||||
JSON.stringify(header),
|
||||
JSON.stringify(payload),
|
||||
'notused',
|
||||
);
|
||||
}
|
||||
|
||||
function joinConference() { // event handler bound in HTML
|
||||
let jwt;
|
||||
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
|
||||
if (!openIdToken?.access_token) { // eslint-disable-line camelcase
|
||||
// We've failing to get a token, don't try to init conference
|
||||
console.warn('Expected to have an OpenID credential, cannot initialize widget.');
|
||||
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
|
||||
return;
|
||||
}
|
||||
jwt = createJWTToken();
|
||||
}
|
||||
|
||||
switchVisibleContainers();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
if (widgetApi) widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works
|
||||
if (widgetApi) {
|
||||
// ignored promise because we don't care if it works
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
widgetApi.setAlwaysOnScreen(true);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " +
|
||||
"they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
|
||||
"our fragment values and not recognizing the options.",
|
||||
);
|
||||
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, {
|
||||
const options = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
parentNode: document.querySelector("#jitsiContainer"),
|
||||
|
@ -113,17 +237,25 @@ function joinConference() { // event handler bound in HTML
|
|||
MAIN_TOOLBAR_BUTTONS: [],
|
||||
VIDEO_LAYOUT_FIT: "height",
|
||||
},
|
||||
});
|
||||
jwt: jwt,
|
||||
};
|
||||
|
||||
meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
|
||||
if (displayName) meetApi.executeCommand("displayName", displayName);
|
||||
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
|
||||
if (userId) meetApi.executeCommand("email", userId);
|
||||
if (roomName) meetApi.executeCommand("subject", roomName);
|
||||
|
||||
meetApi.on("readyToClose", () => {
|
||||
switchVisibleContainers();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
if (widgetApi) widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works
|
||||
if (widgetApi) {
|
||||
// ignored promise because we don't care if it works
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
widgetApi.setAlwaysOnScreen(false);
|
||||
}
|
||||
|
||||
document.getElementById("jitsiContainer").innerHTML = "";
|
||||
meetApi = null;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue