mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
parent
f1c12a2ea6
commit
c9571576fe
@ -22,6 +22,7 @@ import { RssConfigComponent } from "./configs/rss/rss-config.component";
|
||||
import { IrcConfigComponent } from "./configs/irc/irc-config.component";
|
||||
import { IrcApiService } from "./shared/irc-api.service";
|
||||
import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.component";
|
||||
import { CustomWidgetConfigComponent } from "./configs/widget/custom_widget/custom_widget-config.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -45,6 +46,7 @@ import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.comp
|
||||
RssConfigComponent,
|
||||
IrcConfigComponent,
|
||||
TravisCiConfigComponent,
|
||||
CustomWidgetConfigComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
@ -61,6 +63,7 @@ import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.comp
|
||||
RssConfigComponent,
|
||||
TravisCiConfigComponent,
|
||||
IrcConfigComponent,
|
||||
CustomWidgetConfigComponent,
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
|
@ -0,0 +1,65 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img src="/img/avatars/customwidget.png">
|
||||
<h4>Configure custom widgets</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)="addWidget()" novalidate name="addForm">
|
||||
<div class="row">
|
||||
<div class="col-md-8" style="margin-bottom: 12px;">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Custom widget URL"
|
||||
[(ngModel)]="widgetUrl" name="widgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</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 Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label>
|
||||
Widget Name
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Custom Widget"
|
||||
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<label>
|
||||
Widget URL
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Custom widget URL"
|
||||
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
// component styles are encapsulated and only applied to their components
|
@ -0,0 +1,113 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ModalComponent, DialogRef } 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_CUSTOM, WIDGET_SCALAR_CUSTOM } from "../../../shared/models/widget";
|
||||
|
||||
// TODO: A lot of this can probably be abstracted out for other widgets (even the UI), possibly even for other integrations
|
||||
|
||||
@Component({
|
||||
selector: "my-customwidget-config",
|
||||
templateUrl: "custom_widget-config.component.html",
|
||||
styleUrls: ["custom_widget-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class CustomWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public isLoading = true;
|
||||
public isUpdating = false;
|
||||
public widgets: Widget[];
|
||||
public widgetUrl = "";
|
||||
|
||||
private toggledWidgets: string[] = [];
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private toaster: ToasterService,
|
||||
scalarService: ScalarService) {
|
||||
super(scalarService, dialog.context.roomId);
|
||||
|
||||
this.getWidgetsOfType(WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM).then(widgets => {
|
||||
this.widgets = widgets;
|
||||
this.isLoading = false;
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
public addWidget() {
|
||||
let constructedWidget: Widget = {
|
||||
id: "dimension-" + (new Date().getTime()),
|
||||
url: this.widgetUrl,
|
||||
type: WIDGET_DIM_CUSTOM,
|
||||
name: "Custom Widget",
|
||||
};
|
||||
|
||||
this.isUpdating = true;
|
||||
this.scalarApi.setWidget(this.roomId, constructedWidget)
|
||||
.then(() => this.widgets.push(constructedWidget))
|
||||
.then(() => {
|
||||
this.isUpdating = false;
|
||||
this.widgetUrl = "";
|
||||
this.toaster.pop("success", "Widget added!");
|
||||
})
|
||||
.catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
public saveWidget(widget: Widget) {
|
||||
if (widget.newUrl.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a URL for the widget");
|
||||
return;
|
||||
}
|
||||
|
||||
widget.name = widget.newName || "Custom Widget";
|
||||
widget.url = widget.newUrl;
|
||||
|
||||
this.isUpdating = true;
|
||||
this.scalarApi.setWidget(this.roomId, widget)
|
||||
.then(() => this.toggleWidget(widget))
|
||||
.then(() => {
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Widget updated!");
|
||||
})
|
||||
.catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
public removeWidget(widget: Widget) {
|
||||
this.isUpdating = true;
|
||||
this.scalarApi.deleteWidget(this.roomId, widget)
|
||||
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
|
||||
.then(() => {
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Widget deleted!");
|
||||
})
|
||||
.catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
public editWidget(widget: Widget) {
|
||||
widget.newName = widget.name || "Custom Widget";
|
||||
widget.newUrl = widget.url;
|
||||
this.toggleWidget(widget);
|
||||
}
|
||||
|
||||
public toggleWidget(widget: Widget) {
|
||||
let idx = this.toggledWidgets.indexOf(widget.id);
|
||||
if (idx === -1) this.toggledWidgets.push(widget.id);
|
||||
else this.toggledWidgets.splice(idx, 1);
|
||||
}
|
||||
|
||||
public isWidgetToggled(widget: Widget) {
|
||||
return this.toggledWidgets.indexOf(widget.id) !== -1;
|
||||
}
|
||||
}
|
23
web/app/configs/widget/widget.component.ts
Normal file
23
web/app/configs/widget/widget.component.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ScalarService } from "../../shared/scalar.service";
|
||||
import { Widget, ScalarToWidgets } from "../../shared/models/widget";
|
||||
|
||||
export class WidgetComponent {
|
||||
|
||||
constructor(protected scalarApi: ScalarService, protected roomId: string) {
|
||||
}
|
||||
|
||||
protected getWidgetsOfType(type: string, altType: string = null): Promise<Widget[]> {
|
||||
return this.scalarApi.getWidgets(this.roomId)
|
||||
.then(resp => ScalarToWidgets(resp))
|
||||
.then(widgets => {
|
||||
let filtered: Widget[] = [];
|
||||
|
||||
for (let widget of widgets) {
|
||||
if (widget.type === type || (altType && widget.type === altType))
|
||||
filtered.push(widget);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<div class="title">
|
||||
<b>{{ integration.name }}</b>
|
||||
<div style="display: flex;">
|
||||
<div class="switch" *ngIf="integration.type !== 'bridge'">
|
||||
<div class="switch" *ngIf="integration.type !== 'bridge' && integration.type !== 'widget'">
|
||||
<ui-switch [checked]="integration.isEnabled" size="small" [disabled]="integration.isBroken" (change)="update()"></ui-switch>
|
||||
</div>
|
||||
<div class="switch" *ngIf="integration.type == 'bridge' && !integration.isEnabled">
|
||||
|
@ -54,6 +54,12 @@ export class RiotComponent {
|
||||
private updateIntegrationState(integration: Integration) {
|
||||
integration.hasConfig = IntegrationService.hasConfig(integration);
|
||||
|
||||
if (integration.type === "widget") {
|
||||
integration.isEnabled = true;
|
||||
integration.isBroken = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (integration.requirements) {
|
||||
let keys = _.keys(integration.requirements);
|
||||
let promises = [];
|
||||
|
@ -4,6 +4,7 @@ import { RssConfigComponent } from "../configs/rss/rss-config.component";
|
||||
import { ContainerContent } from "ngx-modialog";
|
||||
import { IrcConfigComponent } from "../configs/irc/irc-config.component";
|
||||
import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component";
|
||||
import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component";
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
@ -16,7 +17,10 @@ export class IntegrationService {
|
||||
},
|
||||
"bridge": {
|
||||
"irc": true,
|
||||
}
|
||||
},
|
||||
"widget": {
|
||||
"customwidget": true
|
||||
},
|
||||
};
|
||||
|
||||
private static components = {
|
||||
@ -26,7 +30,10 @@ export class IntegrationService {
|
||||
},
|
||||
"bridge": {
|
||||
"irc": IrcConfigComponent,
|
||||
}
|
||||
},
|
||||
"widget": {
|
||||
"customwidget": CustomWidgetConfigComponent,
|
||||
},
|
||||
};
|
||||
|
||||
static isSupported(integration: Integration): boolean {
|
||||
|
@ -30,4 +30,19 @@ export interface JoinRuleStateResponse extends ScalarRoomResponse {
|
||||
response: {
|
||||
join_rule: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WidgetsResponse extends ScalarRoomResponse {
|
||||
response: {
|
||||
type: "im.vector.modular.widgets";
|
||||
state_key: string;
|
||||
sender: string;
|
||||
room_id: string;
|
||||
content: {
|
||||
type: string;
|
||||
url: string;
|
||||
name?: string;
|
||||
data?: any;
|
||||
}
|
||||
}[];
|
||||
}
|
42
web/app/shared/models/widget.ts
Normal file
42
web/app/shared/models/widget.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { WidgetsResponse } from "./scalar_responses";
|
||||
|
||||
// Scalar's widget types (known)
|
||||
export const WIDGET_SCALAR_CUSTOM = "customwidget";
|
||||
export const WIDGET_SCALAR_ETHERPAD = "etherpad";
|
||||
export const WIDGET_SCALAR_GOOGLEDOCS = "googledocs";
|
||||
export const WIDGET_SCALAR_JITSI = "jitsi";
|
||||
export const WIDGET_SCALAR_YOUTUBE = "youtube";
|
||||
export const WIDGET_SCALAR_GRAFANA = "grafana";
|
||||
|
||||
// Dimension has its own set of types to ensure that we don't conflict with Scalar
|
||||
export const WIDGET_DIM_CUSTOM = "dimension-customwidget";
|
||||
|
||||
export interface Widget {
|
||||
id: string;
|
||||
type: string;
|
||||
url: string;
|
||||
name?: string;
|
||||
data?: any;
|
||||
ownerId?: string;
|
||||
|
||||
// used only in ui
|
||||
newName?: string;
|
||||
newUrl?: string;
|
||||
}
|
||||
|
||||
export function ScalarToWidgets(scalarResponse: WidgetsResponse): Widget[] {
|
||||
let widgets = [];
|
||||
|
||||
for (let event of scalarResponse.response) {
|
||||
widgets.push({
|
||||
id: event.state_key,
|
||||
type: event.content.type,
|
||||
url: event.content.url,
|
||||
name: event.content.name,
|
||||
data: event.content.data,
|
||||
ownerId: event.sender,
|
||||
});
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import * as randomString from "random-string";
|
||||
import { MembershipStateResponse, ScalarSuccessResponse, JoinRuleStateResponse } from "./models/scalar_responses";
|
||||
import {
|
||||
MembershipStateResponse,
|
||||
ScalarSuccessResponse,
|
||||
JoinRuleStateResponse,
|
||||
WidgetsResponse
|
||||
} from "./models/scalar_responses";
|
||||
import { Widget } from "./models/widget";
|
||||
|
||||
@Injectable()
|
||||
export class ScalarService {
|
||||
@ -36,6 +42,32 @@ export class ScalarService {
|
||||
});
|
||||
}
|
||||
|
||||
public getWidgets(roomId: string): Promise<WidgetsResponse> {
|
||||
return this.callAction("get_widgets", {
|
||||
room_id: roomId
|
||||
});
|
||||
}
|
||||
|
||||
public setWidget(roomId: string, widget: Widget): Promise<ScalarSuccessResponse> {
|
||||
return this.callAction("set_widget", {
|
||||
room_id: roomId,
|
||||
widget_id: widget.id,
|
||||
type: widget.type,
|
||||
url: widget.url,
|
||||
name: widget.name,
|
||||
data: widget.data
|
||||
});
|
||||
}
|
||||
|
||||
public deleteWidget(roomId: string, widget: Widget): Promise<ScalarSuccessResponse> {
|
||||
return this.callAction("set_widget", {
|
||||
room_id: roomId,
|
||||
widget_id: widget.id,
|
||||
type: widget.type, // required for some reason
|
||||
url: ""
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.callAction("close_scalar", {});
|
||||
}
|
||||
|
BIN
web/public/img/avatars/customwidget.png
Normal file
BIN
web/public/img/avatars/customwidget.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Loading…
Reference in New Issue
Block a user