mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 01:05:53 -04:00
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:
parent
618d6f44ee
commit
6657d5dbf5
12
package-lock.json
generated
12
package-lock.json
generated
@ -4550,6 +4550,12 @@
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.4.tgz",
|
||||
@ -8052,6 +8058,12 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
|
@ -72,6 +72,7 @@
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"jquery": "^3.2.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"ng2-breadcrumbs": "^0.1.281",
|
||||
"ngx-modialog": "^3.0.4",
|
||||
"node-sass": "^4.7.2",
|
||||
"postcss-cssnext": "^3.0.0",
|
||||
@ -84,6 +85,7 @@
|
||||
"rxjs": "^5.5.5",
|
||||
"sass-loader": "^6.0.3",
|
||||
"shelljs": "^0.7.8",
|
||||
"spinkit": "^1.2.5",
|
||||
"style-loader": "^0.18.2",
|
||||
"ts-helpers": "^1.1.2",
|
||||
"tslint": "^5.8.0",
|
||||
|
@ -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/widgets/embeddable", this._checkEmbeddable.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) {
|
||||
@ -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.
|
||||
|
||||
var type = req.params.type;
|
||||
|
@ -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
|
||||
* @param {string} scalarToken the scalar token to lookup
|
||||
|
@ -2,6 +2,8 @@
|
||||
</header>
|
||||
<main>
|
||||
<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>
|
||||
</main>
|
||||
<footer>
|
||||
|
@ -26,6 +26,9 @@ import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button
|
||||
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 { 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();
|
||||
|
||||
@ -41,6 +44,7 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
|
||||
BrowserAnimationsModule,
|
||||
ModalModule.forRoot(),
|
||||
BootstrapModalModule,
|
||||
BreadcrumbsModule,
|
||||
],
|
||||
declarations: [
|
||||
...WIDGET_CONFIGURATION_COMPONENTS,
|
||||
@ -48,6 +52,8 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
|
||||
HomeComponent,
|
||||
RiotComponent,
|
||||
IntegrationComponent,
|
||||
PageHeaderComponent,
|
||||
SpinnerComponent,
|
||||
ScalarCloseComponent,
|
||||
MyFilterPipe,
|
||||
GenericWidgetWrapperComponent,
|
||||
|
@ -8,7 +8,10 @@ import { GCalWidgetWrapperComponent } from "./widget_wrappers/gcal/gcal.componen
|
||||
|
||||
const routes: Routes = [
|
||||
{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/video", component: VideoWidgetWrapperComponent},
|
||||
{path: "widgets/jitsi", component: JitsiWidgetWrapperComponent},
|
||||
|
9
web/app/page-header/page-header.component.html
Normal file
9
web/app/page-header/page-header.component.html
Normal 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>
|
28
web/app/page-header/page-header.component.scss
Normal file
28
web/app/page-header/page-header.component.scss
Normal 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
|
||||
}
|
10
web/app/page-header/page-header.component.ts
Normal file
10
web/app/page-header/page-header.component.ts
Normal 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;
|
||||
}
|
@ -1,76 +1,57 @@
|
||||
<div id="wrapper">
|
||||
<my-scalar-close></my-scalar-close>
|
||||
<div *ngIf="error">
|
||||
<p class="text-danger">{{ error }}</p>
|
||||
</div>
|
||||
<div *ngIf="loading && !error">
|
||||
<p><i class="fa fa-circle-o-notch fa-spin"></i> Loading...</p>
|
||||
</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.
|
||||
|
||||
<my-page-header pageName="Dimension"></my-page-header>
|
||||
|
||||
<div class="page-content">
|
||||
<div *ngIf="isError">
|
||||
<div class="alert alert-danger">{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div class="alert alert-warning" *ngIf="integrations.length === 0 && isEncryptedRoom">
|
||||
<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="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 *ngIf="isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
|
||||
<!-- ------------------------ -->
|
||||
<!-- WIDGETS -->
|
||||
<!-- ------------------------ -->
|
||||
<h4 *ngIf="hasAnyOf('widget')">
|
||||
Widgets <i class="fa fa-question-circle text-info" style="font-size: 15px;" placement="bottom"
|
||||
ngbTooltip="Widgets add small apps to Riot, like Google Docs, Jitsi conferences, and YouTube videos"></i>
|
||||
</h4>
|
||||
<div class="integration-container">
|
||||
<my-integration *ngFor="let integration of integrations | myFilter:'type':'widget'"
|
||||
[integration]="integration"
|
||||
[roomId]="roomId"
|
||||
[scalarToken]="scalarToken"
|
||||
(updated)="updateIntegration(integration)"></my-integration>
|
||||
</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>
|
||||
|
||||
<!-- ------------------------ -->
|
||||
<!-- BOTS -->
|
||||
<!-- ------------------------ -->
|
||||
<h4 *ngIf="hasAnyOf('bot', 'complex-bot')">
|
||||
Bots <i class="fa fa-question-circle text-info" style="font-size: 15px;" placement="bottom"
|
||||
ngbTooltip="Bots can provide entertainment or some utility to your room"></i>
|
||||
</h4>
|
||||
<div class="integration-container">
|
||||
<my-integration *ngFor="let integration of integrations | myFilter:'type':'bot'"
|
||||
[integration]="integration"
|
||||
[roomId]="roomId"
|
||||
[scalarToken]="scalarToken"
|
||||
(updated)="updateIntegration(integration)"></my-integration>
|
||||
<my-integration *ngFor="let integration of integrations | myFilter:'type':'complex-bot'"
|
||||
[integration]="integration"
|
||||
[roomId]="roomId"
|
||||
[scalarToken]="scalarToken"
|
||||
(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>
|
||||
<!-- ------------------------ -->
|
||||
<!-- 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>
|
@ -1,20 +1,10 @@
|
||||
// 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 {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-left: 5px;
|
||||
.page-content {
|
||||
padding: 30px;
|
||||
}
|
@ -8,6 +8,12 @@ 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({
|
||||
selector: "my-riot",
|
||||
templateUrl: "./riot.component.html",
|
||||
@ -16,15 +22,18 @@ import { IntegrationComponent } from "../integration/integration.component";
|
||||
export class RiotComponent {
|
||||
@ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>;
|
||||
|
||||
public error: string;
|
||||
public integrations: Integration[] = [];
|
||||
public loading = true;
|
||||
public roomId: string;
|
||||
public scalarToken: string;
|
||||
public isEncryptedRoom = false;
|
||||
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,
|
||||
@ -35,27 +44,94 @@ export class RiotComponent {
|
||||
this.requestedScreen = params.screen;
|
||||
this.requestedIntegration = params.integ_id;
|
||||
|
||||
if (!params.scalar_token || !params.room_id) this.error = "Missing scalar token or room ID";
|
||||
else {
|
||||
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.checkScalarToken(params.scalar_token).then(isValid => {
|
||||
if (isValid) this.init();
|
||||
else this.error = "Invalid 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 => {
|
||||
this.error = "Unable to communicate with Dimension";
|
||||
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.isEncryptedRoom = payload.response;
|
||||
this.isRoomEncrypted = payload.response;
|
||||
return this.api.getIntegrations(this.roomId, this.scalarToken);
|
||||
}).then(integrations => {
|
||||
const supportedIntegrations = _.filter(integrations, i => IntegrationService.isSupported(i));
|
||||
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
|
||||
@ -65,20 +141,34 @@ export class RiotComponent {
|
||||
integration.supportsEncryptedRooms = true;
|
||||
}
|
||||
|
||||
if (this.isEncryptedRoom)
|
||||
this.integrations = _.filter(supportedIntegrations, i => i.supportsEncryptedRooms);
|
||||
else this.integrations = supportedIntegrations;
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.isLoading = false;
|
||||
|
||||
// HACK: We wait for the digest cycle so we actually have components to look at
|
||||
setTimeout(() => this.tryOpenConfigScreen(), 20);
|
||||
}).catch(err => {
|
||||
this.error = "Unable to communicate with Dimension";
|
||||
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) {
|
||||
integration.hasConfig = IntegrationService.hasConfig(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 = [];
|
||||
@ -126,29 +217,30 @@ export class RiotComponent {
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
integration.isEnabled = true;
|
||||
integration.isBroken = false;
|
||||
integration.isSupported = true;
|
||||
integration.notSupportedReason = null;
|
||||
}, error => {
|
||||
console.error(error);
|
||||
integration.bridgeError = error.message || error;
|
||||
integration.isEnabled = false;
|
||||
integration.isBroken = false;
|
||||
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 => {
|
||||
integration.isBroken = false;
|
||||
|
||||
if (!payload.response) {
|
||||
integration.isEnabled = false;
|
||||
return;
|
||||
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";
|
||||
}
|
||||
|
||||
integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
|
||||
}, (error) => {
|
||||
console.error(error);
|
||||
integration.isEnabled = false;
|
||||
integration.isBroken = true;
|
||||
integration.isSupported = false;
|
||||
integration.notSupportedReason = "Unable to query membership state for this bot";
|
||||
});
|
||||
}
|
||||
|
||||
@ -163,50 +255,19 @@ export class RiotComponent {
|
||||
}
|
||||
return payload.response.join_rule === requirement
|
||||
? 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":
|
||||
const processPayload = payload => {
|
||||
const response = <any>payload.response;
|
||||
if (response === true) return Promise.resolve();
|
||||
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 this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ export class ApiService {
|
||||
.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[]> {
|
||||
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface Integration {
|
||||
// These are from the server
|
||||
type: string;
|
||||
integrationType: string;
|
||||
userId: string;
|
||||
@ -6,13 +7,14 @@ export interface Integration {
|
||||
avatar: string;
|
||||
about: string; // nullable
|
||||
supportsEncryptedRooms: boolean;
|
||||
requirements: any; // nullable
|
||||
|
||||
// Set by us
|
||||
isEnabled: boolean;
|
||||
isBroken: boolean;
|
||||
hasConfig: boolean;
|
||||
requirements?: any; // nullable
|
||||
bridgeError: string; // nullable
|
||||
// These are set in the UI
|
||||
isSupported: boolean;
|
||||
notSupportedReason: string;
|
||||
hasAdditionalConfig: boolean;
|
||||
isEnabled: boolean; // for the flip-a-bit integrations
|
||||
isUpdating: boolean;
|
||||
}
|
||||
|
||||
export interface RSSIntegration extends Integration {
|
||||
|
6
web/app/spinner/spinner.component.html
Normal file
6
web/app/spinner/spinner.component.html
Normal 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>
|
2
web/app/spinner/spinner.component.scss
Normal file
2
web/app/spinner/spinner.component.scss
Normal 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";
|
9
web/app/spinner/spinner.component.ts
Normal file
9
web/app/spinner/spinner.component.ts
Normal 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 {
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
// 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';
|
||||
|
||||
body {
|
||||
background: #ddd !important;
|
||||
background: rgb(238, 238, 238) !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #222;
|
||||
color: #333;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
// HACK: Work around dialog not showing up
|
||||
|
Loading…
Reference in New Issue
Block a user