Start of a new UI for Dimension

Integrations need styling and the breadcrumbs don't work. Further, you can't actually add/edit anything.
This commit is contained in:
Travis Ralston 2017-12-14 21:25:15 -07:00
parent 618d6f44ee
commit 6657d5dbf5
19 changed files with 322 additions and 160 deletions

12
package-lock.json generated
View File

@ -4550,6 +4550,12 @@
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
"integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU="
}, },
"ng2-breadcrumbs": {
"version": "0.1.281",
"resolved": "https://registry.npmjs.org/ng2-breadcrumbs/-/ng2-breadcrumbs-0.1.281.tgz",
"integrity": "sha1-OKZsYoD7BgwacyQ7ezP3zZ88waY=",
"dev": true
},
"ngx-modialog": { "ngx-modialog": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.4.tgz", "resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.4.tgz",
@ -8052,6 +8058,12 @@
"wbuf": "1.7.2" "wbuf": "1.7.2"
} }
}, },
"spinkit": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/spinkit/-/spinkit-1.2.5.tgz",
"integrity": "sha1-kPn0ZqIOjjnvJNqVnB5hHCow3VQ=",
"dev": true
},
"split-string": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",

View File

@ -72,6 +72,7 @@
"html-webpack-plugin": "^2.28.0", "html-webpack-plugin": "^2.28.0",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"ng2-breadcrumbs": "^0.1.281",
"ngx-modialog": "^3.0.4", "ngx-modialog": "^3.0.4",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"postcss-cssnext": "^3.0.0", "postcss-cssnext": "^3.0.0",
@ -84,6 +85,7 @@
"rxjs": "^5.5.5", "rxjs": "^5.5.5",
"sass-loader": "^6.0.3", "sass-loader": "^6.0.3",
"shelljs": "^0.7.8", "shelljs": "^0.7.8",
"spinkit": "^1.2.5",
"style-loader": "^0.18.2", "style-loader": "^0.18.2",
"ts-helpers": "^1.1.2", "ts-helpers": "^1.1.2",
"tslint": "^5.8.0", "tslint": "^5.8.0",

View File

