mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-09-29 20:25:58 +00:00
Fleshing out the Dimension API, widget code
This commit is contained in:
parent
3fef47e369
commit
5c28ec1d94
27
package-lock.json
generated
27
package-lock.json
generated
@ -2110,6 +2110,14 @@
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"bigbluebutton-api-js": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bigbluebutton-api-js/-/bigbluebutton-api-js-2.2.1.tgz",
|
||||
"integrity": "sha512-pkLc3tur/5UPLlC7hlCfR0fta3ajbVtO48IzIn10Tkm00Ren1OP89cp1i1dfRUVroGq1K4VwqIOpUPWlHghw/w==",
|
||||
"requires": {
|
||||
"jssha": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
|
||||
@ -6951,6 +6959,11 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jssha": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz",
|
||||
"integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q=="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -13962,6 +13975,20 @@
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/node": "^12.0.10",
|
||||
"@types/validator": "^10.11.1",
|
||||
"bigbluebutton-api-js": "^2.2.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"config": "^3.1.0",
|
||||
"dns-then": "^0.1.0",
|
||||
@ -61,7 +62,8 @@
|
||||
"typescript-ioc": "^1.2.5",
|
||||
"typescript-rest": "^2.2.0",
|
||||
"umzug": "^2.2.0",
|
||||
"url": "^0.11.0"
|
||||
"url": "^0.11.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/animations": "^8.0.3",
|
||||
|
@ -2,10 +2,13 @@ import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import * as request from "request";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { URL } from "url";
|
||||
import { BigBlueButtonJoinRequest } from "../../models/Widget";
|
||||
import { BigBlueButtonJoinResponse, BigBlueButtonWidgetResponse } from "../../models/WidgetResponses";
|
||||
import { BigBlueButtonJoinRequest, BigBlueButtonCreateAndJoinMeetingRequest } from "../../models/Widget";
|
||||
import { BigBlueButtonJoinResponse, BigBlueButtonCreateAndJoinMeetingResponse, BigBlueButtonWidgetResponse } from "../../models/WidgetResponses";
|
||||
import { AutoWired } from "typescript-ioc/es6";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { sha1, sha256 } from "../../utils/hashing";
|
||||
import config from "../../config";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
|
||||
/**
|
||||
* API for the BigBlueButton widget.
|
||||
@ -163,6 +166,15 @@ export class DimensionBigBlueButtonService {
|
||||
return {url: joinUrl};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an HTTP request.
|
||||
* @param {string} method The HTTP method to use.
|
||||
* @param {string} url The URL (without query parameters) to request.
|
||||
* @param {string} qs The query parameters to use with the request.
|
||||
* @param {string} body The JSON body of the request
|
||||
* @param {boolean} followRedirect Whether to follow redirect responses automatically.
|
||||
* @private
|
||||
*/
|
||||
private async doRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
@ -180,6 +192,7 @@ export class DimensionBigBlueButtonService {
|
||||
followRedirect: followRedirect,
|
||||
jar: true, // remember cookies between requests
|
||||
json: false, // expect html
|
||||
|
||||
}, (err, res, _body) => {
|
||||
try {
|
||||
if (err) {
|
||||
@ -205,20 +218,40 @@ export class DimensionBigBlueButtonService {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("widget")
|
||||
public async widget(): Promise<BigBlueButtonWidgetResponse|ApiError> {
|
||||
@Path("widget_state")
|
||||
public async widget(
|
||||
@QueryParam("room_id") roomId: string,
|
||||
): Promise<BigBlueButtonWidgetResponse|ApiError> {
|
||||
// Hash the room ID in order to generate a unique widget ID
|
||||
const widgetId = sha256(roomId + "bigbluebutton");
|
||||
|
||||
// TODO: Make configurable
|
||||
const widgetTitle = "BigBlueButton Video Conference";
|
||||
const widgetSubTitle = "Join the conference";
|
||||
const widgetAvatarUrl = "mxc://fosdem.org/0eea5cb67fbe964399060b10b09a22e45e2226ee";
|
||||
|
||||
// TODO: What should we put for the creatorUserId? Also make it configurable?
|
||||
const widgetCreatorUserId = "@bobbb:localhost";
|
||||
|
||||
// TODO: Set to configured Dimension publicUrl
|
||||
let widgetUrl = "http://localhost:8082/widgets/bigbluebutton";
|
||||
|
||||
// Add all necessary client variables to the url when loading the widget
|
||||
widgetUrl += "?widgetId=$matrix_widget_id&roomId=$matrix_room_id#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt";
|
||||
|
||||
return {
|
||||
"widget_id": "1234",
|
||||
"widget_id": widgetId,
|
||||
"widget": {
|
||||
"creatorUserId": "@admin:localhost",
|
||||
"id": "1234",
|
||||
"creatorUserId": widgetCreatorUserId,
|
||||
"id": widgetId,
|
||||
"type": "m.custom",
|
||||
"waitForIframeLoad": true,
|
||||
"name": "Livestream / Q&A",
|
||||
"avatar_url": "mxc://fosdem.org/0eea5cb67fbe964399060b10b09a22e45e2226ee",
|
||||
"url": "https://widgets-fosdem.ems.host/widgets/hybrid.html?widgetId=$matrix_widget_id&roomId=$matrix_room_id#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt",
|
||||
"name": widgetTitle,
|
||||
"avatar_url": widgetAvatarUrl,
|
||||
"url": widgetUrl,
|
||||
"data": {
|
||||
"title": "Join the conference thingy to ask questions",
|
||||
"title": widgetSubTitle,
|
||||
"widgetVersion": 2,
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@ -229,4 +262,81 @@ export class DimensionBigBlueButtonService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("createAndJoinMeeting")
|
||||
public async createAndJoinMeeting(
|
||||
@QueryParam("room_id") roomId: string,
|
||||
): Promise<BigBlueButtonCreateAndJoinMeetingResponse|ApiError> {
|
||||
// Check if a meeting already exists for this room...
|
||||
LogService.info("BigBlueButton", "Got a meeting create and join request for room: " + roomId);
|
||||
|
||||
// Create a new meeting
|
||||
LogService.info("BigBlueButton", "Using secret: " + config.bigbluebutton.sharedSecret);
|
||||
|
||||
// NOTE: BBB meetings will by default end a minute or two after the last person leaves.
|
||||
const queryParameters = {
|
||||
meetingID: roomId + "bigbluebuttondimension",
|
||||
};
|
||||
const response = await this.makeBBBApiCall("GET", "create", queryParameters, null);
|
||||
LogService.info("BigBlueButton", response);
|
||||
|
||||
return {
|
||||
url: "https://bla.com",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API call to the configured BBB server instance
|
||||
* @param {string} method The HTTP method to use for the request.
|
||||
* @param {string} apiCallName The name of the API (the last bit of the endpoint) to call. e.g 'create', 'join'.
|
||||
* @param {any} queryParameters The query parameters to use in the request.
|
||||
* @param {any} body The body of the request.
|
||||
* @private
|
||||
* @returns {BigBlueButtonApiResponse} The response to the call.
|
||||
*/
|
||||
private async makeBBBApiCall(
|
||||
method: string,
|
||||
apiCallName: string,
|
||||
queryParameters: any,
|
||||
body: any,
|
||||
): Promise<any> {
|
||||
// Build the URL path from the api name, query parameter string, shared secret and checksum
|
||||
// Docs: https://docs.bigbluebutton.org/dev/api.html#usage
|
||||
|
||||
LogService.info("BigBlueButton", "given query params: " + queryParameters.meetingID);
|
||||
// Convert the query parameters map into a string
|
||||
// We URL encode each value, as doRequest does so as well. If we don't, our resulting checksum will not match
|
||||
const widgetQueryString = Object.keys(queryParameters).map(k => k + "=" + this.encodeForUrl(queryParameters[k])).join("&");
|
||||
LogService.info("BigBlueButton", "queryString: " + widgetQueryString);
|
||||
|
||||
// SHA1 hash the api name and query parameters to get the checksum, and add it to the set of query parameters
|
||||
queryParameters.checksum = sha1(apiCallName + widgetQueryString + config.bigbluebutton.sharedSecret);
|
||||
LogService.info("BigBlueButton", "hashing: " + apiCallName + widgetQueryString + config.bigbluebutton.sharedSecret);
|
||||
|
||||
// Get the URL host and path using the configured api base and the API call name
|
||||
const url = `${config.bigbluebutton.apiBaseUrl}/${apiCallName}`;
|
||||
const qsWithChecksum = Object.keys(queryParameters).map(k => k + "=" + this.encodeForUrl(queryParameters[k])).join("&");
|
||||
LogService.info("BigBlueButton", "final url: " + url + "?" + qsWithChecksum);
|
||||
|
||||
// Now make the request!
|
||||
// TODO: Unfortunately doRequest is URLencoding the query parameters which the checksum stuff doesn't take into account.
|
||||
// So we need to disable URL encoding here, or do it when calculating the checksum
|
||||
const response = await this.doRequest(method, url, queryParameters, body);
|
||||
|
||||
// Parse and return the XML from the response
|
||||
LogService.info("BigBlueButton", response.body);
|
||||
return await parseStringPromise(response.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string in the same fashion browsers do (encoding ! and other characters)
|
||||
* @param {string} text The text to encode
|
||||
*/
|
||||
encodeForUrl(text: string) {
|
||||
// use + instead of %20 for space to match what the Java tools do.
|
||||
// encodeURIComponent doesn't escape !'()* but browsers do, so manually escape them.
|
||||
return encodeURIComponent(text).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ export interface DimensionConfig {
|
||||
telegram: {
|
||||
botToken: string;
|
||||
};
|
||||
bigbluebutton: {
|
||||
apiBaseUrl: string;
|
||||
sharedSecret: string;
|
||||
};
|
||||
stickers: {
|
||||
enabled: boolean;
|
||||
stickerBot: string;
|
||||
|
@ -5,3 +5,10 @@ export interface BigBlueButtonJoinRequest {
|
||||
// The name the user wishes to join the meeting with
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
export interface BigBlueButtonCreateAndJoinMeetingRequest {
|
||||
// The ID of the room that the BBB meeting is a part of
|
||||
roomId: string;
|
||||
// The name the user wishes to join the meeting with
|
||||
fullName: string;
|
||||
}
|
||||
|
@ -3,6 +3,11 @@ export interface BigBlueButtonJoinResponse {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface BigBlueButtonCreateAndJoinMeetingResponse {
|
||||
// The meeting URL the client should load to join the meeting
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface BigBlueButtonWidgetResponse {
|
||||
widget_id: string;
|
||||
widget: {
|
||||
@ -15,6 +20,7 @@ export interface BigBlueButtonWidgetResponse {
|
||||
url: string;
|
||||
data: {
|
||||
title: string;
|
||||
widgetVersion: number;
|
||||
}
|
||||
};
|
||||
layout: {
|
||||
|
@ -3,3 +3,11 @@ import * as crypto from "crypto";
|
||||
export function md5(text: string): string {
|
||||
return crypto.createHash("md5").update(text).digest('hex').toLowerCase();
|
||||
}
|
||||
|
||||
export function sha1(text: string): string {
|
||||
return crypto.createHash("sha1").update(text).digest('hex').toLowerCase();
|
||||
}
|
||||
|
||||
export function sha256(text: string): string {
|
||||
return crypto.createHash("sha256").update(text).digest('hex').toLowerCase();
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||
|
||||
protected OnNewWidgetPrepared(widget: EditableWidget): void {
|
||||
widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl;
|
||||
widget.dimension.newData["widgetVersion"] = this.bigBlueButtonWidget.options.widgetVersion;
|
||||
}
|
||||
|
||||
protected OnWidgetBeforeAdd(widget: EditableWidget) {
|
||||
@ -50,5 +51,6 @@ export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||
});
|
||||
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
|
||||
widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString;
|
||||
console.log("URL ended up as:", widget.dimension.newUrl);
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,11 @@ export interface FE_BigBlueButtonJoin {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FE_BigBlueButtonCreateAndJoinMeeting {
|
||||
// The meeting URL the client should load to join the meeting
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FE_StickerConfig {
|
||||
enabled: boolean;
|
||||
stickerBot: string;
|
||||
@ -96,6 +101,7 @@ export interface FE_JitsiWidget extends FE_Widget {
|
||||
export interface FE_BigBlueButtonWidget extends FE_Widget {
|
||||
options: {
|
||||
conferenceUrl: string;
|
||||
widgetVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AuthedApi } from "../authed-api";
|
||||
import { FE_BigBlueButtonJoin } from "../../models/integration"
|
||||
import { FE_BigBlueButtonJoin, FE_BigBlueButtonCreateAndJoinMeeting } from "../../models/integration"
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ApiError } from "../../../../../src/api/ApiError";
|
||||
|
||||
@ -13,4 +13,9 @@ export class BigBlueButtonApiService extends AuthedApi {
|
||||
public joinMeeting(url: string, name: string): Promise<FE_BigBlueButtonJoin|ApiError> {
|
||||
return this.authedGet<FE_BigBlueButtonJoin|ApiError>("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise();
|
||||
}
|
||||
|
||||
public createAndJoinMeeting(roomId: string): Promise<FE_BigBlueButtonCreateAndJoinMeeting|ApiError> {
|
||||
return this.authedGet<FE_BigBlueButtonCreateAndJoinMeeting|ApiError>("/api/v1/dimension/bigbluebutton/join_meeting", {roomId: roomId}).toPromise();
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"
|
||||
import { CapableWidget } from "../capable-widget";
|
||||
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
||||
import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service";
|
||||
import { FE_BigBlueButtonJoin } from "../../shared/models/integration";
|
||||
import { FE_BigBlueButtonCreateAndJoinMeeting, FE_BigBlueButtonJoin } from "../../shared/models/integration";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
@Component({
|
||||
@ -25,6 +25,19 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||
private displayName: string;
|
||||
private userId: string;
|
||||
|
||||
/**
|
||||
* Whether we expect the meeting to be created on command.
|
||||
*
|
||||
* True if we'd like the meeting to be created, false if we have a greenlight URL leading to an existing meeting
|
||||
* and would like Dimension to translate that to a BigBlueButton meeting URL.
|
||||
*/
|
||||
private createMeeting: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the room, required if createMeeting is true.
|
||||
*/
|
||||
private roomId: string;
|
||||
|
||||
/**
|
||||
* The poll period in ms while waiting for a meeting to start
|
||||
*/
|
||||
@ -60,12 +73,15 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||
|
||||
let params: any = activatedRoute.snapshot.queryParams;
|
||||
|
||||
console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl);
|
||||
|
||||
this.roomId = params.roomId;
|
||||
this.createMeeting = params.createMeeting;
|
||||
this.conferenceUrl = params.conferenceUrl;
|
||||
this.displayName = params.displayName;
|
||||
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
|
||||
|
||||
console.log("BigBlueButton: should create meeting: " + this.createMeeting);
|
||||
console.log("BigBlueButton: got room ID: " + this.roomId);
|
||||
|
||||
// Set the widget ID if we have it
|
||||
ScalarWidgetApi.widgetId = params.widgetId;
|
||||
}
|
||||
@ -105,6 +121,44 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||
const joinName = `${this.displayName} (${this.userId})`;
|
||||
|
||||
// Make a request to Dimension requesting the join URL
|
||||
if (this.createMeeting === true) {
|
||||
// Ask Dimension to create the meeting for us and return the URL
|
||||
this.createAndJoinMeeting(joinName);
|
||||
} else {
|
||||
// Provide Dimension with a Greenlight URL, which it will transform into
|
||||
// a BBB meeting URL
|
||||
this.joinThroughGreenlightUrl(joinName);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask Dimension to create a meeting (or use an existing one) for this room and return the embeddable meeting URL
|
||||
private createAndJoinMeeting(joinName: string) {
|
||||
console.log("BigBlueButton: joining and creating meeting if it doesn't already exist, with fullname:", joinName);
|
||||
|
||||
this.bigBlueButtonApi.createAndJoinMeeting(this.roomId).then((response) => {
|
||||
if ("errorCode" in response) {
|
||||
// This is an instance of ApiError
|
||||
// if (response.errorCode === "WAITING_FOR_MEETING_START") {
|
||||
// // The meeting hasn't started yet
|
||||
// this.statusMessage = "Waiting for conference to start...";
|
||||
//
|
||||
// // Poll until it has
|
||||
// setTimeout(this.joinConference.bind(this), this.pollIntervalMillis, false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Otherwise this is a generic error
|
||||
this.statusMessage = "An error occurred while loading the meeting";
|
||||
}
|
||||
|
||||
// Retrieve and embed the meeting URL
|
||||
const joinUrl = (response as FE_BigBlueButtonCreateAndJoinMeeting).url;
|
||||
this.embedMeetingWithUrl(joinUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// Hand Dimension a Greenlight URL and receive a translated, embeddable meeting URL in response
|
||||
private joinThroughGreenlightUrl(joinName: string) {
|
||||
console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl);
|
||||
this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).then((response) => {
|
||||
if ("errorCode" in response) {
|
||||
@ -122,15 +176,20 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||
this.statusMessage = "An error occurred while loading the meeting";
|
||||
}
|
||||
|
||||
// Retrieve and embed the meeting URL
|
||||
const joinUrl = (response as FE_BigBlueButtonJoin).url;
|
||||
this.embedMeetingWithUrl(joinUrl);
|
||||
});
|
||||
}
|
||||
|
||||
private embedMeetingWithUrl(url: string) {
|
||||
// Check if the given URL is embeddable
|
||||
this.widgetApi.isEmbeddable(joinUrl).then(result => {
|
||||
this.widgetApi.isEmbeddable(url).then(result => {
|
||||
this.canEmbed = result.canEmbed;
|
||||
this.statusMessage = null;
|
||||
|
||||
// Embed the return meeting URL, joining the meeting
|
||||
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl);
|
||||
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
|
||||
// Inform the client that we would like the meeting to remain visible for its duration
|
||||
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
|
||||
@ -139,7 +198,6 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||
this.canEmbed = false;
|
||||
this.statusMessage = "Unable to embed meeting";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
Loading…
Reference in New Issue
Block a user