diff --git a/config/integrations/jitsi_widget.yaml b/config/integrations/jitsi_widget.yaml new file mode 100644 index 0000000..568c2b7 --- /dev/null +++ b/config/integrations/jitsi_widget.yaml @@ -0,0 +1,15 @@ +# All this configuration does is make "Jitsi Widget" available in the UI +type: "widget" +integrationType: "jitsi" +enabled: true +name: "Jitsi" +about: "Add video conferencing to your room with Jitsi" +avatar: "img/avatars/jitsi.png" + +# This is the domain that will be used to construct the Jitsi widget. It must be just the domain. +# The default is meet.jit.si The Riot.IM instance is at jitsi.riot.im +jitsiDomain: "jitsi.riot.im" + +# This is the path to the external API script. Usually the domain can be replaced with your custom +# domain above without any other modifications. +scriptUrl: "https://jitsi.riot.im/libs/external_api.min.js" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 75dddf8..9ec711f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,81 +5,81 @@ "requires": true, "dependencies": { "@angular/animations": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-4.4.6.tgz", - "integrity": "sha1-+mYYmaik44y3xYPHpcl85l1ZKjU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.1.0.tgz", + "integrity": "sha512-s0tV6y2D16CQAcXjv8CN8AahHb+LoWm9KAUkxvSJ18ZZQweuAY4T8jlRB95ODRFFKfjwyRD9HqXKUC5yHmG9ww==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/common": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-4.4.6.tgz", - "integrity": "sha1-S4FCByTggooOg5uVpV6xp+g5GPI=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.1.0.tgz", + "integrity": "sha512-J6E0OfTJJGcyoKU51ZucsDFV40YEAPgP6VCIPYECgOFHxrqg6O1ZFZSD1fdviMXMLVEFCi6Fy6IB7GJyiWgDIA==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/compiler": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.4.6.tgz", - "integrity": "sha1-LuH68lt1fh0SiXkHS+f65SmzvCA=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.1.0.tgz", + "integrity": "sha512-iyFQqmhKNRSc9JRx7ty6z/wCsypjpbRu0QR6q2LMa6imuCt9qLHOvTajBQExRB8guqd/LTVDG4WiYY1lf8iO2w==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/core": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-4.4.6.tgz", - "integrity": "sha1-EwMf0Q3P5DiHVBmzjyESCVi8I1Q=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.1.0.tgz", + "integrity": "sha512-duObjve+INoz4wWuqcaJzl1isUyI37RtRblTFXgZBp2n2n0nXJq1CubcfgxQhMMR2d64xWLKg9+d34PvnzaMmg==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/forms": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-4.4.6.tgz", - "integrity": "sha1-/mSs5CQ1wbgPSQNLfEHOjK8UpEo=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.1.0.tgz", + "integrity": "sha512-2sJqtMht/6vbFg6HwFs0MX4pRhgLt7h2pa6oTH4oBoQ2UF67jCuq4cMljDm9SVxrGw0Q83+/eBk3ER4QnKk48Q==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/http": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-4.4.6.tgz", - "integrity": "sha1-CvaAxnEL3AJtlA4iXP0PalwAXQw=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.1.0.tgz", + "integrity": "sha512-ltSs52OYnWZJEnbxtHoN5LQiH/37F3GxN6iL0TsQdSlw8HzrdcdbmebKlCpfXwhgcgZC48KWbKSaOs5/xVurfQ==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/platform-browser": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-4.4.6.tgz", - "integrity": "sha1-qYOcVH4bZU+h0kqJeAyLpquNzOA=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.1.0.tgz", + "integrity": "sha512-8aeppeASwQv4Fj3B8KBiFHQrKPrwA328AEhlH/HnggCvt0CFffIs2PSqzJBwnOfFWvhFZk020W51B8jrHLQyoQ==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/platform-browser-dynamic": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.4.6.tgz", - "integrity": "sha1-TT2aanvyzz3kBYphWuBZ7/ZB+jY=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.1.0.tgz", + "integrity": "sha512-f6Iv4NCYQwBkNeyInZzja8pg0nfUOrxx5H5rEvr0J1bwag2eDofGVPOftha7LDOLVALVOQQiXQBePATMNLB85g==", "dev": true, "requires": { "tslib": "1.8.1" } }, "@angular/router": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-4.4.6.tgz", - "integrity": "sha1-D2rSmuD/jSyeo3m9MgRHIXt+yGY=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.1.0.tgz", + "integrity": "sha512-CtOwqeo1IUk4kUs+tUggkYFmuu2fPTZ1G/GP7YK6gd3Jr9OtkMFB7wkmnd5YcaYo3wVeYkJWZdJQAvj6OakMww==", "dev": true, "requires": { "tslib": "1.8.1" @@ -111,6 +111,12 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, + "@types/jquery": { + "version": "3.2.16", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.2.16.tgz", + "integrity": "sha512-q2WC02YxQoX2nY1HRKlYGHpGP1saPmD7GN0pwCDlTz35a4eOtJG+aHRlXyjCuXokUukSrR2aXyBhSW3j+jPc0A==", + "dev": true + }, "@types/node": { "version": "6.0.92", "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.92.tgz", @@ -2923,6 +2929,15 @@ "minimatch": "3.0.4" } }, + "goby": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/goby/-/goby-1.1.2.tgz", + "integrity": "sha1-ca6JCCSWCjhLvROhoY5/8gI/3YM=", + "dev": true, + "requires": { + "ramda": "0.19.1" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -6970,6 +6985,12 @@ "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", "dev": true }, + "ramda": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.19.1.tgz", + "integrity": "sha1-icStaXJl/2sfrOnyhkOeJSDWZ5w=", + "dev": true + }, "random-string": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/random-string/-/random-string-0.2.0.tgz", diff --git a/package.json b/package.json index 9889e7f..d2479eb 100644 --- a/package.json +++ b/package.json @@ -42,18 +42,19 @@ "winston": "^2.4.0" }, "devDependencies": { - "@angular/animations": "^4.4.6", - "@angular/common": "^4.4.6", - "@angular/compiler": "^4.4.6", - "@angular/core": "^4.4.6", - "@angular/forms": "^4.4.6", - "@angular/http": "^4.4.6", - "@angular/platform-browser": "^4.4.6", - "@angular/platform-browser-dynamic": "^4.4.6", - "@angular/router": "^4.4.6", + "@angular/animations": "^5.0.0", + "@angular/common": "^5.0.0", + "@angular/compiler": "^5.0.0", + "@angular/core": "^5.0.0", + "@angular/forms": "^5.0.0", + "@angular/http": "^5.0.0", + "@angular/platform-browser": "^5.0.0", + "@angular/platform-browser-dynamic": "^5.0.0", + "@angular/router": "^5.0.0", "@angularclass/hmr": "^2.1.0", "@angularclass/hmr-loader": "^3.0.2", "@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.7", + "@types/jquery": "^3.2.16", "@types/node": "^6.0.92", "angular2-template-loader": "^0.6.2", "angular2-toaster": "^4.0.0", @@ -66,6 +67,7 @@ "cssnano": "^3.10.0", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.5", + "goby": "^1.1.2", "html-loader": "^0.5.1", "html-webpack-plugin": "^2.28.0", "jquery": "^3.2.1", diff --git a/src/DimensionApi.js b/src/DimensionApi.js index 8d90fce..c8b4728 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -32,6 +32,7 @@ class DimensionApi { app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this)); app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this)); app.get("/api/v1/dimension/widgets/embeddable", this._checkEmbeddable.bind(this)); + app.get("/api/v1/dimension/integration/:type/:integrationType", this._getIntegration.bind(this)); } _checkEmbeddable(req, res) { @@ -101,7 +102,7 @@ class DimensionApi { }); } - _getIntegration(integrationConfig, roomId, scalarToken) { + _findIntegration(integrationConfig, roomId, scalarToken) { var factory = IntegrationImpl.getFactory(integrationConfig); if (!factory) throw new Error("Missing config factory for " + integrationConfig.name); @@ -112,6 +113,27 @@ class DimensionApi { } } + _getIntegration(req, res) {res.setHeader("Content-Type", "application/json"); + // Unauthed endpoint. + + var type = req.params.type; + var integrationType = req.params.integrationType; + + if (!type || !integrationType) { + res.status(400).send({error: "Missing integration type or type"}); + return; + } + + var byIntegrationType = Integrations.byType[type]; + if (!byIntegrationType || !byIntegrationType[integrationType]) { + res.status(400).send({error: "Unknown integration"}); + return; + } + var integrationConfig = byIntegrationType[integrationType]; + + res.status(200).send(integrationConfig); + } + _getIntegrations(req, res) { res.setHeader("Content-Type", "application/json"); @@ -129,7 +151,7 @@ class DimensionApi { var remove = []; _.forEach(integrations, integration => { try { - promises.push(this._getIntegration(integration, roomId, scalarToken).then(builtIntegration => { + promises.push(this._findIntegration(integration, roomId, scalarToken).then(builtIntegration => { return builtIntegration.getState().then(state => { var keys = _.keys(state); for (var key of keys) { @@ -187,7 +209,7 @@ class DimensionApi { log.info("DimensionApi", "Remove requested for " + type + " (" + integrationType + ") in room " + roomId); this._db.checkToken(scalarToken).then(() => { - return this._getIntegration(integrationConfig, roomId, scalarToken); + return this._findIntegration(integrationConfig, roomId, scalarToken); }).then(integration => integration.removeFromRoom(roomId)).then(() => { res.status(200).send({success: true}); }).catch(err => { @@ -217,7 +239,7 @@ class DimensionApi { log.info("DimensionApi", "Update state requested for " + type + " (" + integrationType + ") in room " + roomId); this._db.checkToken(scalarToken).then(() => { - return this._getIntegration(integrationConfig, roomId, scalarToken); + return this._findIntegration(integrationConfig, roomId, scalarToken); }).then(integration => { return integration.updateState(req.body.state); }).then(newState => { @@ -249,7 +271,7 @@ class DimensionApi { log.info("DimensionApi", "State requested for " + type + " (" + integrationType + ") in room " + roomId); this._db.checkToken(scalarToken).then(() => { - return this._getIntegration(integrationConfig, roomId, scalarToken); + return this._findIntegration(integrationConfig, roomId, scalarToken); }).then(integration => { return integration.getState(); }).then(state => { diff --git a/web/app/app.module.ts b/web/app/app.module.ts index cf3afbe..033e0b9 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -31,6 +31,8 @@ import { YoutubeWidgetConfigComponent } from "./configs/widget/youtube/youtube-c import { TwitchWidgetConfigComponent } from "./configs/widget/twitch/twitch-config.component"; import { EtherpadWidgetConfigComponent } from "./configs/widget/etherpad/etherpad-config.component"; import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component"; +import { JitsiWidgetConfigComponent } from "./configs/widget/jitsi/jitsi-config.component"; +import { JitsiWidgetWrapperComponent } from "./widget_wrappers/jitsi/jitsi.component"; @NgModule({ imports: [ @@ -63,6 +65,8 @@ import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.compo TwitchWidgetConfigComponent, EtherpadWidgetConfigComponent, VideoWidgetWrapperComponent, + JitsiWidgetConfigComponent, + JitsiWidgetWrapperComponent, // Vendor ], @@ -84,6 +88,7 @@ import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.compo YoutubeWidgetConfigComponent, TwitchWidgetConfigComponent, EtherpadWidgetConfigComponent, + JitsiWidgetConfigComponent, ] }) export class AppModule { diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 4de7ff9..45c720c 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -3,12 +3,14 @@ import { HomeComponent } from "./home/home.component"; import { RiotComponent } from "./riot/riot.component"; import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component"; import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component"; +import { JitsiWidgetWrapperComponent } from "./widget_wrappers/jitsi/jitsi.component"; const routes: Routes = [ {path: "", component: HomeComponent}, {path: "riot", component: RiotComponent}, {path: "widgets/generic", component: GenericWidgetWrapperComponent}, {path: "widgets/video", component: VideoWidgetWrapperComponent}, + {path: "widgets/jitsi", component: JitsiWidgetWrapperComponent}, ]; export const routing = RouterModule.forRoot(routes); diff --git a/web/app/configs/widget/jitsi/jitsi-config.component.html b/web/app/configs/widget/jitsi/jitsi-config.component.html new file mode 100644 index 0000000..8040769 --- /dev/null +++ b/web/app/configs/widget/jitsi/jitsi-config.component.html @@ -0,0 +1,60 @@ +
+ +
+ +

