From a72177b53045ca5e43426ecad02c8ac979b10643 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 23 Dec 2017 14:16:22 -0700 Subject: [PATCH] Add jitsi widgets --- .../dimension/DimensionIntegrationsService.ts | 18 ++++ web/app/app.module.ts | 2 + web/app/app.routing.ts | 6 ++ .../widget/jitsi/jitsi.widget.component.html | 11 +++ .../widget/jitsi/jitsi.widget.component.scss | 0 .../widget/jitsi/jitsi.widget.component.ts | 89 +++++++++++++++++++ web/app/configs/widget/widget.component.ts | 25 +++++- .../shared/services/dimension-api.service.ts | 2 +- 8 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 web/app/configs/widget/jitsi/jitsi.widget.component.html create mode 100644 web/app/configs/widget/jitsi/jitsi.widget.component.scss create mode 100644 web/app/configs/widget/jitsi/jitsi.widget.component.ts diff --git a/src-ts/api/dimension/DimensionIntegrationsService.ts b/src-ts/api/dimension/DimensionIntegrationsService.ts index f37200b..1ca5b57 100644 --- a/src-ts/api/dimension/DimensionIntegrationsService.ts +++ b/src-ts/api/dimension/DimensionIntegrationsService.ts @@ -5,6 +5,8 @@ import { DimensionStore } from "../../db/DimensionStore"; import { DimensionAdminService } from "./DimensionAdminService"; import { Widget } from "../../integrations/Widget"; import { MemoryCache } from "../../MemoryCache"; +import { Integration } from "../../integrations/Integration"; +import { ApiError } from "../ApiError"; interface IntegrationsResponse { widgets: Widget[], @@ -42,6 +44,22 @@ export class DimensionIntegrationsService { return this.getEnabledIntegrations(scalarToken); } + @GET + @Path(":category/:type") + public getIntegration(@PathParam("category") category: string, @PathParam("type") type: string): Promise { + return this.getIntegrations(true).then(response => { + for (const key of Object.keys(response)) { + for (const integration of response[key]) { + if (integration.category === category && integration.type === type) { + return integration; + } + } + } + + throw new ApiError(404, "Integration not found"); + }); + } + private getIntegrations(isEnabledCheck?: boolean): Promise { const cachedResponse = DimensionIntegrationsService.integrationCache.get("integrations_" + isEnabledCheck); if (cachedResponse) { diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 67f9ee8..7b35226 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -37,6 +37,7 @@ import { EtherpadWidgetConfigComponent } from "./configs/widget/etherpad/etherpa import { NameService } from "./shared/services/name.service"; import { GoogleCalendarWidgetConfigComponent } from "./configs/widget/google_calendar/gcal.widget.component"; import { GoogleDocsWidgetConfigComponent } from "./configs/widget/google_docs/gdoc.widget.component"; +import { JitsiWidgetConfigComponent } from "./configs/widget/jitsi/jitsi.widget.component"; @NgModule({ imports: [ @@ -73,6 +74,7 @@ import { GoogleDocsWidgetConfigComponent } from "./configs/widget/google_docs/gd EtherpadWidgetConfigComponent, GoogleCalendarWidgetConfigComponent, GoogleDocsWidgetConfigComponent, + JitsiWidgetConfigComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 660be2b..8ff83fb 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -10,6 +10,7 @@ import { CustomWidgetConfigComponent } from "./configs/widget/custom/custom.widg import { EtherpadWidgetConfigComponent } from "./configs/widget/etherpad/etherpad.widget.component"; import { GoogleCalendarWidgetConfigComponent } from "./configs/widget/google_calendar/gcal.widget.component"; import { GoogleDocsWidgetConfigComponent } from "./configs/widget/google_docs/gdoc.widget.component"; +import { JitsiWidgetConfigComponent } from "./configs/widget/jitsi/jitsi.widget.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -46,6 +47,11 @@ const routes: Routes = [ component: GoogleDocsWidgetConfigComponent, data: {breadcrumb: "Google Doc Widgets", name: "Google Doc Widgets"} }, + { + path: "jitsi", + component: JitsiWidgetConfigComponent, + data: {breadcrumb: "Jitsi Widgets", name: "Jitsi Widgets"} + }, ], }, ], diff --git a/web/app/configs/widget/jitsi/jitsi.widget.component.html b/web/app/configs/widget/jitsi/jitsi.widget.component.html new file mode 100644 index 0000000..81675bb --- /dev/null +++ b/web/app/configs/widget/jitsi/jitsi.widget.component.html @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/web/app/configs/widget/jitsi/jitsi.widget.component.scss b/web/app/configs/widget/jitsi/jitsi.widget.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/widget/jitsi/jitsi.widget.component.ts b/web/app/configs/widget/jitsi/jitsi.widget.component.ts new file mode 100644 index 0000000..56966ef --- /dev/null +++ b/web/app/configs/widget/jitsi/jitsi.widget.component.ts @@ -0,0 +1,89 @@ +import { DISABLE_AUTOMATIC_WRAPPING, WidgetComponent } from "../widget.component"; +import { EditableWidget, WIDGET_JITSI } from "../../../shared/models/widget"; +import { Component } from "@angular/core"; +import { JitsiWidget } from "../../../shared/models/integration"; +import { SessionStorage } from "../../../shared/SessionStorage"; +import { NameService } from "../../../shared/services/name.service"; +import * as url from "url"; + +@Component({ + templateUrl: "jitsi.widget.component.html", + styleUrls: ["jitsi.widget.component.scss"], +}) +export class JitsiWidgetConfigComponent extends WidgetComponent { + + private jitsiWidget: JitsiWidget = SessionStorage.editIntegration; + + constructor(private nameService: NameService) { + super(WIDGET_JITSI, "Jitsi Video Conference", DISABLE_AUTOMATIC_WRAPPING); + } + + protected OnWidgetsDiscovered(widgets: EditableWidget[]) { + for (const widget of widgets) { + const templatedUrl = this.templateUrl(widget.url, widget.data); + const parsedUrl = url.parse(templatedUrl, true); + const conferenceId = parsedUrl.query["conferenceId"]; + const confId = parsedUrl.query["confId"]; + const domain = parsedUrl.query["domain"]; + let isAudioConf = parsedUrl.query["isAudioConf"]; + + // Convert isAudioConf to boolean + if (isAudioConf === "true") isAudioConf = true; + else if (isAudioConf === "false") isAudioConf = false; + else if (isAudioConf && isAudioConf[0] === '$') isAudioConf = widget.data[isAudioConf]; + else isAudioConf = false; // default + + if (conferenceId) { + // It's a legacy Dimension widget + widget.data.confId = conferenceId; + } else widget.data.confId = confId; + + if (domain) widget.data.domain = domain; + else widget.data.domain = "jitsi.riot.im"; + + widget.data.isAudioConf = isAudioConf; + widget.data.conferenceUrl = "https://" + widget.data.domain + "/" + widget.data.confId; + } + } + + protected OnNewWidgetPrepared(widget: EditableWidget): void { + const name = this.nameService.getHumanReadableName(); + + let rootUrl = "https://jitsi.riot.im/"; + if (this.jitsiWidget.options && this.jitsiWidget.options.jitsiDomain) { + rootUrl = "https://" + this.jitsiWidget.options.jitsiDomain + "/"; + } + + widget.dimension.newData["conferenceUrl"] = rootUrl + name; + } + + protected OnWidgetBeforeAdd(widget: EditableWidget) { + this.setJitsiUrl(widget); + } + + protected OnWidgetBeforeEdit(widget: EditableWidget) { + this.setJitsiUrl(widget); + } + + private setJitsiUrl(widget: EditableWidget) { + const jitsiUrl = url.parse(widget.dimension.newData.conferenceUrl); + widget.dimension.newData.domain = jitsiUrl.host; + widget.dimension.newData.confId = jitsiUrl.path.substring(1); + widget.dimension.newData.isAudioConf = false; + + let widgetQueryString = url.format({ + query: { + // TODO: Use templating when mobile riot supports it + "confId": widget.dimension.newData.confId, + "domain": widget.dimension.newData.domain, + "isAudioConf": widget.dimension.newData.isAudioConf, + "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/jitsi" + widgetQueryString; + } +} \ No newline at end of file diff --git a/web/app/configs/widget/widget.component.ts b/web/app/configs/widget/widget.component.ts index 191a7e0..1cce8f1 100644 --- a/web/app/configs/widget/widget.component.ts +++ b/web/app/configs/widget/widget.component.ts @@ -219,6 +219,29 @@ export class WidgetComponent implements OnInit { return encodedURL; } + /** + * Performs the templating calculation on a URL as best as it can. Variables that cannot be converted + * will be left unchanged, such as the $matrix_display_name and $matrix_avatar_url. This is intended to + * be used for Scalar compatibility operations. + * @param {string} urlTemplate The URL with variables to template + * @param {*} data The data to consider while templating + * @returns {string} The URL with the variables replaced + */ + protected templateUrl(urlTemplate: string, data: any): string { + let result = urlTemplate; + + result = result.replace("$matrix_room_id", SessionStorage.roomId); + result = result.replace("$matrix_user_id", SessionStorage.userId); + // result = result.replace("$matrix_display_name", "NOT SUPPORTED"); + // result = result.replace("$matrix_avatar_url", "NOT SUPPORTED"); + + for (const key of Object.keys(data)) { + result = result.replace("$" + key, data[key]); + } + + return result; + } + /** * Adds the widget stored in newWidget to the room. * @returns {Promise<*>} Resolves when the widget has been added and newWidget is populated @@ -264,7 +287,7 @@ export class WidgetComponent implements OnInit { public saveWidget(widget: EditableWidget): Promise { // Make sure we call "before add" before validating the URL try { - this.OnWidgetBeforeEdit(this.newWidget); + this.OnWidgetBeforeEdit(widget); } catch (error) { this.toaster.pop("warning", error.message); return; diff --git a/web/app/shared/services/dimension-api.service.ts b/web/app/shared/services/dimension-api.service.ts index 6cc2da7..4c2174a 100644 --- a/web/app/shared/services/dimension-api.service.ts +++ b/web/app/shared/services/dimension-api.service.ts @@ -15,7 +15,7 @@ export class DimensionApiService extends AuthedApi { } public getWidget(type: string): Promise { - return this.http.get("/api/v1/dimension/widget/" + type).map(r => r.json()).toPromise(); + return this.http.get("/api/v1/dimension/integrations/widget/" + type).map(r => r.json()).toPromise(); } public isEmbeddable(url: string): Promise { // 200 = success, anything else = error