diff --git a/src/api/dimension/DimensionBigBlueButtonService.ts b/src/api/dimension/DimensionBigBlueButtonService.ts new file mode 100644 index 0000000..0a4f141 --- /dev/null +++ b/src/api/dimension/DimensionBigBlueButtonService.ts @@ -0,0 +1,207 @@ +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 } from "../../models/WidgetResponses"; +import { AutoWired } from "typescript-ioc/es6"; +import { ApiError } from "../ApiError"; + +/** + * API for the BigBlueButton widget. + */ +@Path("/api/v1/dimension/bigbluebutton") +@AutoWired +export class DimensionBigBlueButtonService { + + /** + * A regex used for extracting the authenticity token from the HTML of a + * greenlight server response + */ + private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`); + + // join handles the request from a client to join a BigBlueButton meeting + // + // The client is expected to send a link created by greenlight, the nice UI + // that's recommended to be installed on top of BBB, which is itself a BBB + // client. + // + // This greenlight link is nice, but greenlight unfortunately doesn't have any + // API, and no simple way for us to translate a link from it into a BBB meeting + // URL. It's intended to be loaded by browsers. You enter your preferred name, + // click submit, you potentially wait for the meeting to start, and then you + // finally get the link to join the meeting, and you load that. + // + // As there's no other way to do it, we just reverse-engineer it and pretend + // to be a browser below. We can't do this from the client side as widgets + // run in iframes and browsers can't inspect the content of an iframe if + // it's running on a separate domain. + // + // So the client gets a greenlight URL pasted into it. The flow is then: + // + // + // +---------+ +-----------+ +-------------+ +-----+ + // | Client | | Dimension | | Greenlight | | BBB | + // +---------+ +-----------+ +-------------+ +-----+ + // | | | | + // | | | | + // | | | | + // | | | | + // | /bigbluebutton/join&greenlightUrl=https://.../abc-def-123&fullName=bob | | | + // |---------------------------------------------------------------------------->| | | + // | | | | + // | | GET https://.../abc-def-123 | | + // | |-------------------------------------------------------------------------------------->| | + // | | | | + // | | Have some HTML | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | | Extract authenticity_token from HTML | | + // | |------------------------------------- | | + // | | | | | + // | |<------------------------------------ | | + // | | | | + // | | Extract cookies from HTTP response | | + // | |----------------------------------- | | + // | | | | | + // | |<---------------------------------- | | + // | | | | + // | | POST https://.../abc-def-123&authenticity_token=...&abc-def-123[join_name]=bob | | + // | |-------------------------------------------------------------------------------------->| | + // |===============================================================================================If the meeting has not started yet================================================| + // | | | | + // | | HTML https://.../abc-def-123 Meeting not started | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | 400 MEETING_NOT_STARTED_YET | | | + // |<----------------------------------------------------------------------------| | | + // | | | | + // | | | | + // | Wait a bit and restart the process | | | + // |------------------------------------- | | | + // | | | | | + // |<------------------------------------ | | | + // | | | | + // |=================================================================================================================================================================================| + // | | | | + // | | 302 Location: https://bbb.example.com/join?... | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | | Extract value of Location header | | + // | |--------------------------------- | | + // | | | | | + // | |<-------------------------------- | | + // | | | | + // | https://bbb.example.com/join?... | | | + // |<----------------------------------------------------------------------------| | | + // | | | | + // | GET https://bbb.example.com/join?... | | | + // |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->| + // | | | | + // | | Send back meeting page HTML | | + // |<--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + // + @GET + @Path("join") + public async join( + joinRequest: BigBlueButtonJoinRequest, + @QueryParam("greenlightUrl") greenlightURL: string, + @QueryParam("fullName") fullName: string, + ): Promise { + // Parse the greenlight url and retrieve the path + const greenlightMeetingID = new URL(greenlightURL).pathname; + + LogService.info("BigBlueButton", "URL from client: " + greenlightURL); + LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID); + LogService.info("BigBlueButton", "Name given from client: " + fullName); + LogService.info("BigBlueButton", joinRequest); + + // Query the URL the user has given us + let response = await this.doRequest("GET", greenlightURL); + if (!response || !response.body) { + throw new Error("Invalid response from Greenlight server while joining meeting"); + } + + // Attempt to extract the authenticity token + const matches = response.body.match(this.authenticityTokenRegexp); + if (matches.length < 2) { + throw new Error("Unable to find authenticity token for given 'greenlightUrl' parameter"); + } + const authenticityToken = matches[1]; + + // Give the authenticity token and desired name to greenlight, getting the + // join URL in return. Greenlight will send the URL back as a Location: + // header. We want to extract and return the contents of this header, rather + // than following it ourselves + + // Add authenticity token and full name to the query parameters + let queryParams = {authenticity_token: authenticityToken}; + queryParams[`${greenlightMeetingID}[join_name]`] = fullName; + + // Request the updated URL + response = await this.doRequest("POST", greenlightURL, queryParams, "{}", false); + if (!response || !response.body) { + throw new Error("Invalid response from Greenlight server while joining meeting"); + } + + if (!("location" in response.response.headers)) { + // We didn't get a meeting URL back. This could either happen due to an issue with the parameters + // sent to the server... or the meeting simply hasn't started yet. + + // Assume it hasn't started yet. Send a custom error code back to the client informing them to try + // again in a bit + return new ApiError( + 400, + {error: "Unable to find meeting URL in greenlight response"}, + "WAITING_FOR_MEETING_START", + ); + } + + // Return the join URL for the client to load + const joinUrl = response.response.headers["location"]; + LogService.info("BigBlueButton", "Sending back join URL: " + joinUrl) + return {url: joinUrl}; + } + + private async doRequest( + method: string, + url: string, + qs?: any, + body?: any, + followRedirect: boolean = true, + ): Promise { + // Query a URL, expecting an HTML response in return + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + body: body, + followRedirect: followRedirect, + jar: true, // remember cookies between requests + json: false, // expect html + }, (err, res, _body) => { + try { + if (err) { + LogService.error("BigBlueButtonWidget", "Error calling " + url); + LogService.error("BigBlueButtonWidget", err); + reject(err); + } else if (!res) { + LogService.error("BigBlueButtonWidget", "There is no response for " + url); + reject(new Error("No response provided - is the service online?")); + } else if (res.statusCode !== 200 && res.statusCode !== 302) { + LogService.error("BigBlueButtonWidget", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("BigBlueButtonWidget", res.body); + reject({body: res.body, status: res.statusCode}); + } else { + resolve({body: res.body, response: res}); + } + } catch (e) { + LogService.error("BigBlueButtonWidget", e); + reject(e); + } + }); + }); + } + +} diff --git a/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts b/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts new file mode 100644 index 0000000..27c00ba --- /dev/null +++ b/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_widgets", [ + { + type: "bigbluebutton", + name: "BigBlueButton", + avatarUrl: "/img/avatars/bigbluebutton.png", + isEnabled: true, + isPublic: true, + description: "Embed a BigBlueButton conference", + } + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_widgets", { + type: "bigbluebutton", + })); + } +} diff --git a/src/models/Widget.ts b/src/models/Widget.ts new file mode 100644 index 0000000..3a26ef2 --- /dev/null +++ b/src/models/Widget.ts @@ -0,0 +1,7 @@ +export interface BigBlueButtonJoinRequest { + // A URL supplied by greenlight, BigBlueButton's nice UI project that is itself + // a BigBlueButton client + greenlightUrl: string; + // The name the user wishes to join the meeting with + fullName: string; +} diff --git a/src/models/WidgetResponses.ts b/src/models/WidgetResponses.ts new file mode 100644 index 0000000..854ea31 --- /dev/null +++ b/src/models/WidgetResponses.ts @@ -0,0 +1,4 @@ +export interface BigBlueButtonJoinResponse { + // The meeting URL the client should load to join the meeting + url: string; +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 6f5496e..9d9dbf7 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -118,6 +118,9 @@ import { CKEditorModule } from "@ckeditor/ckeditor5-angular"; import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component"; import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component"; import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component"; +import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; +import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component"; +import { BigBlueButtonApiService } from "./shared/services/integrations/bigbluebutton-api.service"; @NgModule({ imports: [ @@ -147,7 +150,9 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo FullscreenButtonComponent, VideoWidgetWrapperComponent, JitsiWidgetWrapperComponent, + BigBlueButtonWidgetWrapperComponent, GCalWidgetWrapperComponent, + BigBlueButtonConfigComponent, RiotHomeComponent, IboxComponent, ConfigScreenWidgetComponent, @@ -234,6 +239,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo AdminStickersApiService, MediaService, StickerApiService, + BigBlueButtonApiService, AdminTelegramApiService, TelegramApiService, AdminWebhooksApiService, diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 1086ea8..8bbd11c 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -2,6 +2,8 @@ import { RouterModule, Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; import { RiotComponent } from "./riot/riot.component"; import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component"; +import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component"; +import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component"; import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component"; import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component"; @@ -180,6 +182,11 @@ const routes: Routes = [ component: CustomWidgetConfigComponent, data: {breadcrumb: "Custom Widgets", name: "Custom Widgets"}, }, + { + path: "bigbluebutton", + component: BigBlueButtonConfigComponent, + data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"}, + }, { path: "etherpad", component: EtherpadWidgetConfigComponent, @@ -286,6 +293,7 @@ const routes: Routes = [ {path: "generic", component: GenericWidgetWrapperComponent}, {path: "video", component: VideoWidgetWrapperComponent}, {path: "jitsi", component: JitsiWidgetWrapperComponent}, + {path: "bigbluebutton", component: BigBlueButtonWidgetWrapperComponent}, {path: "gcal", component: GCalWidgetWrapperComponent}, {path: "stickerpicker", component: StickerPickerWidgetWrapperComponent}, {path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent}, diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html new file mode 100644 index 0000000..a0ab995 --- /dev/null +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html @@ -0,0 +1,11 @@ + + + + + diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.scss b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts new file mode 100644 index 0000000..ac92217 --- /dev/null +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts @@ -0,0 +1,53 @@ +import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component"; +import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget"; +import { Component } from "@angular/core"; +import { FE_BigBlueButtonWidget } from "../../../shared/models/integration"; +import { SessionStorage } from "../../../shared/SessionStorage"; +import * as url from "url"; + +@Component({ + templateUrl: "bigbluebutton.widget.component.html", + styleUrls: ["bigbluebutton.widget.component.scss"], +}) + +// Configuration of BigBlueButton widgets +export class BigBlueButtonConfigComponent extends WidgetComponent { + private bigBlueButtonWidget: FE_BigBlueButtonWidget = SessionStorage.editIntegration; + + constructor() { + super(WIDGET_BIGBLUEBUTTON, "BigBlueButton Conference", DISABLE_AUTOMATIC_WRAPPING); + } + + protected OnWidgetsDiscovered(widgets: EditableWidget[]) { + for (const widget of widgets) { + widget.data.conferenceUrl = this.templateUrl(widget.url, widget.data); + } + } + + protected OnNewWidgetPrepared(widget: EditableWidget): void { + widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl; + } + + protected OnWidgetBeforeAdd(widget: EditableWidget) { + this.setWidgetOptions(widget); + } + + protected OnWidgetBeforeEdit(widget: EditableWidget) { + this.setWidgetOptions(widget); + } + + private setWidgetOptions(widget: EditableWidget) { + widget.dimension.newData.url = widget.dimension.newData.conferenceUrl; + + let widgetQueryString = url.format({ + query: { + "conferenceUrl": "$conferenceUrl", + "displayName": "$matrix_display_name", + "avatarUrl": "$matrix_avatar_url", + "userId": "$matrix_user_id", + }, + }); + widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k)); + widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString; + } +} diff --git a/web/app/home/home.component.html b/web/app/home/home.component.html index 20c63ea..72a58f1 100644 --- a/web/app/home/home.component.html +++ b/web/app/home/home.component.html @@ -69,6 +69,10 @@ Google Calendar +
+ + BigBlueButton +
Custom Widget diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index a841e4d..6d8a444 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -64,6 +64,11 @@ export interface FE_Sticker { }; } +export interface FE_BigBlueButtonJoin { + // The meeting URL the client should load to join the meeting + url: string; +} + export interface FE_StickerConfig { enabled: boolean; stickerBot: string; @@ -88,8 +93,14 @@ export interface FE_JitsiWidget extends FE_Widget { }; } +export interface FE_BigBlueButtonWidget extends FE_Widget { + options: { + conferenceUrl: string; + }; +} + export interface FE_IntegrationRequirement { condition: "publicRoom" | "canSendEventTypes" | "userInRoom"; argument: any; expectedValue: any; -} \ No newline at end of file +} diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 37d853a..795189b 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -1,6 +1,7 @@ import { WidgetsResponse } from "./server-client-responses"; export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"]; +export const WIDGET_BIGBLUEBUTTON = ["bigbluebutton", "dimension-bigbluebutton"]; export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"]; export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"]; export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"]; diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 3d902b9..673836b 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { WIDGET_CUSTOM, + WIDGET_BIGBLUEBUTTON, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, @@ -35,6 +36,9 @@ export class IntegrationsRegistry { "custom": { types: WIDGET_CUSTOM, }, + "bigbluebutton": { + types: WIDGET_BIGBLUEBUTTON, + }, "youtube": { types: WIDGET_YOUTUBE }, diff --git a/web/app/shared/services/integrations/bigbluebutton-api.service.ts b/web/app/shared/services/integrations/bigbluebutton-api.service.ts new file mode 100644 index 0000000..4a23cfe --- /dev/null +++ b/web/app/shared/services/integrations/bigbluebutton-api.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { FE_BigBlueButtonJoin } from "../../models/integration" +import { HttpClient } from "@angular/common/http"; +import { ApiError } from "../../../../../src/api/ApiError"; + +@Injectable() +export class BigBlueButtonApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public joinMeeting(url: string, name: string): Promise { + return this.authedGet("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise(); + } +} \ No newline at end of file diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html new file mode 100644 index 0000000..c42759f --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html @@ -0,0 +1,26 @@ + + +
+
+
+

+
+ +
+

BigBlueButton Conference

+ +
+
+
+
diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss new file mode 100644 index 0000000..1918e2a --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss @@ -0,0 +1,32 @@ +// component styles are encapsulated and only applied to their components +@import "../../../style/themes/themes"; + +@include themifyComponent() { + #bigBlueButtonContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .join-conference-wrapper { + display: table; + position: absolute; + height: 100%; + width: 100%; + background-color: themed(widgetWelcomeBgColor); + } + + .join-conference-boat { + display: table-cell; + vertical-align: middle; + } + + .join-conference-prompt { + margin-left: auto; + margin-right: auto; + width: 90%; + text-align: center; + } +} diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts new file mode 100644 index 0000000..a423e99 --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts @@ -0,0 +1,152 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { WidgetApiService } from "../../shared/services/integrations/widget-api.service"; +import { Subscription } from "rxjs/Subscription"; +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"; + +@Component({ + selector: "my-bigbluebutton-widget-wrapper", + templateUrl: "bigbluebutton.component.html", + styleUrls: ["bigbluebutton.component.scss"], +}) +export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy { + + public canEmbed = true; + + /** + * User metadata passed to us by the client + */ + private conferenceUrl: string; + private displayName: string; + private userId: string; + + /** + * The poll period in ms while waiting for a meeting to start + */ + private pollIntervalMillis = 5000; + + /** + * Subscriber for messages from the client via the postMessage API + */ + private bigBlueButtonApiSubscription: Subscription; + + /** + * A status message to display to the user in the widget, typically for loading messages + */ + public statusMessage: string; + + /** + * Whether we are currently in a meeting + */ + private inMeeting: boolean = false; + + /** + * The URL to embed into the iframe + */ + public embedUrl: SafeUrl = null; + + constructor(activatedRoute: ActivatedRoute, + private bigBlueButtonApi: BigBlueButtonApiService, + private widgetApi: WidgetApiService, + private sanitizer: DomSanitizer) { + super(); + this.supportsAlwaysOnScreen = true; + + let params: any = activatedRoute.snapshot.queryParams; + + console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl); + + this.conferenceUrl = params.conferenceUrl; + this.displayName = params.displayName; + this.userId = params.userId || params.email; // Element uses `email` when placing a conference call + + // Set the widget ID if we have it + ScalarWidgetApi.widgetId = params.widgetId; + } + + public ngOnInit() { + super.ngOnInit(); + } + + public onIframeLoad() { + if (this.inMeeting) { + // The meeting has ended and we've come back full circle + this.inMeeting = false; + this.statusMessage = null; + this.embedUrl = null; + + ScalarWidgetApi.sendSetAlwaysOnScreen(false); + return; + } + + // Have a toggle for whether we're in a meeting. We do this as we don't have a method + // of checking which URL was just loaded in the iframe (due to different origin domains + // and browser security), so we have to guess that it'll always be the second load (the + // first being joining the meeting) + this.inMeeting = true; + + // We've successfully joined the meeting + ScalarWidgetApi.sendSetAlwaysOnScreen(true); + } + + public joinConference(updateStatusMessage: boolean = true) { + if (updateStatusMessage) { + // Inform the user that we're loading their meeting + this.statusMessage = "Joining conference..."; + } + + // Generate a nick to display in the meeting + const joinName = `${this.displayName} (${this.userId})`; + + // Make a request to Dimension requesting the join URL + console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl); + this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).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"; + } + + const joinUrl = (response as FE_BigBlueButtonJoin).url; + + // Check if the given URL is embeddable + this.widgetApi.isEmbeddable(joinUrl).then(result => { + this.canEmbed = result.canEmbed; + this.statusMessage = null; + + // Embed the return meeting URL, joining the meeting + this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl); + + // Inform the client that we would like the meeting to remain visible for its duration + ScalarWidgetApi.sendSetAlwaysOnScreen(true); + }).catch(err => { + console.error(err); + this.canEmbed = false; + this.statusMessage = "Unable to embed meeting"; + }); + }); + } + + public ngOnDestroy() { + if (this.bigBlueButtonApiSubscription) this.bigBlueButtonApiSubscription.unsubscribe(); + } + + protected onCapabilitiesSent(): void { + super.onCapabilitiesSent(); + ScalarWidgetApi.sendSetAlwaysOnScreen(false); + } + +} diff --git a/web/app/widget-wrappers/jitsi/jitsi.component.scss b/web/app/widget-wrappers/jitsi/jitsi.component.scss index 553cb6c..38484f6 100644 --- a/web/app/widget-wrappers/jitsi/jitsi.component.scss +++ b/web/app/widget-wrappers/jitsi/jitsi.component.scss @@ -16,7 +16,7 @@ position: absolute; height: 100%; width: 100%; - background-color: themed(jitsiWelcomeBgColor); + background-color: themed(widgetWelcomeBgColor); } .join-conference-boat { @@ -30,4 +30,4 @@ width: 90%; text-align: center; } -} \ No newline at end of file +} diff --git a/web/public/img/avatars/bigbluebutton.png b/web/public/img/avatars/bigbluebutton.png new file mode 100644 index 0000000..43f8aa5 Binary files /dev/null and b/web/public/img/avatars/bigbluebutton.png differ diff --git a/web/style/themes/dark.scss b/web/style/themes/dark.scss index cf6abfa..d360e43 100644 --- a/web/style/themes/dark.scss +++ b/web/style/themes/dark.scss @@ -48,7 +48,7 @@ $theme_dark: ( stickerPickerStickerBgColor: #fff, stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2), - jitsiWelcomeBgColor: #fff, + widgetWelcomeBgColor: #fff, troubleshooterBgColor: #2d2d2d, troubleshooterNeutralColor: rgb(205, 215, 222), diff --git a/web/style/themes/light.scss b/web/style/themes/light.scss index 9c54a6f..c1e6a0b 100644 --- a/web/style/themes/light.scss +++ b/web/style/themes/light.scss @@ -48,7 +48,7 @@ $theme_light: ( stickerPickerStickerBgColor: #fff, stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2), - jitsiWelcomeBgColor: #fff, + widgetWelcomeBgColor: #fff, troubleshooterBgColor: #fff, troubleshooterNeutralColor: rgb(205, 215, 222), @@ -86,4 +86,4 @@ $theme_light: ( appserviceConfigPreFgColor: rgb(41, 43, 44), appserviceConfigPreBorderColor: #ccc, appserviceConfigPreBgColor: #eee, -); \ No newline at end of file +);