Configure Jitsi Conferences

+
+
+
+
+

Loading widgets...

+
+
+
+
+
+
+
+
+ https://{{ integration.jitsiDomain }}/ + + + + +
+
+
+ {{ widget.data.dimOriginalConferenceUrl }} (added by {{ widget.ownerId }}) + + +
+ + + +
+
+
+
+
+
\ No newline at end of file diff --git a/web/app/configs/widget/jitsi/jitsi-config.component.scss b/web/app/configs/widget/jitsi/jitsi-config.component.scss new file mode 100644 index 0000000..92dce18 --- /dev/null +++ b/web/app/configs/widget/jitsi/jitsi-config.component.scss @@ -0,0 +1,4 @@ +// component styles are encapsulated and only applied to their components +.widget-item { + margin-top: 3px; +} diff --git a/web/app/configs/widget/jitsi/jitsi-config.component.ts b/web/app/configs/widget/jitsi/jitsi-config.component.ts new file mode 100644 index 0000000..e6092d7 --- /dev/null +++ b/web/app/configs/widget/jitsi/jitsi-config.component.ts @@ -0,0 +1,98 @@ +import { Component } from "@angular/core"; +import { DialogRef, ModalComponent } from "ngx-modialog"; +import { WidgetComponent } from "../widget.component"; +import { ScalarService } from "../../../shared/scalar.service"; +import { ConfigModalContext } from "../../../integration/integration.component"; +import { ToasterService } from "angular2-toaster"; +import { Widget, WIDGET_DIM_JITSI, WIDGET_SCALAR_JITSI } from "../../../shared/models/widget"; +import { JitsiWidgetIntegration } from "../../../shared/models/integration"; +import * as gobyInit from "goby"; +import * as url from "url"; + +const goby = gobyInit.init({ + // Converts words to a url-safe name + // Ie: "hello world how-are you" becomes "HelloWorldHowAreYou" + decorator: parts => parts.map(p => p ? p.split('-').map(p2 => p2 ? p2[0].toUpperCase() + p2.substring(1).toLowerCase() : '').join('') : '').join(''), +}); + +@Component({ + selector: "my-jitsi-config", + templateUrl: "jitsi-config.component.html", + styleUrls: ["jitsi-config.component.scss", "./../../config.component.scss"], +}) +export class JitsiWidgetConfigComponent extends WidgetComponent implements ModalComponent { + + public integration: JitsiWidgetIntegration; + + constructor(public dialog: DialogRef, + toaster: ToasterService, + scalarService: ScalarService, + window: Window) { + super( + toaster, + scalarService, + dialog.context.roomId, + window, + WIDGET_DIM_JITSI, + WIDGET_SCALAR_JITSI, + dialog.context.integrationId, + "Jitsi Video Conference", + "" // we intentionally don't specify the wrapper so we can control the behaviour + ); + + this.integration = dialog.context.integration; + this.newWidgetName = this.generateConferenceId(); + } + + protected finishParsing(widget: Widget): Widget { + const parsedUrl = url.parse(widget.url, true); + const conferenceId = parsedUrl.query["confId"]; + + if (!widget.data) widget.data = {}; + + if (conferenceId) { + // It's a scalar widget + widget.data.dimOriginalConferenceUrl = "https://jitsi.riot.im/" + conferenceId; + widget.data.dimConferenceUrl = widget.data.dimOriginalConferenceUrl; + } + + return widget; + } + + public validateAndAddWidget() { + const conferenceUrl = "https://" + this.integration.jitsiDomain + "/" + this.newWidgetName; + const conferenceId = this.newWidgetName; + const data = { + dimOriginalConferenceUrl: conferenceUrl, + dimConferenceUrl: conferenceUrl, + }; + + let widgetQueryString = url.format({ + query: { + //"scriptUrl": this.integration.scriptUrl, // handled in wrapper + "domain": this.integration.jitsiDomain, + "conferenceId": conferenceId, + "displayName": "$matrix_display_name", + "avatarUrl": "$matrix_avatar_url", + "userId": "$matrix_user_id", + }, + }); + widgetQueryString = this.unformatParams(widgetQueryString, data); + + this.newWidgetUrl = window.location.origin + "/widgets/jitsi" + widgetQueryString; + this.newWidgetName = "Jitsi Video Conference"; + this.addWidget(data); + } + + public validateAndSaveWidget(widget: Widget) { + console.log(widget); + } + + protected widgetAdded() { + this.newWidgetName = this.generateConferenceId(); + } + + private generateConferenceId() { + return goby.generate(["adj", "pre", "suf"]); + } +} diff --git a/web/app/configs/widget/twitch/twitch-config.component.ts b/web/app/configs/widget/twitch/twitch-config.component.ts index 9c38f75..56b16e8 100644 --- a/web/app/configs/widget/twitch/twitch-config.component.ts +++ b/web/app/configs/widget/twitch/twitch-config.component.ts @@ -62,7 +62,7 @@ export class TwitchWidgetConfigComponent extends WidgetComponent implements Moda this.saveWidget(widget); } - editWidget(widget: Widget) { + public editWidget(widget: Widget) { widget.data.newDimChannelName = widget.data.dimChannelName; super.editWidget(widget); } diff --git a/web/app/configs/widget/widget.component.ts b/web/app/configs/widget/widget.component.ts index 7aa3b1e..9824045 100644 --- a/web/app/configs/widget/widget.component.ts +++ b/web/app/configs/widget/widget.component.ts @@ -33,11 +33,14 @@ export class WidgetComponent { scalarWrapperId = null) { this.isLoading = true; this.isUpdating = false; - this.wrapperUrl = window.location.origin + "/widgets/" + wrapperId + "?url="; - if (!scalarWrapperId) scalarWrapperId = wrapperId; - for (let widgetLink of SCALAR_WIDGET_LINKS) { - this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", scalarWrapperId)); + if (wrapperId) { + this.wrapperUrl = window.location.origin + "/widgets/" + wrapperId + "?url="; + + if (!scalarWrapperId) scalarWrapperId = wrapperId; + for (let widgetLink of SCALAR_WIDGET_LINKS) { + this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", scalarWrapperId)); + } } this.getWidgetsOfType(primaryWidgetType, alternateWidgetType).then(widgets => { @@ -62,6 +65,15 @@ export class WidgetComponent { }); } + protected finishParsing(widget: Widget) { + // We don't actually need to do anything + return widget; + } + + protected widgetAdded() { + // Meant to be overridden + } + private getWidgetsOfType(type: string, altType: string): Promise { return this.scalarApi.getWidgets(this.roomId) .then(resp => ScalarToWidgets(resp)) @@ -73,11 +85,13 @@ export class WidgetComponent { filtered.push(widget); } - return filtered; + return filtered.map(w => this.finishParsing(w)); }); } private getWrappedUrl(url: string): string { + if (!this.wrapperUrl) return url; + const urls = [this.wrapperUrl].concat(this.scalarWrapperUrls); for (let scalarUrl of urls) { if (url.startsWith(scalarUrl)) { @@ -88,18 +102,29 @@ export class WidgetComponent { } private wrapUrl(url: string): string { + if (!this.wrapperUrl) return url; + let encodedURL = this.wrapperUrl + encodeURIComponent(url); - //don't URL encode $vars of the widget Spec - //TODO do the same with vars from the data object - encodedURL = encodedURL.replace(encodeURIComponent("$matrix_user_id"), "$matrix_user_id"); - encodedURL = encodedURL.replace(encodeURIComponent("$matrix_room_id"), "$matrix_room_id"); - encodedURL = encodedURL.replace(encodeURIComponent("$matrix_display_name"), "$matrix_display_name"); - encodedURL = encodedURL.replace(encodeURIComponent("$matrix_avatar_url"), "$matrix_avatar_url"); + // TODO: Decode data parameters + encodedURL = this.unformatParams(encodedURL); return encodedURL; } + protected unformatParams(encodedUrl: string, additionalData: any = {}):string { + encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_user_id"), "$matrix_user_id"); + encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_room_id"), "$matrix_room_id"); + encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_display_name"), "$matrix_display_name"); + encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_avatar_url"), "$matrix_avatar_url"); + + for (const key of Object.keys(additionalData)) { + encodedUrl = encodedUrl.replace(encodeURIComponent("$" + key), "$" + key); + } + + return encodedUrl; + } + private setWidgetUrl(widget: Widget) { widget.url = this.getWrappedUrl(widget.url); @@ -127,6 +152,7 @@ export class WidgetComponent { this.newWidgetUrl = ""; this.newWidgetName = ""; this.toaster.pop("success", "Widget added!"); + this.widgetAdded(); }) .catch(err => { this.toaster.pop("error", err.json().error); diff --git a/web/app/riot/riot.component.ts b/web/app/riot/riot.component.ts index 8a123ff..cac0958 100644 --- a/web/app/riot/riot.component.ts +++ b/web/app/riot/riot.component.ts @@ -6,7 +6,10 @@ import { ToasterService } from "angular2-toaster"; import { Integration } from "../shared/models/integration"; import { IntegrationService } from "../shared/integration.service"; import * as _ from "lodash"; -import { WIDGET_DIM_CUSTOM, WIDGET_DIM_ETHERPAD, WIDGET_DIM_TWITCH, WIDGET_DIM_YOUTUBE } from "../shared/models/widget"; +import { + WIDGET_DIM_CUSTOM, WIDGET_DIM_ETHERPAD, WIDGET_DIM_JITSI, WIDGET_DIM_TWITCH, + WIDGET_DIM_YOUTUBE +} from "../shared/models/widget"; import { IntegrationComponent } from "../integration/integration.component"; @Component({ @@ -82,6 +85,9 @@ export class RiotComponent { } else if (this.requestedScreen === "type_" + WIDGET_DIM_ETHERPAD) { type = "widget"; integrationType = "etherpad"; + } else if (this.requestedScreen === "type_" + WIDGET_DIM_JITSI) { + type = "widget"; + integrationType = "jitsi"; } else { console.log("Unknown screen requested: " + this.requestedScreen); } diff --git a/web/app/shared/api.service.ts b/web/app/shared/api.service.ts index ad33ca2..d78dc38 100644 --- a/web/app/shared/api.service.ts +++ b/web/app/shared/api.service.ts @@ -40,4 +40,9 @@ export class ApiService { return this.http.get(url, {params: {url: checkUrl}}) .map(res => res.json()).toPromise(); } + + getIntegration(type: string, integrationType: string): Promise { + const url = "/api/v1/dimension/integration/" + type + "/" + integrationType; + return this.http.get(url).map(res => res.json()).toPromise(); + } } diff --git a/web/app/shared/integration.service.ts b/web/app/shared/integration.service.ts index 2a429e0..0a8e77e 100644 --- a/web/app/shared/integration.service.ts +++ b/web/app/shared/integration.service.ts @@ -8,6 +8,7 @@ import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/cus import { YoutubeWidgetConfigComponent } from "../configs/widget/youtube/youtube-config.component"; import { TwitchWidgetConfigComponent } from "../configs/widget/twitch/twitch-config.component"; import { EtherpadWidgetConfigComponent } from "../configs/widget/etherpad/etherpad-config.component"; +import { JitsiWidgetConfigComponent } from "../configs/widget/jitsi/jitsi-config.component"; @Injectable() export class IntegrationService { @@ -26,6 +27,7 @@ export class IntegrationService { "youtube": true, "twitch": true, "etherpad": true, + "jitsi": true, }, }; @@ -42,6 +44,7 @@ export class IntegrationService { "youtube": YoutubeWidgetConfigComponent, "twitch": TwitchWidgetConfigComponent, "etherpad": EtherpadWidgetConfigComponent, + "jitsi": JitsiWidgetConfigComponent, }, }; diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index c1b13b3..296445a 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -31,3 +31,8 @@ export interface IRCIntegration extends Integration { export interface EtherpadWidgetIntegration extends Integration { defaultUrl: string; } + +export interface JitsiWidgetIntegration extends Integration { + jitsiDomain: string; + scriptUrl: string +} \ No newline at end of file diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index e921e0e..5c160a3 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -15,6 +15,7 @@ export const WIDGET_DIM_CUSTOM = "dimension-customwidget"; export const WIDGET_DIM_YOUTUBE = "dimension-youtube"; export const WIDGET_DIM_TWITCH = "dimension-twitch"; export const WIDGET_DIM_ETHERPAD = "dimension-etherpad"; +export const WIDGET_DIM_JITSI = "dimension-jitsi"; export interface Widget { id: string; diff --git a/web/app/widget_wrappers/jitsi/jitsi.component.html b/web/app/widget_wrappers/jitsi/jitsi.component.html new file mode 100644 index 0000000..65c586d --- /dev/null +++ b/web/app/widget_wrappers/jitsi/jitsi.component.html @@ -0,0 +1,13 @@ +
+
+ +
+
+
+

