Set up the correct routing and preparations for the "Riot" version of Dimension
This commit is contained in:
parent
b5a8231a7a
commit
86a4d8dac2
|
@ -29,6 +29,7 @@ import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.componen
|
||||||
import { PageHeaderComponent } from "./page-header/page-header.component";
|
import { PageHeaderComponent } from "./page-header/page-header.component";
|
||||||
import { SpinnerComponent } from "./spinner/spinner.component";
|
import { SpinnerComponent } from "./spinner/spinner.component";
|
||||||
import { BreadcrumbsModule } from "ng2-breadcrumbs";
|
import { BreadcrumbsModule } from "ng2-breadcrumbs";
|
||||||
|
import { RiotHomeComponent } from "./riot/riot-home/home.component";
|
||||||
|
|
||||||
const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents();
|
const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents();
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
|
||||||
VideoWidgetWrapperComponent,
|
VideoWidgetWrapperComponent,
|
||||||
JitsiWidgetWrapperComponent,
|
JitsiWidgetWrapperComponent,
|
||||||
GCalWidgetWrapperComponent,
|
GCalWidgetWrapperComponent,
|
||||||
|
RiotHomeComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
|
|
@ -5,12 +5,21 @@ import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic
|
||||||
import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component";
|
import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component";
|
||||||
import { JitsiWidgetWrapperComponent } from "./widget_wrappers/jitsi/jitsi.component";
|
import { JitsiWidgetWrapperComponent } from "./widget_wrappers/jitsi/jitsi.component";
|
||||||
import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.component";
|
import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.component";
|
||||||
|
import { RiotHomeComponent } from "./riot/riot-home/home.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
|
{path: "riot", pathMatch: "full", redirectTo: "riot-app/home"},
|
||||||
{
|
{
|
||||||
path: "riot", component: RiotComponent, data: {breadcrumb: "Home"},
|
path: "riot-app",
|
||||||
children: [{path: "test", component: RiotComponent, data: {breadcrumb: "Testing"}}]
|
component: RiotComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "home",
|
||||||
|
component: RiotHomeComponent,
|
||||||
|
data: {breadcrumb: "Home"},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{path: "widgets/generic", component: GenericWidgetWrapperComponent},
|
{path: "widgets/generic", component: GenericWidgetWrapperComponent},
|
||||||
{path: "widgets/video", component: VideoWidgetWrapperComponent},
|
{path: "widgets/video", component: VideoWidgetWrapperComponent},
|
||||||
|
|
49
web/app/riot/riot-home/home.component.html
Normal file
49
web/app/riot/riot-home/home.component.html
Normal 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>
|
1
web/app/riot/riot-home/home.component.scss
Normal file
1
web/app/riot/riot-home/home.component.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// component styles are encapsulated and only applied to their components
|
279
web/app/riot/riot-home/home.component.ts
Normal file
279
web/app/riot/riot-home/home.component.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,54 +4,6 @@
|
||||||
</my-page-header>
|
</my-page-header>
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div *ngIf="isError">
|
<router-outlet></router-outlet>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,18 +1,4 @@
|
||||||
import { Component, ViewChildren } from "@angular/core";
|
import { Component } 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"],
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "my-riot",
|
selector: "my-riot",
|
||||||
|
@ -20,254 +6,5 @@ const CATEGORY_MAP = {
|
||||||
styleUrls: ["./riot.component.scss"],
|
styleUrls: ["./riot.component.scss"],
|
||||||
})
|
})
|
||||||
export class RiotComponent {
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue