diff --git a/web/app/app.module.ts b/web/app/app.module.ts
index dcd56e7..bb8bff0 100644
--- a/web/app/app.module.ts
+++ b/web/app/app.module.ts
@@ -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
],
diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts
index d3c3ae7..29afa5e 100644
--- a/web/app/app.routing.ts
+++ b/web/app/app.routing.ts
@@ -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},
diff --git a/web/app/riot/riot-home/home.component.html b/web/app/riot/riot-home/home.component.html
new file mode 100644
index 0000000..06dbc19
--- /dev/null
+++ b/web/app/riot/riot-home/home.component.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
This room is encrypted
+ Integrations are not encrypted!
+ 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.
+
+
+
This room is encrypted
+ There are currently no integrations which support encrypted rooms. Sorry about that!
+
+
+
No integrations available
+ This room does not have any compatible integrations. Please contact the server owner if you're seeing
+ this
+ message.
+
+
+
+
+
+
+
0">
+
+
{{ category }}
+
+
+
+
+
{{ integration.name }}
+
{{ integration.about }}
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/riot/riot-home/home.component.scss b/web/app/riot/riot-home/home.component.scss
new file mode 100644
index 0000000..12e299a
--- /dev/null
+++ b/web/app/riot/riot-home/home.component.scss
@@ -0,0 +1 @@
+// component styles are encapsulated and only applied to their components
diff --git a/web/app/riot/riot-home/home.component.ts b/web/app/riot/riot-home/home.component.ts
new file mode 100644
index 0000000..8f7bfee
--- /dev/null
+++ b/web/app/riot/riot-home/home.component.ts
@@ -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;
+
+ 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 = 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");
+ }
+ }
+}
diff --git a/web/app/riot/riot.component.html b/web/app/riot/riot.component.html
index b59e2c3..a2be092 100644
--- a/web/app/riot/riot.component.html
+++ b/web/app/riot/riot.component.html
@@ -4,54 +4,6 @@
-
-
-
-
-
-
-
-
-
-
-
This room is encrypted
- Integrations are not encrypted!
- 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.
-
-
-
This room is encrypted
- There are currently no integrations which support encrypted rooms. Sorry about that!
-
-
-
No integrations available
- This room does not have any compatible integrations. Please contact the server owner if you're seeing
- this
- message.
-
-
-
-
-
-
-
0">
-
-
{{ category }}
-
-
-
-
-
{{ integration.name }}
-
{{ integration.about }}
-
-
-
-
-
+
\ No newline at end of file
diff --git a/web/app/riot/riot.component.ts b/web/app/riot/riot.component.ts
index 9ff4246..de6e3db 100644
--- a/web/app/riot/riot.component.ts
+++ b/web/app/riot/riot.component.ts
@@ -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;
- 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 = 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");
- }
- }
}