Jitsi Video Conference

+ +
+
+
\ No newline at end of file diff --git a/web/app/widget_wrappers/jitsi/jitsi.component.scss b/web/app/widget_wrappers/jitsi/jitsi.component.scss new file mode 100644 index 0000000..5cebc93 --- /dev/null +++ b/web/app/widget_wrappers/jitsi/jitsi.component.scss @@ -0,0 +1,28 @@ +// component styles are encapsulated and only applied to their components +#jitsiContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; +} + +.join-conference-wrapper { + display: table; + position: absolute; + height: 100%; + width: 100%; +} + +.join-conference-boat { + display: table-cell; + vertical-align: middle; +} + +.join-conference-prompt { + margin-left: auto; + margin-right: auto; + width: 90%; + text-align: center; +} \ No newline at end of file diff --git a/web/app/widget_wrappers/jitsi/jitsi.component.ts b/web/app/widget_wrappers/jitsi/jitsi.component.ts new file mode 100644 index 0000000..72188f4 --- /dev/null +++ b/web/app/widget_wrappers/jitsi/jitsi.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import * as $ from "jquery"; +import { ApiService } from "../../shared/api.service"; +import { JitsiWidgetIntegration } from "../../shared/models/integration"; + +declare var JitsiMeetExternalAPI: any; + +@Component({ + selector: "my-jitsi-widget-wrapper", + templateUrl: "jitsi.component.html", + styleUrls: ["jitsi.component.scss"], +}) +export class JitsiWidgetWrapperComponent implements OnInit { + + public isJoined = false; + + private domain: string; + private conferenceId: string; + private displayName: string; + private avatarUrl: string; + private userId: string; + private jitsiApiObj: any; + + constructor(activatedRoute: ActivatedRoute, private api: ApiService) { + let params: any = activatedRoute.snapshot.queryParams; + + this.domain = params.domain; + this.conferenceId = params.conferenceId; + this.displayName = params.displayName; + this.avatarUrl = params.avatarUrl; + this.userId = params.userId; + } + + public ngOnInit() { + this.api.getIntegration("widget", "jitsi").then(integration => { + const widget = integration; + $.getScript(widget.scriptUrl); + }); + } + + public joinConference() { + $(".join-conference-wrapper").hide(); + $("#jitsiContainer").show(); + + this.jitsiApiObj = new JitsiMeetExternalAPI(this.domain, { + width: "100%", + height: "100%", + parentNode: document.querySelector("#jitsiContainer"), + roomName: this.conferenceId, + interfaceConfigOverwrite: { + SHOW_JITSI_WATERMARK: false, + SHOW_WATERMARK_FOR_GUESTS: false, + MAIN_TOOLBAR_BUTTONS: [], + VIDEO_LAYOUT_FIT: "height", + } + }); + if (this.displayName) this.jitsiApiObj.executeCommand("displayName", this.displayName); + if (this.avatarUrl) this.jitsiApiObj.executeCommand("avatarUrl", this.avatarUrl.toString()); + if (this.userId) this.jitsiApiObj.executeCommand("email", this.userId); + + this.jitsiApiObj.on("readyToClose", () => { + this.isJoined = false; + $(".join-conference-wrapper").show(); + $("#jitsiContainer").hide().html(""); + }); + + this.isJoined = true; + } + +} diff --git a/web/public/img/avatars/etherpad.png b/web/public/img/avatars/etherpad.png index c420f30..84b6f43 100644 Binary files a/web/public/img/avatars/etherpad.png and b/web/public/img/avatars/etherpad.png differ diff --git a/web/public/img/avatars/jitsi.png b/web/public/img/avatars/jitsi.png new file mode 100644 index 0000000..71eeb3e Binary files /dev/null and b/web/public/img/avatars/jitsi.png differ diff --git a/web/vendor.ts b/web/vendor.ts index c6f1a08..caab141 100644 --- a/web/vendor.ts +++ b/web/vendor.ts @@ -5,4 +5,4 @@ import "@angular/common"; import "@angular/http"; import "@angular/router"; import "rxjs"; -import "@angularclass/hmr"; +import "@angularclass/hmr"; \ No newline at end of file