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 @@
+
+
+
+
+
+
\ 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