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",
|
"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",
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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},
|
||||||
|
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">
|
<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>
|
@ -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;
|
||||||
}
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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 {
|
||||||
|
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
|
// 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
|
||||||
|
Loading…
Reference in New Issue
Block a user