Set up the correct routing and preparations for the "Riot" version of Dimension

This commit is contained in:
Travis Ralston 2017-12-14 23:41:56 -07:00
parent b5a8231a7a
commit 86a4d8dac2
7 changed files with 344 additions and 315 deletions

View file

@ -29,6 +29,7 @@ import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.componen
import { PageHeaderComponent } from "./page-header/page-header.component";
import { SpinnerComponent } from "./spinner/spinner.component";
import { BreadcrumbsModule } from "ng2-breadcrumbs";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents();
@ -62,6 +63,7 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
VideoWidgetWrapperComponent,
JitsiWidgetWrapperComponent,
GCalWidgetWrapperComponent,
RiotHomeComponent,
// Vendor
],

View file

@ -5,12 +5,21 @@ import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic
import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component";
import { JitsiWidgetWrapperComponent } from "./widget_wrappers/jitsi/jitsi.component";
import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.component";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
const routes: Routes = [
{path: "", component: HomeComponent},
{path: "riot", pathMatch: "full", redirectTo: "riot-app/home"},
{
path: "riot", component: RiotComponent, data: {breadcrumb: "Home"},
children: [{path: "test", component: RiotComponent, data: {breadcrumb: "Testing"}}]
path: "riot-app",
component: RiotComponent,
children: [
{
path: "home",
component: RiotHomeComponent,
data: {breadcrumb: "Home"},
},
],
},
{path: "widgets/generic", component: GenericWidgetWrapperComponent},
{path: "widgets/video", component: VideoWidgetWrapperComponent},

View file

@ -0,0 +1,49 @@
<div *ngIf="isError">
<div class="alert alert-danger">{{ errorMessage }}</div>
</div>
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading && !isError">
<!-- ------------------------ -->
<!-- EMPTY/ENCRYPTED STATES -->
<!-- ------------------------ -->
<div class="alert alert-warning" *ngIf="hasIntegrations() && isRoomEncrypted">
<h4>This room is encrypted</h4>
<strong>Integrations are not encrypted!</strong>
This means that some information about yourself and the
room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
name,
your username, your avatar, information about Riot, and other similar details. Add integrations with
caution.
</div>
<div class="alert alert-warning" *ngIf="!hasIntegrations() && isRoomEncrypted">
<h4>This room is encrypted</h4>
There are currently no integrations which support encrypted rooms. Sorry about that!
</div>
<div class="alert alert-warning" *ngIf="!hasIntegrations() && !isRoomEncrypted">
<h4>No integrations available</h4>
This room does not have any compatible integrations. Please contact the server owner if you're seeing
this
message.
</div>
<!-- ------------------------ -->
<!-- CATEGORIES -->
<!-- ------------------------ -->
<div *ngFor="let category of getCategories()">
<div class="ibox" *ngIf="getIntegrationsIn(category).length > 0">
<div class="ibox-title">
<h4>{{ category }}</h4>
</div>
<div class="ibox-content">
<div class="integration" *ngFor="let integration of getIntegrationsIn(category)">
<img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
<div class="integration-name">{{ integration.name }}</div>
<div class="integration-description">{{ integration.about }}</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1 @@
// component styles are encapsulated and only applied to their components

View file

@ -0,0 +1,279 @@
import { Component, ViewChildren } from "@angular/core";
import { IntegrationService } from "../../shared/integration.service";
import { IntegrationComponent } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Integration } from "../../shared/models/integration";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "../../shared/api.service";
import { ScalarService } from "../../shared/scalar.service";
import * as _ from "lodash";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
const CATEGORY_MAP = {
"Widgets": ["widget"],
"Bots": ["complex-bot", "bot"],
"Bridges": ["bridge"],
};
@Component({
selector: "my-riot-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.scss"],
})
export class RiotHomeComponent {
@ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>;
public isLoading = true;
public isError = false;
public errorMessage: string;
public isRoomEncrypted: boolean;
private scalarToken: string;
private roomId: string;
private userId: string;
private requestedScreen: string = null;
private requestedIntegration: string = null;
private integrationsForCategory: { [category: string]: Integration[] } = {};
private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP;
constructor(private activatedRoute: ActivatedRoute,
private api: ApiService,
private scalar: ScalarService,
private toaster: ToasterService,
private sanitizer: DomSanitizer) {
let params: any = this.activatedRoute.snapshot.queryParams;
this.requestedScreen = params.screen;
this.requestedIntegration = params.integ_id;
if (!params.scalar_token || !params.room_id) {
console.error("Unable to load Dimension. Missing room ID or scalar token.");
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to load Dimension - missing room ID or token.";
} else {
this.roomId = params.room_id;
this.scalarToken = params.scalar_token;
this.api.getTokenOwner(params.scalar_token).then(userId => {
if (!userId) {
console.error("No user returned for token. Is the token registered in Dimension?");
this.isError = true;
this.isLoading = false;
this.errorMessage = "Could not verify your token. Please try logging out of Riot and back in. Be sure to back up your encryption keys!";
} else {
this.userId = userId;
console.log("Scalar token belongs to " + userId);
this.prepareIntegrations();
}
}).catch(err => {
console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to communicate with Dimension due to an unknown error.";
});
}
}
public getSafeUrl(url: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
public hasIntegrations(): boolean {
for (const category of this.getCategories()) {
if (this.getIntegrationsIn(category).length > 0) return true;
}
return false;
}
public getCategories(): string[] {
return Object.keys(this.categoryMap);
}
public getIntegrationsIn(category: string): Integration[] {
return this.integrationsForCategory[category];
}
public modifyIntegration(integration: Integration) {
console.log(this.userId + " is trying to modify " + integration.name);
if (integration.hasAdditionalConfig) {
// TODO: Navigate to edit screen
console.log("EDIT SCREEN FOR " + integration.name);
} else {
// It's a flip-a-bit (simple bot)
// TODO: "Are you sure?" dialog
let promise = null;
if (!integration.isEnabled) {
promise = this.scalar.inviteUser(this.roomId, integration.userId);
} else promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken);
// We set this ahead of the promise for debouncing
integration.isEnabled = !integration.isEnabled;
integration.isUpdating = true;
promise.then(() => {
integration.isUpdating = false;
if (integration.isEnabled) this.toaster.pop("success", integration.name + " was invited to the room");
else this.toaster.pop("success", integration.name + " was removed from the room");
}).catch(err => {
integration.isEnabled = !integration.isEnabled; // revert the status change
integration.isUpdating = false;
console.error(err);
let errorMessage = null;
if (err.json) errorMessage = err.json().error;
if (err.response && err.response.error) errorMessage = err.response.error.message;
if (!errorMessage) errorMessage = "Could not update integration status";
this.toaster.pop("error", errorMessage);
})
}
}
private prepareIntegrations() {
this.scalar.isRoomEncrypted(this.roomId).then(payload => {
this.isRoomEncrypted = payload.response;
return this.api.getIntegrations(this.roomId, this.scalarToken);
}).then(integrations => {
const supportedIntegrations: Integration[] = _.filter(integrations, i => IntegrationService.isSupported(i));
for (const integration of supportedIntegrations) {
// Widgets technically support encrypted rooms, so unless they explicitly declare that
// they don't, we'll assume they do. A warning about adding widgets in encrypted rooms
// is displayed to users elsewhere.
if (integration.type === "widget" && integration.supportsEncryptedRooms !== false)
integration.supportsEncryptedRooms = true;
}
// Flag integrations that aren't supported in encrypted rooms
if (this.isRoomEncrypted) {
for (const integration of supportedIntegrations) {
if (!integration.supportsEncryptedRooms) {
integration.isSupported = false;
integration.notSupportedReason = "This integration is not supported in encrypted rooms";
}
}
}
// Set up the categories
for (const category of Object.keys(this.categoryMap)) {
const supportedTypes = this.categoryMap[category];
this.integrationsForCategory[category] = _.filter(supportedIntegrations, i => supportedTypes.indexOf(i.type) !== -1);
}
let promises = supportedIntegrations.map(i => this.updateIntegrationState(i));
return Promise.all(promises);
}).then(() => {
this.isLoading = false;
// HACK: We wait for the digest cycle so we actually have components to look at
setTimeout(() => this.tryOpenConfigScreen(), 20);
}).catch(err => {
console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to set up Dimension. This version of Riot may not supported or there may be a problem with the server.";
});
}
private tryOpenConfigScreen() {
let type = null;
let integrationType = null;
if (!this.requestedScreen) return;
const targetIntegration = IntegrationService.getIntegrationForScreen(this.requestedScreen);
if (targetIntegration) {
type = targetIntegration.type;
integrationType = targetIntegration.integrationType;
} else {
console.log("Unknown screen requested: " + this.requestedScreen);
}
let opened = false;
this.integrationComponents.forEach(component => {
if (opened) return;
if (component.integration.type !== type || component.integration.integrationType !== integrationType) return;
console.log("Configuring integration " + this.requestedIntegration + " type=" + type + " integrationType=" + integrationType);
component.configureIntegration(this.requestedIntegration);
opened = true;
});
if (!opened) {
console.log("Failed to find integration component for type=" + type + " integrationType=" + integrationType);
}
}
private updateIntegrationState(integration: Integration) {
integration.hasAdditionalConfig = IntegrationService.hasConfig(integration);
if (integration.type === "widget") {
if (!integration.requirements) integration.requirements = {};
integration.requirements["canSetWidget"] = true;
}
// If the integration has requirements, then we'll check those instead of anything else
if (integration.requirements) {
let keys = _.keys(integration.requirements);
let promises = [];
for (let key of keys) {
let requirement = this.checkRequirement(integration, key);
promises.push(requirement);
}
return Promise.all(promises).then(() => {
integration.isSupported = true;
integration.notSupportedReason = null;
}, error => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = error;
});
}
// The integration doesn't have requirements, so we'll just make sure the bot user can be retrieved.
return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => {
if (payload.response) {
integration.isSupported = true;
integration.notSupportedReason = null;
integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
} else {
console.error("No response received to membership query of " + integration.userId);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
}
}, (error) => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
});
}
private checkRequirement(integration: Integration, key: string) {
let requirement = integration.requirements[key];
switch (key) {
case "joinRule":
return this.scalar.getJoinRule(this.roomId).then(payload => {
if (!payload.response) {
return Promise.reject("Could not communicate with Riot");
}
return payload.response.join_rule === requirement
? Promise.resolve()
: Promise.reject("The room must be " + requirement + " to use this integration.");
});
case "canSetWidget":
const processPayload = payload => {
const response = <any>payload.response;
if (response === true) return Promise.resolve();
if (response.error || response.error.message)
return Promise.reject("You cannot modify widgets in this room");
return Promise.reject("Error communicating with Riot");
};
return this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload);
default:
return Promise.reject("Requirement '" + key + "' not found");
}
}
}

