Add Jitsi screensharing support

Self-hosted Jitsi Meet servers are possible for those running Dimension. The default server is Riot.IM's server.

Adds #87
This commit is contained in:
Travis Ralston 2017-12-10 00:59:36 -07:00
parent 99bc1eb660
commit 993dcdac21
22 changed files with 442 additions and 55 deletions

View File

@ -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"

75
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 => {

View File

@ -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 {

View File

@ -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);

View File

@ -0,0 +1,60 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img src="/img/avatars/jitsi.png">
<h4>Configure Jitsi Conferences</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<span class="input-group-addon">https://{{ integration.jitsiDomain }}/</span>
<input type="text" class="form-control"
placeholder="MyConferenceName"
[(ngModel)]="newWidgetName" name="newWidgetName"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.data.dimOriginalConferenceUrl }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove
</button>
<div *ngIf="isWidgetToggled(widget)">
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
Conference URL
<input type="text" class="form-control"
placeholder="https://jitsi.riot.im/MyConference"
[(ngModel)]="widget.data.dimConferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,4 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -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<ConfigModalContext> {
public integration: JitsiWidgetIntegration;
constructor(public dialog: DialogRef<ConfigModalContext>,
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 = <JitsiWidgetIntegration>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"]);
}
}

View File

@ -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);
}

View File

@ -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<Widget[]> {
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);

View File

@ -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);
}

View File

@ -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<Integration> {
const url = "/api/v1/dimension/integration/" + type + "/" + integrationType;
return this.http.get(url).map(res => res.json()).toPromise();
}
}

View File

@ -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,
},
};

View File

@ -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
}

View File

@ -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;

View File

@ -0,0 +1,13 @@
<div id="jitsiContainer">
</div>
<div class="join-conference-wrapper">
<div class="join-conference-boat">
<div *ngIf="!isJoined" class="join-conference-prompt">
<h3>Jitsi Video Conference</h3>
<button type="button" (click)="joinConference()" class="btn btn-primary btn-large">
Join Conference
</button>
</div>
</div>
</div>

View File

@ -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;
}

View File

@ -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 = <JitsiWidgetIntegration>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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -5,4 +5,4 @@ import "@angular/common";
import "@angular/http";
import "@angular/router";
import "rxjs";
import "@angularclass/hmr";
import "@angularclass/hmr";