408 lines
No EOL
16 KiB
TypeScript
408 lines
No EOL
16 KiB
TypeScript
import { convertScalarWidgetsToDtos, EditableWidget } from "../../shared/models/widget";
|
|
import { ToasterService } from "angular2-toaster";
|
|
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
|
|
import { ServiceLocator } from "../../shared/services/locator.service";
|
|
import { SessionStorage } from "../../shared/SessionStorage";
|
|
import { OnInit } from "@angular/core";
|
|
|
|
const SCALAR_WIDGET_LINKS = [
|
|
"https://scalar-staging.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
|
"https://scalar-staging.vector.im/scalar/api/widgets/__TYPE__.html?url=",
|
|
"https://scalar-develop.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
|
"https://demo.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
|
];
|
|
|
|
export const DISABLE_AUTOMATIC_WRAPPING = "";
|
|
|
|
export class WidgetComponent implements OnInit {
|
|
|
|
public isLoading = true;
|
|
public isUpdating = false;
|
|
public widgets: EditableWidget[];
|
|
public newWidget: EditableWidget;
|
|
public defaultExpandedWidgetId: string;
|
|
|
|
private scalarWrapperUrls: string[] = [];
|
|
private wrapperUrl = "";
|
|
|
|
protected toaster = ServiceLocator.injector.get(ToasterService);
|
|
private window = ServiceLocator.injector.get(Window);
|
|
private scalarApi = ServiceLocator.injector.get(ScalarClientApiService);
|
|
|
|
constructor(private widgetTypes: string[],
|
|
public defaultName: string,
|
|
private wrapperId = "generic",
|
|
private scalarWrapperId = null) {
|
|
this.isLoading = true;
|
|
this.isUpdating = false;
|
|
}
|
|
|
|
public ngOnInit(): void {
|
|
if (this.wrapperId) {
|
|
this.wrapperUrl = this.window.location.origin + "/widgets/" + this.wrapperId + "?url=";
|
|
|
|
if (!this.scalarWrapperId) this.scalarWrapperId = this.wrapperId;
|
|
for (let widgetLink of SCALAR_WIDGET_LINKS) {
|
|
this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", this.scalarWrapperId));
|
|
}
|
|
}
|
|
|
|
this.prepareNewWidget();
|
|
this.getWidgetsOfType(this.widgetTypes).then(widgets => {
|
|
this.widgets = widgets;
|
|
for (let widget of this.widgets) {
|
|
this.unpackWidget(widget);
|
|
}
|
|
|
|
this.OnWidgetsDiscovered(this.widgets);
|
|
this.isLoading = false;
|
|
this.isUpdating = false;
|
|
|
|
// We reset after discovering to ensure that the widget component can correctly
|
|
// set the state of the widget prior to us unpacking it (again)
|
|
for (let widget of this.widgets) {
|
|
this.resetWidget(widget);
|
|
}
|
|
|
|
// See if we should request editing a particular widget
|
|
if (SessionStorage.editIntegrationId && SessionStorage.editsRequested === 1) {
|
|
let editWidget: EditableWidget = null;
|
|
let otherWidgets: EditableWidget[] = [];
|
|
for (let widget of this.widgets) {
|
|
if (widget.id === SessionStorage.editIntegrationId) {
|
|
editWidget = widget;
|
|
} else otherWidgets.push(widget);
|
|
}
|
|
|
|
if (editWidget) {
|
|
console.log("Requesting edit for " + editWidget.id);
|
|
this.widgets = [editWidget];
|
|
otherWidgets.forEach(w => this.widgets.push(w));
|
|
this.defaultExpandedWidgetId = editWidget.id;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Populates the Dimension-specific fields of the widget, overwriting any values that
|
|
* were there previously.
|
|
* @param {EditableWidget} widget The widget to unpack.
|
|
*/
|
|
private unpackWidget(widget: EditableWidget) {
|
|
widget.dimension = {
|
|
newUrl: this.unwrapUrl(widget.url),
|
|
newName: widget.name,
|
|
newTitle: widget.data ? widget.data.title : null,
|
|
newData: JSON.parse(JSON.stringify(widget.data || "{}")),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Packs a widget, converting the Dimension-specific parameters into a widget. This will
|
|
* overwrite any values in the widget which need to be committed to from the Dimension
|
|
* fields.
|
|
* @param {EditableWidget} widget The widget to pack.
|
|
*/
|
|
private packWidget(widget: EditableWidget) {
|
|
// The widget already has an ID and type, we just need to fill in the bits
|
|
widget.name = widget.dimension.newName || this.defaultName;
|
|
widget.data = widget.dimension.newData || {};
|
|
widget.url = this.wrapUrl(widget.dimension.newUrl, Object.keys(widget.data).map(k => "$" + k));
|
|
widget.type = this.widgetTypes[0]; // always set the type to be the latest type
|
|
|
|
// Populate our stuff
|
|
widget.data["dimension:app:metadata"] = {
|
|
inRoomId: SessionStorage.roomId,
|
|
wrapperUrlBase: this.wrapperUrl,
|
|
wrapperId: this.wrapperId,
|
|
scalarWrapperId: this.scalarWrapperId,
|
|
integration: {
|
|
category: SessionStorage.editIntegration.category,
|
|
type: SessionStorage.editIntegration.type,
|
|
},
|
|
lastUpdatedTs: new Date().getTime(),
|
|
};
|
|
|
|
// Set the title to an appropriate value. Empty strings are valid titles, but other falsey
|
|
// values should be considered as "no title".
|
|
if (widget.dimension.newTitle || widget.dimension.newTitle === "")
|
|
widget.data["title"] = widget.dimension.newTitle;
|
|
else widget.data["title"] = undefined;
|
|
}
|
|
|
|
/**
|
|
* Builds a new widget and assigns it to the newWidget property. This also expands the
|
|
* Dimension-specific fields for the widget with reasonable defaults.
|
|
*/
|
|
protected prepareNewWidget() {
|
|
this.newWidget = <EditableWidget>{
|
|
id: "dimension-" + this.widgetTypes[0] + "-" + (new Date().getTime()),
|
|
type: this.widgetTypes[0],
|
|
name: this.defaultName,
|
|
url: window.location.origin,
|
|
ownerId: SessionStorage.userId,
|
|
dimension: {
|
|
newUrl: "",
|
|
newName: "",
|
|
newTitle: null, // by default we don't offer a title (empty strings are valid titles)
|
|
newData: {},
|
|
},
|
|
};
|
|
console.log("Emitting new widget prepared event");
|
|
this.OnNewWidgetPrepared(this.newWidget);
|
|
}
|
|
|
|
/**
|
|
* Gets widgets from the Scalar API. The widgets returned from here are not unpacked and
|
|
* may not have the Dimension-specific fields set.
|
|
* @param {string[]} types The widget types to look for
|
|
* @return {Promise<EditableWidget[]>} The widgets discovered
|
|
*/
|
|
private getWidgetsOfType(types: string[]): Promise<EditableWidget[]> {
|
|
return this.scalarApi.getWidgets(SessionStorage.roomId)
|
|
.then(resp => convertScalarWidgetsToDtos(resp))
|
|
.then(widgets => widgets.filter(w => types.indexOf(w.type) !== -1));
|
|
}
|
|
|
|
/**
|
|
* Gets the actual URL from known wrappers. If the URL is found to be not wrapped, or no
|
|
* wrappers are applicable, then the given URL is returned.
|
|
* @param {string} url The URL to unwrap
|
|
* @return {string} The unwrapped URL
|
|
*/
|
|
private unwrapUrl(url: string): string {
|
|
if (!this.wrapperUrl) return url;
|
|
|
|
const urls = [this.wrapperUrl].concat(this.scalarWrapperUrls);
|
|
for (let scalarUrl of urls) {
|
|
if (url.startsWith(scalarUrl)) {
|
|
return decodeURIComponent(url.substring(scalarUrl.length));
|
|
}
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Wraps the URL with an appropriate wrapper. If no wrapper is defined for this component
|
|
* then the given URL is returned.
|
|
* @param {string} url The URL to wrap
|
|
* @param {string[]} customVars The values in the URL which should not be encoded
|
|
* @return {string} The wrapped URL
|
|
*/
|
|
private wrapUrl(url: string, customVars: string[]): string {
|
|
if (!this.wrapperUrl) return url;
|
|
|
|
let encodedURL = this.wrapperUrl + encodeURIComponent(url);
|
|
encodedURL = this.decodeParams(encodedURL, customVars);
|
|
|
|
return encodedURL;
|
|
}
|
|
|
|
/**
|
|
* Decodes the given URL to make the parameters safe for interpretation by clients. The variables
|
|
* specified in the widget spec will be decoded automatically.
|
|
* @param {string} encodedURL The encoded URL
|
|
* @param {string[]} customVars The custom variables to decode
|
|
* @returns {string} The URL with variables decoded accordingly
|
|
*/
|
|
protected decodeParams(encodedURL: string, customVars: string[]): 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 customVars) {
|
|
encodedURL = encodedURL.replace(encodeURIComponent(key), key);
|
|
}
|
|
|
|
return encodedURL;
|
|
}
|
|
|
|
/**
|
|
* Adds the widget stored in newWidget to the room.
|
|
* @returns {Promise<*>} Resolves when the widget has been added and newWidget is populated
|
|
* with a new widget.
|
|
*/
|
|
public addWidget(): Promise<any> {
|
|
// Make sure we call "before add" before validating the URL
|
|
try {
|
|
this.OnWidgetBeforeAdd(this.newWidget);
|
|
} catch (error) {
|
|
this.toaster.pop("warning", error.message);
|
|
return;
|
|
}
|
|
|
|
if (!this.newWidget.dimension.newUrl || this.newWidget.dimension.newUrl.trim().length === 0) {
|
|
this.toaster.pop("warning", "Please enter a URL for the widget");
|
|
return;
|
|
}
|
|
|
|
this.packWidget(this.newWidget);
|
|
|
|
this.isUpdating = true;
|
|
return this.scalarApi.setWidget(SessionStorage.roomId, this.newWidget)
|
|
.then(() => this.widgets.push(this.newWidget))
|
|
.then(() => {
|
|
this.isUpdating = false;
|
|
this.OnWidgetAfterAdd(this.newWidget);
|
|
this.prepareNewWidget();
|
|
this.toaster.pop("success", "Widget added!");
|
|
})
|
|
.catch(err => {
|
|
this.isUpdating = false;
|
|
console.error(err);
|
|
this.toaster.pop("error", err.json().error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Saves a widget, persisting the changes to the room.
|
|
* @param {EditableWidget} widget The widget to save.
|
|
* @returns {Promise<any>} Resolves when the widget has been updated in the room.
|
|
*/
|
|
public saveWidget(widget: EditableWidget): Promise<any> {
|
|
// Make sure we call "before add" before validating the URL
|
|
try {
|
|
this.OnWidgetBeforeEdit(this.newWidget);
|
|
} catch (error) {
|
|
this.toaster.pop("warning", error.message);
|
|
return;
|
|
}
|
|
|
|
if (!widget.dimension.newUrl || widget.dimension.newUrl.trim().length === 0) {
|
|
this.toaster.pop("warning", "Please enter a URL for the widget");
|
|
return;
|
|
}
|
|
|
|
this.packWidget(widget);
|
|
|
|
this.isUpdating = true;
|
|
return this.scalarApi.setWidget(SessionStorage.roomId, widget)
|
|
.then(() => {
|
|
this.isUpdating = false;
|
|
this.OnWidgetAfterEdit(widget);
|
|
this.toaster.pop("success", "Widget updated!");
|
|
})
|
|
.catch(err => {
|
|
this.isUpdating = false;
|
|
console.error(err);
|
|
this.toaster.pop("error", err.json().error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes a widget from the room
|
|
* @param {EditableWidget} widget The widget to remove
|
|
* @returns {Promise<*>} Resolves when the widget has been removed.
|
|
*/
|
|
public removeWidget(widget: EditableWidget): Promise<any> {
|
|
this.isUpdating = true;
|
|
this.OnWidgetBeforeDelete(widget);
|
|
return this.scalarApi.deleteWidget(SessionStorage.roomId, widget)
|
|
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
|
|
.then(() => {
|
|
this.isUpdating = false;
|
|
this.OnWidgetAfterDelete(widget);
|
|
this.toaster.pop("success", "Widget deleted!");
|
|
})
|
|
.catch(err => {
|
|
this.isUpdating = false;
|
|
console.error(err);
|
|
this.toaster.pop("error", err.json().error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resets a widget to before it had changes made to it
|
|
* @param {EditableWidget} widget The widget to reset
|
|
*/
|
|
public resetWidget(widget: EditableWidget) {
|
|
this.unpackWidget(widget);
|
|
this.OnWidgetPreparedForEdit(widget);
|
|
}
|
|
|
|
// Component hooks below here
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* Called when a new widget has been created in the newWidget field
|
|
* @param {EditableWidget} _widget The widget that has been prepared
|
|
*/
|
|
protected OnNewWidgetPrepared(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called after all the widgets have been discovered and unpacked for the room.
|
|
* @param {EditableWidget[]} _widgets The widgets that were discovered
|
|
*/
|
|
protected OnWidgetsDiscovered(_widgets: EditableWidget[]): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called before the widget is added to the room, and before the Dimension properties
|
|
* have been copied over. This is a good time to do validation.
|
|
* @param {EditableWidget} _widget The widget that is about to be added
|
|
*/
|
|
protected OnWidgetBeforeAdd(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called after the widget has been added to the room, but before the newWidget field
|
|
* has been set to a new widget.
|
|
* @param {EditableWidget} _widget The widget that has been added.
|
|
*/
|
|
protected OnWidgetAfterAdd(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called when the given widget has been asked to be prepared for editing. At this point
|
|
* the widget is not being persisted to the room, it is just updating the EditingWidget's
|
|
* properties for the user's ability to edit it.
|
|
* @param {EditableWidget} _widget The widget that has been prepared for editing
|
|
*/
|
|
protected OnWidgetPreparedForEdit(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called before the given widget has been updated in the room, and before the Dimension
|
|
* properties have been copied over. This is a good time to do validation.
|
|
* This is not called for widgets being deleted.
|
|
* @param {EditableWidget} _widget The widget about to be edited
|
|
*/
|
|
protected OnWidgetBeforeEdit(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called after a given widget has been updated in the room. This is not called for
|
|
* widgets being deleted.
|
|
* @param {EditableWidget} _widget The widget that has been updated.
|
|
*/
|
|
protected OnWidgetAfterEdit(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called before the given widget has been removed from the room. No changes to the
|
|
* widget have been made at this point.
|
|
* @param {EditableWidget} _widget The widget about to be deleted.
|
|
*/
|
|
protected OnWidgetBeforeDelete(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
|
|
/**
|
|
* Called after a given widget has been deleted from the room. The widget will be in
|
|
* the deleted state and will no longer be tracked anywhere on the component.
|
|
* @param {EditableWidget} _widget The widget that has been deleted.
|
|
*/
|
|
protected OnWidgetAfterDelete(_widget: EditableWidget): void {
|
|
// Component hook
|
|
}
|
|
} |