View file

@ -4,54 +4,6 @@
</my-page-header>
<div class="page-content">
<div *ngIf="isError">
<div class="alert alert-danger">{{ errorMessage }}</div>
</div>
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading && !isError">
<!-- ------------------------ -->
<!-- EMPTY/ENCRYPTED STATES -->
<!-- ------------------------ -->
<div class="alert alert-warning" *ngIf="hasIntegrations() && isRoomEncrypted">
<h4>This room is encrypted</h4>
<strong>Integrations are not encrypted!</strong>
This means that some information about yourself and the
room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
name,
your username, your avatar, information about Riot, and other similar details. Add integrations with
caution.
</div>
<div class="alert alert-warning" *ngIf="!hasIntegrations() && isRoomEncrypted">
<h4>This room is encrypted</h4>
There are currently no integrations which support encrypted rooms. Sorry about that!
</div>
<div class="alert alert-warning" *ngIf="!hasIntegrations() && !isRoomEncrypted">
<h4>No integrations available</h4>
This room does not have any compatible integrations. Please contact the server owner if you're seeing
this
message.
</div>
<!-- ------------------------ -->
<!-- CATEGORIES -->
<!-- ------------------------ -->
<div *ngFor="let category of getCategories()">
<div class="ibox" *ngIf="getIntegrationsIn(category).length > 0">
<div class="ibox-title">
<h4>{{ category }}</h4>
</div>
<div class="ibox-content">
<div class="integration" *ngFor="let integration of getIntegrationsIn(category)">
<img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
<div class="integration-name">{{ integration.name }}</div>
<div class="integration-description">{{ integration.about }}</div>
</div>
</div>
</div>
</div>
</div>
<router-outlet></router-outlet>
</div>
</div>