@ -33,6 +33,25 @@ class DimensionApi {
app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this)); app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this));
app.get("/api/v1/dimension/widgets/embeddable", this._checkEmbeddable.bind(this)); app.get("/api/v1/dimension/widgets/embeddable", this._checkEmbeddable.bind(this));
app.get("/api/v1/dimension/integration/:type/:integrationType", this._getIntegration.bind(this)); app.get("/api/v1/dimension/integration/:type/:integrationType", this._getIntegration.bind(this));
app.get("/api/v1/dimension/whoami", this._getTokenOwner.bind(this));
}
_getTokenOwner(req, res) {
res.setHeader("Content-Type", "application/json");
var scalarToken = req.query.scalar_token;
if (!scalarToken) {
res.status(400).send({error: 'Missing scalar token'});
return;
}
this._db.getTokenOwner(scalarToken).then(userId => {
res.status(200).send({userId: userId});
}).catch(err => {
log.error("DimensionApi", err);
console.error(err);
res.status(401).send({error: 'Invalid token or other error'});
});
} }
_checkEmbeddable(req, res) { _checkEmbeddable(req, res) {
@ -113,7 +132,8 @@ class DimensionApi {
} }
} }
_getIntegration(req, res) {res.setHeader("Content-Type", "application/json"); _getIntegration(req, res) {
res.setHeader("Content-Type", "application/json");
// Unauthed endpoint. // Unauthed endpoint.
var type = req.params.type; var type = req.params.type;

View File

@ -92,6 +92,18 @@ class DimensionStore {
}); });
} }
/**
* Gets the user ID that owns a given token, returning a falsey value if no one owns the token.
* @param {string} scalarToken the scalar token to check
* @returns {Promise<String>} resolves to the user ID, or a falsey value if no user ID was found
*/
getTokenOwner(scalarToken) {
return this.__Tokens.find({where:{scalarToken: scalarToken}}).then(token => {
if (!token) return Promise.reject(new Error("Token not found"));
return Promise.resolve(token.matrixUserId);
})
}
/** /**
* Gets the upstream token for a given scalar token * Gets the upstream token for a given scalar token
* @param {string} scalarToken the scalar token to lookup * @param {string} scalarToken the scalar token to lookup

View File

@ -2,6 +2,8 @@
</header> </header>
<main> <main>
<toaster-container></toaster-container> <toaster-container></toaster-container>
<!-- The breadcrumb needs to be defined here otherwise it doesn't work -->
<breadcrumb [allowBootstrap]="false" [hidden]="true"></breadcrumb>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<footer> <footer>

View File

@ -26,6 +26,9 @@ import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button
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 { PageHeaderComponent } from "./page-header/page-header.component";
import { SpinnerComponent } from "./spinner/spinner.component";
import { BreadcrumbsModule } from "ng2-breadcrumbs";
const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents(); const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents();
@ -41,6 +44,7 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
BrowserAnimationsModule, BrowserAnimationsModule,
ModalModule.forRoot(), ModalModule.forRoot(),
BootstrapModalModule, BootstrapModalModule,
BreadcrumbsModule,
], ],
declarations: [ declarations: [
...WIDGET_CONFIGURATION_COMPONENTS, ...WIDGET_CONFIGURATION_COMPONENTS,
@ -48,6 +52,8 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
HomeComponent, HomeComponent,
RiotComponent, RiotComponent,
IntegrationComponent, IntegrationComponent,
PageHeaderComponent,
SpinnerComponent,
ScalarCloseComponent, ScalarCloseComponent,
MyFilterPipe, MyFilterPipe,
GenericWidgetWrapperComponent, GenericWidgetWrapperComponent,

View File

@ -8,7 +8,10 @@ import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.componen
const routes: Routes = [ const routes: Routes = [
{path: "", component: HomeComponent}, {path: "", component: HomeComponent},
{path: "riot", component: RiotComponent}, {
path: "riot", component: RiotComponent, data: {breadcrumb: "Home"},
children: [{path: "test", component: RiotComponent, data: {breadcrumb: "Testing"}}]
},
{path: "widgets/generic", component: GenericWidgetWrapperComponent}, {path: "widgets/generic", component: GenericWidgetWrapperComponent},
{path: "widgets/video", component: VideoWidgetWrapperComponent}, {path: "widgets/video", component: VideoWidgetWrapperComponent},
{path: "widgets/jitsi", component: JitsiWidgetWrapperComponent}, {path: "widgets/jitsi", component: JitsiWidgetWrapperComponent},

View File

@ -0,0 +1,9 @@
<div class="header">
<div class="title">
<h2 class="pageName">{{ pageName }}</h2>
<breadcrumb [allowBootstrap]="false"></breadcrumb>
</div>
<div class="quickAction">
<ng-content></ng-content>
</div>
</div>

View File

@ -0,0 +1,28 @@
// component styles are encapsulated and only applied to their components
.header {
width: 100%;
margin-top: 30px;
border-top: 1px solid #e7eaec;
border-bottom: 1px solid #e7eaec;
background-color: #fff;
}
.header .title {
margin: 0;
width: 83.333333%; // col-sm-10
padding: 15px;
}
.header .title .pageName {
font-weight: 100;
padding: 0;
margin: 0;
}
.header .quickAction {
padding: 0;
margin: 0;
float: right;
text-align: right;
width: 16.666667%; // col-sm-2
}

View File

@ -0,0 +1,10 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "my-page-header",
templateUrl: "./page-header.component.html",
styleUrls: ["./page-header.component.scss"],
})
export class PageHeaderComponent {
@Input() pageName: string;
}

View File

@ -1,76 +1,57 @@
<div id="wrapper"> <div id="wrapper">
<my-scalar-close></my-scalar-close> <my-scalar-close></my-scalar-close>
<div *ngIf="error">
<p class="text-danger">{{ error }}</p> <my-page-header pageName="Dimension"></my-page-header>
</div>
<div *ngIf="loading && !error"> <div class="page-content">
<p><i class="fa fa-circle-o-notch fa-spin"></i> Loading...</p> <div *ngIf="isError">
</div> <div class="alert alert-danger">{{ errorMessage }}</div>
<div *ngIf="!error && !loading">
<!-- ------------------------ -->
<!-- EMPTY/ENCRYPTED STATES -->
<!-- ------------------------ -->
<div class="alert alert-warning" *ngIf="integrations.length > 0 && isEncryptedRoom">
<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>
<div class="alert alert-warning" *ngIf="integrations.length === 0 && isEncryptedRoom"> <div *ngIf="isLoading">
<h4>This room is encrypted</h4> <my-spinner></my-spinner>
There are currently no integrations which support encrypted rooms. Sorry about that!
</div>
<div class="alert alert-warning" *ngIf="integrations.length === 0 && !isEncryptedRoom">
<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> </div>
<!-- ------------------------ --> <div *ngIf="!isLoading && !isError">
<!-- WIDGETS --> <!-- ------------------------ -->
<!-- ------------------------ --> <!-- EMPTY/ENCRYPTED STATES -->
<h4 *ngIf="hasAnyOf('widget')"> <!-- ------------------------ -->
Widgets <i class="fa fa-question-circle text-info" style="font-size: 15px;" placement="bottom" <div class="alert alert-warning" *ngIf="hasIntegrations() && isRoomEncrypted">
ngbTooltip="Widgets add small apps to Riot, like Google Docs, Jitsi conferences, and YouTube videos"></i> <h4>This room is encrypted</h4>
</h4> <strong>Integrations are not encrypted!</strong>
<div class="integration-container"> This means that some information about yourself and the
<my-integration *ngFor="let integration of integrations | myFilter:'type':'widget'" room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
[integration]="integration" name,
[roomId]="roomId" your username, your avatar, information about Riot, and other similar details. Add integrations with
[scalarToken]="scalarToken" caution.
(updated)="updateIntegration(integration)"></my-integration> </div>
</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>
<!-- ------------------------ --> <!-- ------------------------ -->
<!-- BOTS --> <!-- CATEGORIES -->
<!-- ------------------------ --> <!-- ------------------------ -->
<h4 *ngIf="hasAnyOf('bot', 'complex-bot')"> <div *ngFor="let category of getCategories()">
Bots <i class="fa fa-question-circle text-info" style="font-size: 15px;" placement="bottom" <div class="ibox" *ngIf="getIntegrationsIn(category).length > 0">
ngbTooltip="Bots can provide entertainment or some utility to your room"></i> <div class="ibox-title">
</h4> <h4>{{ category }}</h4>
<div class="integration-container"> </div>
<my-integration *ngFor="let integration of integrations | myFilter:'type':'bot'" <div class="ibox-content">
[integration]="integration" <div class="integration" *ngFor="let integration of getIntegrationsIn(category)">
[roomId]="roomId" <img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
[scalarToken]="scalarToken" <div class="integration-name">{{ integration.name }}</div>
(updated)="updateIntegration(integration)"></my-integration> <div class="integration-description">{{ integration.about }}</div>
<my-integration *ngFor="let integration of integrations | myFilter:'type':'complex-bot'" </div>
[integration]="integration" </div>
[roomId]="roomId" </div>
[scalarToken]="scalarToken" </div>
(updated)="updateIntegration(integration)"></my-integration>
</div>
<!-- ------------------------ -->
<!-- BRIDGES -->
<!-- ------------------------ -->
<h4 *ngIf="hasAnyOf('bridge')">
Bridges <i class="fa fa-question-circle text-info" style="font-size: 15px;" placement="bottom"
ngbTooltip="Bridges allow people on other platforms to talk in the room"></i>
</h4>
<div class="integration-container">
<my-integration *ngFor="let integration of integrations | myFilter:'type':'bridge'"
[integration]="integration"
[roomId]="roomId"
[scalarToken]="scalarToken"
(updated)="updateIntegration(integration)"></my-integration>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,20 +1,10 @@
// component styles are encapsulated and only applied to their components // component styles are encapsulated and only applied to their components
.integration-container {
display: flex;
flex-wrap: wrap;
margin-bottom: 20px;
}
#wrapper {
padding: 30px;
}
my-scalar-close { my-scalar-close {
position: fixed; position: fixed;
top: 15px; top: 10px;
right: 15px; right: 15px;
} }
h4 { .page-content {
margin-left: 5px; padding: 30px;
} }

View File

@ -8,6 +8,12 @@ import { IntegrationService } from "../shared/integration.service";
import * as _ from "lodash"; import * as _ from "lodash";
import { IntegrationComponent } from "../integration/integration.component"; 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",
templateUrl: "./riot.component.html", templateUrl: "./riot.component.html",
@ -16,15 +22,18 @@ import { IntegrationComponent } from "../integration/integration.component";
export class RiotComponent { export class RiotComponent {
@ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>; @ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>;
public error: string; public isLoading = true;
public integrations: Integration[] = []; public isError = false;
public loading = true; public errorMessage: string;
public roomId: string; public isRoomEncrypted: boolean;
public scalarToken: string;
public isEncryptedRoom = false;
private scalarToken: string;
private roomId: string;
private userId: string;
private requestedScreen: string = null; private requestedScreen: string = null;
private requestedIntegration: string = null; private requestedIntegration: string = null;
private integrationsForCategory: { [category: string]: Integration[] } = {};
private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP;
constructor(private activatedRoute: ActivatedRoute, constructor(private activatedRoute: ActivatedRoute,
private api: ApiService, private api: ApiService,
@ -35,27 +44,94 @@ export class RiotComponent {
this.requestedScreen = params.screen; this.requestedScreen = params.screen;
this.requestedIntegration = params.integ_id; this.requestedIntegration = params.integ_id;
if (!params.scalar_token || !params.room_id) this.error = "Missing scalar token or room ID"; if (!params.scalar_token || !params.room_id) {
else { 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.roomId = params.room_id;
this.scalarToken = params.scalar_token; this.scalarToken = params.scalar_token;
this.api.checkScalarToken(params.scalar_token).then(isValid => { this.api.getTokenOwner(params.scalar_token).then(userId => {
if (isValid) this.init(); if (!userId) {
else this.error = "Invalid scalar token"; 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 => { }).catch(err => {
this.error = "Unable to communicate with Dimension";
console.error(err); console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to communicate with Dimension due to an unknown error.";
}); });
} }
} }
private init() { 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.scalar.isRoomEncrypted(this.roomId).then(payload => {
this.isEncryptedRoom = payload.response; this.isRoomEncrypted = payload.response;
return this.api.getIntegrations(this.roomId, this.scalarToken); return this.api.getIntegrations(this.roomId, this.scalarToken);
}).then(integrations => { }).then(integrations => {
const supportedIntegrations = _.filter(integrations, i => IntegrationService.isSupported(i)); const supportedIntegrations: Integration[] = _.filter(integrations, i => IntegrationService.isSupported(i));
for (const integration of supportedIntegrations) { for (const integration of supportedIntegrations) {
// Widgets technically support encrypted rooms, so unless they explicitly declare that // Widgets technically support encrypted rooms, so unless they explicitly declare that
@ -65,20 +141,34 @@ export class RiotComponent {
integration.supportsEncryptedRooms = true; integration.supportsEncryptedRooms = true;
} }
if (this.isEncryptedRoom) // Flag integrations that aren't supported in encrypted rooms
this.integrations = _.filter(supportedIntegrations, i => i.supportsEncryptedRooms); if (this.isRoomEncrypted) {
else this.integrations = supportedIntegrations; for (const integration of supportedIntegrations) {
if (!integration.supportsEncryptedRooms) {
integration.isSupported = false;
integration.notSupportedReason = "This integration is not supported in encrypted rooms";
}
}
}
let promises = this.integrations.map(b => this.updateIntegrationState(b)); // 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); return Promise.all(promises);
}).then(() => { }).then(() => {
this.loading = false; this.isLoading = false;
// HACK: We wait for the digest cycle so we actually have components to look at // HACK: We wait for the digest cycle so we actually have components to look at
setTimeout(() => this.tryOpenConfigScreen(), 20); setTimeout(() => this.tryOpenConfigScreen(), 20);
}).catch(err => { }).catch(err => {
this.error = "Unable to communicate with Dimension";
console.error(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.";
}); });
} }
@ -109,13 +199,14 @@ export class RiotComponent {
} }
private updateIntegrationState(integration: Integration) { private updateIntegrationState(integration: Integration) {
integration.hasConfig = IntegrationService.hasConfig(integration); integration.hasAdditionalConfig = IntegrationService.hasConfig(integration);
if (integration.type === "widget") { if (integration.type === "widget") {
if (!integration.requirements) integration.requirements = {}; if (!integration.requirements) integration.requirements = {};
integration.requirements["canSetWidget"] = true; integration.requirements["canSetWidget"] = true;
} }
// If the integration has requirements, then we'll check those instead of anything else
if (integration.requirements) { if (integration.requirements) {
let keys = _.keys(integration.requirements); let keys = _.keys(integration.requirements);
let promises = []; let promises = [];
@ -126,29 +217,30 @@ export class RiotComponent {
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
integration.isEnabled = true; integration.isSupported = true;
integration.isBroken = false; integration.notSupportedReason = null;
}, error => { }, error => {
console.error(error); console.error(error);
integration.bridgeError = error.message || error; integration.isSupported = false;
integration.isEnabled = false; integration.notSupportedReason = error;
integration.isBroken = false;
}); });
} }
// 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 => { return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => {
integration.isBroken = false; if (payload.response) {
integration.isSupported = true;
if (!payload.response) { integration.notSupportedReason = null;
integration.isEnabled = false; integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
return; } 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";
} }
integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
}, (error) => { }, (error) => {
console.error(error); console.error(error);
integration.isEnabled = false; integration.isSupported = false;
integration.isBroken = true; integration.notSupportedReason = "Unable to query membership state for this bot";
}); });
} }
@ -163,50 +255,19 @@ export class RiotComponent {
} }
return payload.response.join_rule === requirement return payload.response.join_rule === requirement
? Promise.resolve() ? Promise.resolve()
: Promise.reject(new Error("The room must be " + requirement + " to use this integration.")); : Promise.reject("The room must be " + requirement + " to use this integration.");
}); });
case "canSetWidget": case "canSetWidget":
const processPayload = payload => { const processPayload = payload => {
const response = <any>payload.response; const response = <any>payload.response;
if (response === true) return Promise.resolve(); if (response === true) return Promise.resolve();
if (response.error || response.error.message) if (response.error || response.error.message)
return Promise.reject(new Error("You cannot modify widgets in this room")); return Promise.reject("You cannot modify widgets in this room");
return Promise.reject("Error communicating with Riot"); return Promise.reject("Error communicating with Riot");
}; };
return this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload); return this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload);
default: default:
return Promise.reject(new Error("Requirement '" + key + "' not found")); return Promise.reject("Requirement '" + key + "' not found");
} }
} }
public updateIntegration(integration: Integration) {
let promise = null;
if (!integration.isEnabled) {
promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken);
} else promise = this.scalar.inviteUser(this.roomId, integration.userId);
promise.then(() => {
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 => {
let errorMessage = "Could not update integration status";
if (err.json) {
errorMessage = err.json().error;
} else errorMessage = err.response.error.message;
integration.isEnabled = !integration.isEnabled;
this.toaster.pop("error", errorMessage);
});
}
public hasAnyOf(...types: string[]): boolean {
for (const integration of this.integrations) {
if (types.indexOf(integration.type) !== -1) return true;
}
return false;
}
} }

View File

@ -12,6 +12,11 @@ export class ApiService {
.map(res => res.status === 200).toPromise(); .map(res => res.status === 200).toPromise();
} }
getTokenOwner(scalarToken: String): Promise<string> {
return this.http.get("/api/v1/dimension/whoami", {params:{scalar_token:scalarToken}})
.map(res => res.status === 200 ? res.json()["userId"] : null).toPromise();
}
getIntegrations(roomId: string, scalarToken: string): Promise<Integration[]> { getIntegrations(roomId: string, scalarToken: string): Promise<Integration[]> {
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}}) return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise(); .map(res => res.json()).toPromise();

View File

@ -1,4 +1,5 @@
export interface Integration { export interface Integration {
// These are from the server
type: string; type: string;
integrationType: string; integrationType: string;
userId: string; userId: string;
@ -6,13 +7,14 @@ export interface Integration {
avatar: string; avatar: string;
about: string; // nullable about: string; // nullable
supportsEncryptedRooms: boolean; supportsEncryptedRooms: boolean;
requirements: any; // nullable
// Set by us // These are set in the UI
isEnabled: boolean; isSupported: boolean;
isBroken: boolean; notSupportedReason: string;
hasConfig: boolean; hasAdditionalConfig: boolean;
requirements?: any; // nullable isEnabled: boolean; // for the flip-a-bit integrations
bridgeError: string; // nullable isUpdating: boolean;
} }
export interface RSSIntegration extends Integration { export interface RSSIntegration extends Integration {

View File

@ -0,0 +1,6 @@
<div class="sk-folding-cube">
<div class="sk-cube1 sk-cube"></div>
<div class="sk-cube2 sk-cube"></div>
<div class="sk-cube4 sk-cube"></div>
<div class="sk-cube3 sk-cube"></div>
</div>

View File

@ -0,0 +1,2 @@
// component styles are encapsulated and only applied to their components
@import "../../../node_modules/spinkit/scss/spinners/11-folding-cube.scss";

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
@Component({
selector: "my-spinner",
templateUrl: "./spinner.component.html",
styleUrls: ["./spinner.component.scss"],
})
export class SpinnerComponent {
}

View File

@ -1,11 +1,13 @@
// styles in src/style directory are applied to the whole page // styles in src/style directory are applied to the whole page
@import url('https://fonts.googleapis.com/css?family=Open+Sans:100|Roboto:300');
@import '../../node_modules/angular2-toaster/toaster'; @import '../../node_modules/angular2-toaster/toaster';
body { body {
background: #ddd !important; background: rgb(238, 238, 238) !important;
margin: 0; margin: 0;
padding: 0; padding: 0;
color: #222; color: #333;
font-family: 'Open Sans', sans-serif;
} }
// HACK: Work around dialog not showing up // HACK: Work around dialog not showing up