View file

@ -1,18 +1,4 @@
import { Component, ViewChildren } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "../shared/api.service";
import { ScalarService } from "../shared/scalar.service";
import { ToasterService } from "angular2-toaster";
import { Integration } from "../shared/models/integration";
import { IntegrationService } from "../shared/integration.service";
import * as _ from "lodash";
import { IntegrationComponent } from "../integration/integration.component";
const CATEGORY_MAP = {
"Widgets": ["widget"],
"Bots": ["complex-bot", "bot"],
"Bridges": ["bridge"],
};
import { Component } from "@angular/core";
@Component({
selector: "my-riot",
@ -20,254 +6,5 @@ const CATEGORY_MAP = {
styleUrls: ["./riot.component.scss"],
})
export class RiotComponent {
@ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>;
public isLoading = true;
public isError = false;
public errorMessage: string;
public isRoomEncrypted: boolean;
private scalarToken: string;
private roomId: string;
private userId: string;
private requestedScreen: string = null;
private requestedIntegration: string = null;
private integrationsForCategory: { [category: string]: Integration[] } = {};
private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP;
constructor(private activatedRoute: ActivatedRoute,
private api: ApiService,
private scalar: ScalarService,
private toaster: ToasterService) {
let params: any = this.activatedRoute.snapshot.queryParams;
this.requestedScreen = params.screen;
this.requestedIntegration = params.integ_id;
if (!params.scalar_token || !params.room_id) {
console.error("Unable to load Dimension. Missing room ID or scalar token.");
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to load Dimension - missing room ID or token.";
} else {
this.roomId = params.room_id;
this.scalarToken = params.scalar_token;
this.api.getTokenOwner(params.scalar_token).then(userId => {
if (!userId) {
console.error("No user returned for token. Is the token registered in Dimension?");
this.isError = true;
this.isLoading = false;
this.errorMessage = "Could not verify your token. Please try logging out of Riot and back in. Be sure to back up your encryption keys!";
} else {
this.userId = userId;
console.log("Scalar token belongs to " + userId);
this.prepareIntegrations();
}
}).catch(err => {
console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to communicate with Dimension due to an unknown error.";
});
}
}
public hasIntegrations(): boolean {
for (const category of this.getCategories()) {
if (this.getIntegrationsIn(category).length > 0) return true;
}
return false;
}
public getCategories(): string[] {
return Object.keys(this.categoryMap);
}
public getIntegrationsIn(category: string): Integration[] {
return this.integrationsForCategory[category];
}
public modifyIntegration(integration: Integration) {
console.log(this.userId + " is trying to modify " + integration.name);
if (integration.hasAdditionalConfig) {
// TODO: Navigate to edit screen
console.log("EDIT SCREEN FOR " + integration.name);
} else {
// It's a flip-a-bit (simple bot)
// TODO: "Are you sure?" dialog
let promise = null;
if (!integration.isEnabled) {
promise = this.scalar.inviteUser(this.roomId, integration.userId);
} else promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken);
// We set this ahead of the promise for debouncing
integration.isEnabled = !integration.isEnabled;
integration.isUpdating = true;
promise.then(() => {
integration.isUpdating = false;
if (integration.isEnabled) this.toaster.pop("success", integration.name + " was invited to the room");
else this.toaster.pop("success", integration.name + " was removed from the room");
}).catch(err => {
integration.isEnabled = !integration.isEnabled; // revert the status change
integration.isUpdating = false;
console.error(err);
let errorMessage = null;
if (err.json) errorMessage = err.json().error;
if (err.response && err.response.error) errorMessage = err.response.error.message;
if (!errorMessage) errorMessage = "Could not update integration status";
this.toaster.pop("error", errorMessage);
})
}
}
private prepareIntegrations() {
this.scalar.isRoomEncrypted(this.roomId).then(payload => {
this.isRoomEncrypted = payload.response;
return this.api.getIntegrations(this.roomId, this.scalarToken);
}).then(integrations => {
const supportedIntegrations: Integration[] = _.filter(integrations, i => IntegrationService.isSupported(i));
for (const integration of supportedIntegrations) {
// Widgets technically support encrypted rooms, so unless they explicitly declare that
// they don't, we'll assume they do. A warning about adding widgets in encrypted rooms
// is displayed to users elsewhere.
if (integration.type === "widget" && integration.supportsEncryptedRooms !== false)
integration.supportsEncryptedRooms = true;
}
// Flag integrations that aren't supported in encrypted rooms
if (this.isRoomEncrypted) {
for (const integration of supportedIntegrations) {
if (!integration.supportsEncryptedRooms) {
integration.isSupported = false;
integration.notSupportedReason = "This integration is not supported in encrypted rooms";
}
}
}
// Set up the categories
for (const category of Object.keys(this.categoryMap)) {
const supportedTypes = this.categoryMap[category];
this.integrationsForCategory[category] = _.filter(supportedIntegrations, i => supportedTypes.indexOf(i.type) !== -1);
}
let promises = supportedIntegrations.map(i => this.updateIntegrationState(i));
return Promise.all(promises);
}).then(() => {
this.isLoading = false;
// HACK: We wait for the digest cycle so we actually have components to look at
setTimeout(() => this.tryOpenConfigScreen(), 20);
}).catch(err => {
console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to set up Dimension. This version of Riot may not supported or there may be a problem with the server.";
});
}
private tryOpenConfigScreen() {
let type = null;
let integrationType = null;
if (!this.requestedScreen) return;
const targetIntegration = IntegrationService.getIntegrationForScreen(this.requestedScreen);
if (targetIntegration) {
type = targetIntegration.type;
integrationType = targetIntegration.integrationType;
} else {
console.log("Unknown screen requested: " + this.requestedScreen);
}
let opened = false;
this.integrationComponents.forEach(component => {
if (opened) return;
if (component.integration.type !== type || component.integration.integrationType !== integrationType) return;
console.log("Configuring integration " + this.requestedIntegration + " type=" + type + " integrationType=" + integrationType);
component.configureIntegration(this.requestedIntegration);
opened = true;
});
if (!opened) {
console.log("Failed to find integration component for type=" + type + " integrationType=" + integrationType);
}
}
private updateIntegrationState(integration: Integration) {
integration.hasAdditionalConfig = IntegrationService.hasConfig(integration);
if (integration.type === "widget") {
if (!integration.requirements) integration.requirements = {};
integration.requirements["canSetWidget"] = true;
}
// If the integration has requirements, then we'll check those instead of anything else
if (integration.requirements) {
let keys = _.keys(integration.requirements);
let promises = [];
for (let key of keys) {
let requirement = this.checkRequirement(integration, key);
promises.push(requirement);
}
return Promise.all(promises).then(() => {
integration.isSupported = true;
integration.notSupportedReason = null;
}, error => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = error;
});
}
// The integration doesn't have requirements, so we'll just make sure the bot user can be retrieved.
return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => {
if (payload.response) {
integration.isSupported = true;
integration.notSupportedReason = null;
integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
} else {
console.error("No response received to membership query of " + integration.userId);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
}
}, (error) => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
});
}
private checkRequirement(integration: Integration, key: string) {
let requirement = integration.requirements[key];
switch (key) {
case "joinRule":
return this.scalar.getJoinRule(this.roomId).then(payload => {
if (!payload.response) {
return Promise.reject("Could not communicate with Riot");
}
return payload.response.join_rule === requirement
? Promise.resolve()
: Promise.reject("The room must be " + requirement + " to use this integration.");
});
case "canSetWidget":
const processPayload = payload => {
const response = <any>payload.response;
if (response === true) return Promise.resolve();
if (response.error || response.error.message)
return Promise.reject("You cannot modify widgets in this room");
return Promise.reject("Error communicating with Riot");
};
return this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload);
default:
return Promise.reject("Requirement '" + key + "' not found");
}
}
}