From 487f3bd3aad603fb5b932a34ca43f750d0b5e083 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 3 Feb 2022 12:27:24 +0100 Subject: [PATCH] Commit current state of noracle --- src/app/app.module.ts | 21 +- .../graph-view-page.component.css | 36 +++ .../graph-view-page.component.html | 104 +++----- .../graph-view-page.component.ts | 123 ++++++++- .../graph-view/graph-view.component.ts | 35 ++- .../graph-view/graph-view.service.ts | 6 +- .../after-login/after-login.component.ts | 8 +- src/app/navigation/nav/nav.component.html | 9 +- src/app/navigation/nav/nav.component.ts | 4 +- .../sidenav-list/sidenav-list.component.html | 6 +- .../sidenav-list/sidenav-list.component.ts | 8 +- src/app/shared/agent/agent.service.ts | 30 ++- .../shared/auth-guard/auth-guard.service.ts | 48 +--- .../authentication.service.spec.ts | 16 ++ .../authentication/authentication.service.ts | 70 +++++ .../question-vote/question-vote.service.ts | 4 +- src/app/shared/question/question.service.ts | 8 +- .../relation-vote/relation-vote.service.ts | 3 +- src/app/shared/relation/relation.service.ts | 8 +- src/app/shared/rest-data-model/space.ts | 1 + .../shared/rest-helper/rest-helper.service.ts | 24 +- src/app/shared/shared.module.ts | 2 +- src/app/shared/space/space.service.ts | 26 +- .../create-space/create-space.component.css | 5 + .../create-space/create-space.component.html | 6 + .../create-space/create-space.component.ts | 3 + src/app/space/space.module.ts | 9 +- .../subscribed-spaces-overview.component.css | 60 ++++- .../subscribed-spaces-overview.component.html | 105 +++----- .../subscribed-spaces-overview.component.ts | 241 +++--------------- src/assets/default-avatar.png | Bin 0 -> 10056 bytes src/environments/environment.ts | 5 +- 32 files changed, 571 insertions(+), 463 deletions(-) create mode 100644 src/app/shared/authentication/authentication.service.spec.ts create mode 100644 src/app/shared/authentication/authentication.service.ts create mode 100644 src/assets/default-avatar.png diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 333ffe2..3e98d4b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -23,6 +23,7 @@ import {MatProgressBarModule} from '@angular/material/progress-bar'; import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular'; import { environment } from 'src/environments/environment'; import { HttpRequestInterceptor } from './shared/http-request-interceptor/http-request-interceptor'; +import { AuthenticationService } from './shared/authentication/authentication.service'; function initializeKeycloak(keycloak: KeycloakService): () => Promise { return () => @@ -56,14 +57,15 @@ function initializeKeycloak(keycloak: KeycloakService): () => Promise { SpaceModule, GraphViewModule, RouterModule.forRoot([ - { path: 'welcome', component: WelcomePageComponent }, - { path: 'login', component: LoginPageComponent }, - { path: 'afterlogin', component: AfterLoginComponent }, - { path: 'myspaces', component: SubscribedSpacesOverviewComponent, canActivate: [AuthGuardService] }, - { path: 'spaces/create', component: CreateSpaceComponent, canActivate: [AuthGuardService] }, - { path: 'spaces/:spaceId', component: GraphViewPageComponent, canActivate: [AuthGuardService] }, - { path: '**', redirectTo: 'welcome' } -], { relativeLinkResolution: 'legacy' }), + { path: 'welcome', component: WelcomePageComponent }, + { path: 'login', component: LoginPageComponent }, + { path: 'afterlogin', component: AfterLoginComponent }, + { path: 'myspaces', component: SubscribedSpacesOverviewComponent, canActivate: [AuthGuardService] }, + { path: 'spaces/create', component: CreateSpaceComponent, canActivate: [AuthGuardService] }, + { path: 'spaces/:spaceId', component: GraphViewPageComponent, canActivate: [AuthGuardService] }, + { path: '**', redirectTo: 'welcome' } + ], + { relativeLinkResolution: 'legacy' }), HttpClientModule, KeycloakAngularModule, MatSidenavModule, @@ -79,7 +81,8 @@ function initializeKeycloak(keycloak: KeycloakService): () => Promise { { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true - } + }, + AuthenticationService ], bootstrap: [AppComponent] }) diff --git a/src/app/graph-view/graph-view-page/graph-view-page.component.css b/src/app/graph-view/graph-view-page/graph-view-page.component.css index ea62379..6d498e8 100644 --- a/src/app/graph-view/graph-view-page/graph-view-page.component.css +++ b/src/app/graph-view/graph-view-page/graph-view-page.component.css @@ -13,6 +13,7 @@ top: 80px; right: 10px; direction: rtl; + max-width: 290px; } .memberList ul { @@ -40,4 +41,39 @@ mat-icon { .graph-view { width: 100%; height: 100%; +} + +.mat-icon.share-icon { + height:32px !important; + width:32px !important; + font-size:32px !important; +} + +.recommendation-card { + position: fixed; + max-width: 300px; + margin: 24px; +} + +.example-header-image { + background-image: url('../../../assets/default-avatar.png'); + background-size: cover; +} + +.recommendation-link { + padding: 8px; + display: flex; + cursor: pointer; +} + +.recommendation-link:hover { + background-color: #F8F8FF; +} + +.img-avatar { + border-radius: 50%; + height: 32px; + width: 32px; + display: block; + object-fit: cover; } \ No newline at end of file diff --git a/src/app/graph-view/graph-view-page/graph-view-page.component.html b/src/app/graph-view/graph-view-page/graph-view-page.component.html index e6b547c..97c23e8 100644 --- a/src/app/graph-view/graph-view-page/graph-view-page.component.html +++ b/src/app/graph-view/graph-view-page/graph-view-page.component.html @@ -1,10 +1,33 @@ - -
+ + + Recommended For You + + + + + + + + + + + {{r.authorName}} + · + {{GetPastTime(r)}} + + {{r.question.text}} + + + + + +

Subscribing to space…

@@ -28,7 +51,7 @@
- + Participants @@ -43,69 +66,20 @@ Help
    -
  • chat_bubble_outline selected / chat_bubble_outline deselected -
  • -
  • chat_bubble_outline rated positively / chat_bubble_outline rated negatively
  • -
  • >>> follow-up question
  • -
  • ―― related question
  • -
  • ―――――――――――――――――――――
  • -
  • More questions? noracle@dbis.rwth-aachen.de
  • +
  • chat_bubble_outlineselected
  • +
  • chat_bubble_outlinedeselected
  • +
  • chat_bubble_outlinerated positively
  • +
  • chat_bubble_outlinerated negatively
  • +
  • >>>follow-up question
  • +
  • ――related question
  • +
  • ―――――――――――――――――――――
  • +
  • More questions?
  • +
  • noracle@dbis.rwth-aachen.de
+
- - - - - - - - diff --git a/src/app/graph-view/graph-view-page/graph-view-page.component.ts b/src/app/graph-view/graph-view-page/graph-view-page.component.ts index b561c21..d2cabba 100644 --- a/src/app/graph-view/graph-view-page/graph-view-page.component.ts +++ b/src/app/graph-view/graph-view-page/graph-view-page.component.ts @@ -1,29 +1,59 @@ -import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; +import {ChangeDetectorRef, Component, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core'; import {GraphInteractionMode} from '../graph-view/graph-data-model/graph-interaction-mode.enum'; -import {ActivatedRoute, Router} from '@angular/router'; -import {Subscription} from 'rxjs'; +import {ActivatedRoute, NavigationExtras, Router} from '@angular/router'; +import {BehaviorSubject, Subscription} from 'rxjs'; import {MyspacesService} from '../../shared/myspaces/myspaces.service'; import { GraphViewService } from '../graph-view/graph-view.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Clipboard } from '@angular/cdk/clipboard'; +import { SpaceService } from 'src/app/shared/space/space.service'; +import { Space } from 'src/app/shared/rest-data-model/space'; +import { RecommendationService } from 'src/app/shared/recommendation/recommendation.service'; +import { RecommenderQuestion } from 'src/app/shared/rest-data-model/recommender-question'; +import { Question } from 'src/app/shared/rest-data-model/question'; +import { AgentService } from 'src/app/shared/agent/agent.service'; @Component({ selector: 'dnor-graph-view-page', templateUrl: './graph-view-page.component.html', styleUrls: ['./graph-view-page.component.css'] }) -export class GraphViewPageComponent implements OnInit, OnDestroy { +export class GraphViewPageComponent implements OnInit, OnDestroy, OnChanges { public subscriptionInProgress = false; public interactionMode = GraphInteractionMode.SelectAndNavigate; public height = 800; public width = 1100; public spaceId = '1'; + public space: Space; public selectedQuestions; public spaceMembers = []; public loading = false; + public recommenderQuestions: RecommenderQuestion[]; + public selectedRecommenderQuestion: []; + public recommendationsLoaded = false; private subscription: Subscription = new Subscription(); - constructor(private activatedRoute: ActivatedRoute, private router: Router, private ref: ChangeDetectorRef, - private myspacesService: MyspacesService, private graphViewService: GraphViewService) {} + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private ref: ChangeDetectorRef, + private myspacesService: MyspacesService, + private graphViewService: GraphViewService, + private snackBar: MatSnackBar, + private clipboard: Clipboard, + private spaceService: SpaceService, + private recommendationService: RecommendationService, + private agentService: AgentService) {} + + ngOnChanges(changes: SimpleChanges): void {} + + copyInviteUrl() { + let url = window.location.href; + url = url.substring(0, url.indexOf('/spaces')) + `/spaces/${this.space.spaceId}?pw=${this.space.spaceSecret}`; + this.clipboard.copy(url); + this.snackBar.open('Copied invitation link to clipboard. Paste to share with friends!', 'Ok'); + } ngOnInit() { this.subscription.add(this.activatedRoute.params.subscribe((params) => { @@ -31,6 +61,13 @@ export class GraphViewPageComponent implements OnInit, OnDestroy { this.myspacesService.getSubscribersObservable(this.spaceId).subscribe(subs => { this.spaceMembers = subs.map(sub => sub.name); }); + this.myspacesService.getMySpaces().then(res => { + if (res.find(s => s.space.spaceId === this.spaceId)) { + this.spaceService.getSpace(this.spaceId).then((space: Space) => { + this.space = space; + }); + } + }); })); this.subscription.add(this.activatedRoute.queryParams.subscribe((queryParams) => { const pw = queryParams['pw']; @@ -38,11 +75,11 @@ export class GraphViewPageComponent implements OnInit, OnDestroy { this.subscriptionInProgress = true; this.myspacesService.subscribeToSpace(this.spaceId, pw).then(() => { const qp = queryParams['sq'] !== undefined ? {sq: queryParams['sq']} : {}; - this.router.navigate([], {queryParams: qp, replaceUrl: true}).then(() => + this.router.navigate([], {queryParams: qp, replaceUrl: true}).then(() => { + this.ngOnInit(); // reload component this.subscriptionInProgress = false - ); + }); }); - } const q = queryParams['sq']; if (q === undefined) { @@ -51,12 +88,76 @@ export class GraphViewPageComponent implements OnInit, OnDestroy { this.selectedQuestions = JSON.parse(q); } })); + this.width = window.innerWidth * 0.99; + this.height = window.innerHeight * 0.92; + this.subscription.add(this.graphViewService.loading.subscribe((loading) => { this.loading = loading; this.ref.detectChanges(); })); - this.width = window.innerWidth * 0.99; - this.height = window.innerHeight * 0.91; + + this.loadRecommendations(); + } + + loadRecommendations(): void { + this.recommendationsLoaded = false; + this.agentService.getAgent().then((agent) => { + this.recommendationService.getRecommendedQuestionsForSpace(agent.agentid, this.spaceId) + .then((res: RecommenderQuestion[]) => { + this.recommenderQuestions = res; + }).catch(() => { + console.error("error while getting recommendations..."); + }).finally(() => { + this.recommendationsLoaded = true; + }) + }); + } + + recClicked(rq: RecommenderQuestion): void { + //let questionIds = r.questionNeighbourIds; + let questionIds = []; + questionIds.push(rq.question.questionId); + let navigationExtras: NavigationExtras = { + queryParams: { + sq: JSON.stringify(questionIds) + } + } + this.graphViewService.recommenderSubject.next(questionIds); + this.router.navigate(['/spaces', rq.question.spaceId], navigationExtras); + } + + GetPastTime(rq: RecommenderQuestion): string { + // TODO: This is way to complicated! + let date = new Date(rq.question.timestampCreated); + let dateNow = new Date(); + + if (date.getFullYear() === dateNow.getFullYear()) { + if (date.getMonth() === dateNow.getMonth()) { + if (date.getDay() === dateNow.getDay()) { + if (date.getHours() === dateNow.getHours()) { + if (date.getMinutes() === dateNow.getMinutes()) { + // show seconds + return (dateNow.getSeconds() - date.getSeconds()) + ' second(s) ago'; + } else { + // show minutes + return (dateNow.getMinutes() - date.getMinutes()) + ' minute(s) ago'; + } + } else { + // show hours + return (dateNow.getHours() - date.getHours()) + ' hour(s) ago'; + } + } else { + // show days + return (dateNow.getDay() - date.getDay()) + ' day(s) ago'; + } + } else { + // show month + return (dateNow.getMonth() - date.getMonth()) + ' month(s) ago'; + } + } else { + // show years + return (dateNow.getFullYear() - date.getFullYear()) + 'year(s) ago'; + } } ngOnDestroy() { diff --git a/src/app/graph-view/graph-view/graph-view.component.ts b/src/app/graph-view/graph-view/graph-view.component.ts index dba58c8..6012fe2 100644 --- a/src/app/graph-view/graph-view/graph-view.component.ts +++ b/src/app/graph-view/graph-view/graph-view.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild} from '@angular/core'; import {ForceLink, Simulation} from 'd3-force'; import {GraphNode} from './graph-data-model/graph-node'; import {Edge} from './graph-data-model/edge'; @@ -11,7 +11,7 @@ import {NodeInteractionBehavior} from './interaction-behaviors/node-interaction- import {AddChildNodeBehavior} from './interaction-behaviors/add-child-node-behavior'; import {EditQuestionBehavior} from './interaction-behaviors/edit-question-behavior'; import {AddRelationBehavior} from './interaction-behaviors/add-relation-behavior'; -import {Subscription} from 'rxjs'; +import {BehaviorSubject, Subscription} from 'rxjs'; import {AgentService} from '../../shared/agent/agent.service'; import {QuestionVoteService} from '../../shared/question-vote/question-vote.service'; import {EdgeInteractionBehavior} from './interaction-behaviors/edge-interaction-behavior'; @@ -29,11 +29,11 @@ import { D3DragEvent, D3ZoomEvent } from 'd3'; }) export class GraphViewComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { @ViewChild('d3root') private d3Root: ElementRef; - @Input('height') public height = 800; - @Input('width') public width = 1100; - @Input('interactionMode') private interactionMode: GraphInteractionMode; - @Input('spaceId') private spaceId = 'dummy'; - @Input('selectedQuestions') private selectedQuestions: string[]; + @Input() height = 800; + @Input() width = 1100; + @Input() private interactionMode: GraphInteractionMode; + @Input() private spaceId = 'dummy'; + @Input() private selectedQuestions: string[]; private loadedSpaceId; private network: Network; @@ -44,13 +44,22 @@ export class GraphViewComponent implements OnInit, OnChanges, OnDestroy, AfterVi private d3Sim: Simulation; private activatedInteractionMode: GraphInteractionMode; private updateSubscription: Subscription; + private recommendingSubscription: Subscription; constructor(private graphViewService: GraphViewService, private agentService: AgentService, private dialog: MatDialog) { this.transform = d3.zoomIdentity; } - ngOnInit(): void {} + ngOnInit(): void { + this.recommendingSubscription = this.graphViewService.recommending.subscribe(questions => { + if (questions !== undefined && questions.length > 0) { + this.selectedQuestions = questions; + this.initData(); + this.initVisualization(); + } + }); + } ngAfterViewInit(): void { this.canvas = this.d3Root.nativeElement; @@ -59,6 +68,7 @@ export class GraphViewComponent implements OnInit, OnChanges, OnDestroy, AfterVi this.d3Sim = d3.forceSimulation() as Simulation; this.d3Sim.force('link', d3.forceLink().id((n, i, d) => n.id)); this.d3Sim.force('charge', d3.forceManyBody()); + this.d3Sim.force('center', d3.forceCenter(this.width / 2, this.height / 2)); this.d3Sim.force('collide', d3.forceCollide((node: GraphNode) => (node as GraphNode).radius * 1.2)); @@ -70,19 +80,26 @@ export class GraphViewComponent implements OnInit, OnChanges, OnDestroy, AfterVi ngOnDestroy(): void { this.updateSubscription.unsubscribe(); + this.recommendingSubscription.unsubscribe(); this.graphViewService.initServiceForSpace(null); } - ngOnChanges(): void { + ngOnChanges(changes: SimpleChanges): void { if (this.d3Sim) { + if (changes['selectedQuestions']) { + this.selectedQuestions = changes['selectedQuestions'].currentValue; + } + if (this.spaceId !== this.loadedSpaceId) { this.initData(); this.initVisualization(); this.updateInteractionMode(); } + this.network.getNodes().forEach((node) => { node.isSelected = (this.selectedQuestions.indexOf(node.id) !== -1); }); + // visual update of selection this.updateInteractionMode(); } } diff --git a/src/app/graph-view/graph-view/graph-view.service.ts b/src/app/graph-view/graph-view/graph-view.service.ts index 98a0d98..d7c018e 100644 --- a/src/app/graph-view/graph-view/graph-view.service.ts +++ b/src/app/graph-view/graph-view/graph-view.service.ts @@ -29,9 +29,11 @@ export class GraphViewService { private observedQuestionIds: string[] = []; private updateSubject = new Subject(); private loadingSubject = new BehaviorSubject(false); + public recommenderSubject = new BehaviorSubject([]); - public loading = this.loadingSubject.asObservable(); public update = this.updateSubject.asObservable(); + public loading = this.loadingSubject.asObservable(); + public recommending = this.recommenderSubject.asObservable(); constructor(private questionService: QuestionService, private relationService: RelationService, private questionVoteService: QuestionVoteService, private relationVoteService: RelationVoteService, @@ -310,7 +312,7 @@ export class GraphViewService { private schedulePolling() { if (!this.isPollScheduled) { this.isPollScheduled = true; - window.setTimeout(() => this.poll(), 20000); + window.setTimeout(() => this.poll(), 12000); } } diff --git a/src/app/login/after-login/after-login.component.ts b/src/app/login/after-login/after-login.component.ts index cc4d986..4f0db32 100644 --- a/src/app/login/after-login/after-login.component.ts +++ b/src/app/login/after-login/after-login.component.ts @@ -1,8 +1,8 @@ import {Component, OnInit} from '@angular/core'; -import {AuthGuardService} from '../../shared/auth-guard/auth-guard.service'; import {Router} from '@angular/router'; import {AgentService} from '../../shared/agent/agent.service'; import { KeycloakService } from 'keycloak-angular'; +import { AuthenticationService } from 'src/app/shared/authentication/authentication.service'; @Component({ selector: 'dnor-after-login', @@ -10,12 +10,12 @@ import { KeycloakService } from 'keycloak-angular'; }) export class AfterLoginComponent implements OnInit { - constructor(authGuardService: AuthGuardService, agentService: AgentService, router: Router, protected readonly keycloak: KeycloakService) { + constructor(authService: AuthenticationService, agentService: AgentService, router: Router, protected readonly keycloak: KeycloakService) { this.keycloak.isLoggedIn().then((loggedIn) => { if (loggedIn) { agentService.getAgent().then((agent) => - agentService.putAgentName(agent.agentid, authGuardService.getUserName())); - const lastRouteRequested = authGuardService.getLastRouteRequested(); + agentService.putAgentName(agent.agentid, authService.getUserName())); + const lastRouteRequested = authService.getLastRouteRequested(); if (lastRouteRequested !== undefined) { router.navigate([lastRouteRequested.url], { queryParams: lastRouteRequested.queryParams, diff --git a/src/app/navigation/nav/nav.component.html b/src/app/navigation/nav/nav.component.html index 01879a8..ea19333 100644 --- a/src/app/navigation/nav/nav.component.html +++ b/src/app/navigation/nav/nav.component.html @@ -1,16 +1,17 @@ -
+
+
Noracle
diff --git a/src/app/navigation/nav/nav.component.ts b/src/app/navigation/nav/nav.component.ts index c7d87a8..0ae285d 100644 --- a/src/app/navigation/nav/nav.component.ts +++ b/src/app/navigation/nav/nav.component.ts @@ -2,6 +2,7 @@ import {Component, EventEmitter, OnInit, Output} from '@angular/core'; import {AuthGuardService} from '../../shared/auth-guard/auth-guard.service'; import { KeycloakService } from 'keycloak-angular'; import { environment } from 'src/environments/environment'; +import { AuthenticationService } from 'src/app/shared/authentication/authentication.service'; @Component({ @@ -15,13 +16,14 @@ export class NavComponent implements OnInit { public sidenavToggle = new EventEmitter(); constructor(public authGuardService: AuthGuardService, + public authService: AuthenticationService, protected readonly keycloak: KeycloakService) { } ngOnInit() {} logout() { - if (this.authGuardService.isAuthorized) { + if (this.authService.isAuthorized) { this.authGuardService.logoff(); this.keycloak.logout(environment.redirectUrl); } diff --git a/src/app/navigation/sidenav-list/sidenav-list.component.html b/src/app/navigation/sidenav-list/sidenav-list.component.html index 51a07e8..37419f3 100644 --- a/src/app/navigation/sidenav-list/sidenav-list.component.html +++ b/src/app/navigation/sidenav-list/sidenav-list.component.html @@ -9,7 +9,7 @@ workspaces My Spaces @@ -17,12 +17,12 @@ login Login - + logout Logout diff --git a/src/app/navigation/sidenav-list/sidenav-list.component.ts b/src/app/navigation/sidenav-list/sidenav-list.component.ts index 564a48c..1096088 100644 --- a/src/app/navigation/sidenav-list/sidenav-list.component.ts +++ b/src/app/navigation/sidenav-list/sidenav-list.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; import { AuthGuardService } from 'src/app/shared/auth-guard/auth-guard.service'; +import { AuthenticationService } from 'src/app/shared/authentication/authentication.service'; import { environment } from 'src/environments/environment'; @Component({ @@ -13,7 +14,10 @@ export class SidenavListComponent implements OnInit { @Output() public closeSidenav = new EventEmitter(); - constructor(public authGuardService: AuthGuardService, public keycloak: KeycloakService) { } + constructor( + public authGuardService: AuthGuardService, + public authService: AuthenticationService, + public keycloak: KeycloakService) {} ngOnInit(): void {} @@ -22,7 +26,7 @@ export class SidenavListComponent implements OnInit { } logout() { - if (this.authGuardService.isAuthorized) { + if (this.authService.isAuthorized) { this.authGuardService.logoff(); this.keycloak.logout(environment.redirectUrl); } diff --git a/src/app/shared/agent/agent.service.ts b/src/app/shared/agent/agent.service.ts index ce0383d..bc4bd46 100644 --- a/src/app/shared/agent/agent.service.ts +++ b/src/app/shared/agent/agent.service.ts @@ -1,26 +1,32 @@ import {Injectable} from '@angular/core'; import {RestHelperService} from '../rest-helper/rest-helper.service'; import {SpaceSubscription} from '../rest-data-model/spacesubscription'; +import { catchError, shareReplay } from 'rxjs/operators'; +import { EMPTY, Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; @Injectable() export class AgentService { - private cachedAgent; + private cachedAgent: {agentid: string}; + private cache: Observable private cachedAgentNames: Map = new Map(); constructor(private restHelperService: RestHelperService) { } - public getAgent(): Promise<{ agentid: string }> { - if (this.cachedAgent !== undefined) { - return Promise.resolve(this.cachedAgent); + public getAgent(): Promise { + if (this.cache) { + return this.cache.toPromise(); } else { - return this.restHelperService.getCurrentAgent().then((res) => { - this.cachedAgent = res; - return this.cachedAgent; - }).catch(error => { - console.error(error); - }) + this.cache = this.restHelperService.getCurrentAgentLocal().pipe( + shareReplay(1), + catchError(err => { + delete this.cache; + return EMPTY; + }) + ) + return this.cache.toPromise(); } } @@ -60,7 +66,9 @@ export class AgentService { .then(res => { // TODO: Hacky hack, needs to be fixed by the backend! let location: string = res.headers.get('location'); - location = location.replace("http", "https"); + if (environment.production) { + location = location.replace("http", "https"); + } return this.restHelperService.getAbsoulte(location) .then((res2: SpaceSubscription) => res2); }); diff --git a/src/app/shared/auth-guard/auth-guard.service.ts b/src/app/shared/auth-guard/auth-guard.service.ts index e468793..9f39328 100644 --- a/src/app/shared/auth-guard/auth-guard.service.ts +++ b/src/app/shared/auth-guard/auth-guard.service.ts @@ -1,59 +1,27 @@ import {Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; -import { KeycloakService } from 'keycloak-angular'; -import { KeycloakLoginOptions } from 'keycloak-js'; +import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; +import { AuthenticationService } from '../authentication/authentication.service'; @Injectable() export class AuthGuardService implements CanActivate { - public isAuthorized = false; - private userName: string; - private keycloakLoginOptions: KeycloakLoginOptions = { - redirectUri: environment.redirectUrl - } - - constructor(private router: Router, protected readonly keycloak: KeycloakService) { - this.keycloak.isLoggedIn().then((loggedIn: boolean) => { - this.isAuthorized = loggedIn; - this.userName = this.keycloak.getUsername(); - }); + constructor(private authService: AuthenticationService) { } async login() { - await this.keycloak.login(this.keycloakLoginOptions); - this.isAuthorized = await this.keycloak.isLoggedIn(); - } - - getUserName(): string { - return this.userName; + return this.authService.login(); } - getLastRouteRequested() { - const cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)rejectedPath\s*\=\s*([^;]*).*$)|^.*$/, '$1'); - document.cookie = 'rejectedPath=; expires=' + new Date(0).toUTCString(); - return cookieValue !== undefined && cookieValue.length > 0 ? JSON.parse(decodeURIComponent(cookieValue)) : undefined; + getUserName() { + return this.authService.getUserName(); } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable | Promise { - return this.keycloak.isLoggedIn().then((loggedIn: boolean) => { - if (loggedIn) { - this.isAuthorized = true; - return this.isAuthorized; - } else { - document.cookie = 'rejectedPath=' + encodeURIComponent(JSON.stringify({ - url: route.url.map((v) => v.path).reduce((prev, cur) => prev + '/' + cur, ''), - queryParams: route.queryParams - })) + '; path=/; expires=' + new Date(Date.now() + ((3 * 60 * 1000))).toUTCString(); - this.router.navigate(['/login'], {replaceUrl: true}); - this.isAuthorized = false; - return this.isAuthorized; - } - }); + return this.authService.checkAuthorized(route); } logoff(): void { - this.isAuthorized = false; + return this.authService.logoff(); } } diff --git a/src/app/shared/authentication/authentication.service.spec.ts b/src/app/shared/authentication/authentication.service.spec.ts new file mode 100644 index 0000000..5c98867 --- /dev/null +++ b/src/app/shared/authentication/authentication.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthenticationService } from './authentication.service'; + +describe('AuthenticationService', () => { + let service: AuthenticationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthenticationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/authentication/authentication.service.ts b/src/app/shared/authentication/authentication.service.ts new file mode 100644 index 0000000..497dd8f --- /dev/null +++ b/src/app/shared/authentication/authentication.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; +import { KeycloakLoginOptions } from 'keycloak-js'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { AgentService } from '../agent/agent.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { + + public isAuthorized = false; + private userName: string; + + private keycloakLoginOptions: KeycloakLoginOptions = { + redirectUri: environment.redirectUrl + } + + constructor(private router: Router, protected readonly keycloak: KeycloakService, private agentService: AgentService) { + this.keycloak.isLoggedIn().then((loggedIn: boolean) => { + this.isAuthorized = loggedIn; + if (loggedIn) { + this.userName = this.keycloak.getUsername(); + this.agentService.getAgent(); + } + }); + } + + async login() { + await this.keycloak.login(this.keycloakLoginOptions); + this.isAuthorized = await this.keycloak.isLoggedIn(); + if (this.isAuthorized) { + + } + } + + getUserName(): string { + return this.userName; + } + + getLastRouteRequested() { + const cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)rejectedPath\s*\=\s*([^;]*).*$)|^.*$/, '$1'); + document.cookie = 'rejectedPath=; expires=' + new Date(0).toUTCString(); + return cookieValue !== undefined && cookieValue.length > 0 ? JSON.parse(decodeURIComponent(cookieValue)) : undefined; + } + + checkAuthorized(route: ActivatedRouteSnapshot): boolean | Observable | Promise { + return this.keycloak.isLoggedIn().then((loggedIn: boolean) => { + if (loggedIn) { + this.isAuthorized = true; + return this.isAuthorized; + } else { + document.cookie = 'rejectedPath=' + encodeURIComponent(JSON.stringify({ + url: route.url.map((v) => v.path).reduce((prev, cur) => prev + '/' + cur, ''), + queryParams: route.queryParams + })) + '; path=/; expires=' + new Date(Date.now() + ((3 * 60 * 1000))).toUTCString(); + + this.router.navigate(['/login'], {replaceUrl: true}); + this.isAuthorized = false; + return this.isAuthorized; + } + }); + } + + logoff(): void { + this.isAuthorized = false; + } +} diff --git a/src/app/shared/question-vote/question-vote.service.ts b/src/app/shared/question-vote/question-vote.service.ts index f8c9193..3b25f16 100644 --- a/src/app/shared/question-vote/question-vote.service.ts +++ b/src/app/shared/question-vote/question-vote.service.ts @@ -1,7 +1,6 @@ import {Injectable} from '@angular/core'; import {RestHelperService} from '../rest-helper/rest-helper.service'; import {QuestionVote} from '../rest-data-model/question-vote'; -import {Vote} from '../rest-data-model/vote'; @Injectable() export class QuestionVoteService { @@ -10,7 +9,8 @@ export class QuestionVoteService { } public getQuestionVotes(spaceId: string, questionId: string) { - return this.restHelperService.get(`/spaces/${spaceId}/questions/${questionId}/votes`).then((res: QuestionVote[]) => { + return this.restHelperService.get(`/spaces/${spaceId}/questions/${questionId}/votes`) + .then((res: QuestionVote[]) => { return res; }); } diff --git a/src/app/shared/question/question.service.ts b/src/app/shared/question/question.service.ts index cd2d0a0..31b7532 100644 --- a/src/app/shared/question/question.service.ts +++ b/src/app/shared/question/question.service.ts @@ -1,4 +1,5 @@ import {Injectable} from '@angular/core'; +import { environment } from 'src/environments/environment'; import {Question} from '../rest-data-model/question'; import {RestHelperService} from '../rest-helper/rest-helper.service'; @@ -10,7 +11,8 @@ export class QuestionService { public getQuestionsOfSpace(spaceId: string): Promise { // TODO: proper pagination - return this.restHelperService.get(`/spaces/${spaceId}/questions?limit=1000`).then((res: Question[]) => { + return this.restHelperService.get(`/spaces/${spaceId}/questions?limit=1000`) + .then((res: Question[]) => { return res; }); } @@ -19,7 +21,9 @@ export class QuestionService { return this.restHelperService.post(`/spaces/${spaceId}/questions`, question).then((res) => { // TODO: Hacky hack, needs to be fixed by the backend! let location: string = res.headers.get('location'); - location = location.replace("http", "https"); + if (environment.production) { + location = location.replace("http", "https"); + } return this.restHelperService.getAbsoulte(location) .then((r: Question) => { return r; diff --git a/src/app/shared/relation-vote/relation-vote.service.ts b/src/app/shared/relation-vote/relation-vote.service.ts index fbe31be..235bc12 100644 --- a/src/app/shared/relation-vote/relation-vote.service.ts +++ b/src/app/shared/relation-vote/relation-vote.service.ts @@ -9,7 +9,8 @@ export class RelationVoteService { } public getRelationVotes(spaceId: string, relationId: string) { - return this.restHelperService.get(`/spaces/${spaceId}/relations/${relationId}/votes`).then((res: RelationVote[]) => { + return this.restHelperService.get(`/spaces/${spaceId}/relations/${relationId}/votes`) + .then((res: RelationVote[]) => { return res; }); } diff --git a/src/app/shared/relation/relation.service.ts b/src/app/shared/relation/relation.service.ts index 7135888..5e7fac4 100644 --- a/src/app/shared/relation/relation.service.ts +++ b/src/app/shared/relation/relation.service.ts @@ -1,4 +1,5 @@ import {Injectable} from '@angular/core'; +import { environment } from 'src/environments/environment'; import {Relation} from '../rest-data-model/relation'; import {RestHelperService} from '../rest-helper/rest-helper.service'; @@ -10,7 +11,8 @@ export class RelationService { public getRelationsOfSpace(id): Promise { // TODO: proper pagination - return this.restHelperService.get(`/spaces/${id}/relations?limit=1000`).then((res: Relation[]) => { + return this.restHelperService.get(`/spaces/${id}/relations?limit=1000`) + .then((res: Relation[]) => { return res; }); } @@ -24,7 +26,9 @@ export class RelationService { return this.restHelperService.post(`/spaces/${spaceId}/relations`, relation).then((res) => { // TODO: Hacky hack, needs to be fixed by the backend! let location: string = res.headers.get('location'); - location = location.replace("http", "https"); + if (environment.production) { + location = location.replace("http", "https"); + } return this.restHelperService.getAbsoulte(location) .then((r: Relation) => r); }); diff --git a/src/app/shared/rest-data-model/space.ts b/src/app/shared/rest-data-model/space.ts index b9eaf65..429c6bf 100644 --- a/src/app/shared/rest-data-model/space.ts +++ b/src/app/shared/rest-data-model/space.ts @@ -7,4 +7,5 @@ export class Space { spaceOwnerId: string; spaceReaderGroupId: string; spaceSecret: string; + private: boolean; } diff --git a/src/app/shared/rest-helper/rest-helper.service.ts b/src/app/shared/rest-helper/rest-helper.service.ts index f12801b..c461a09 100644 --- a/src/app/shared/rest-helper/rest-helper.service.ts +++ b/src/app/shared/rest-helper/rest-helper.service.ts @@ -4,6 +4,7 @@ import {Router} from '@angular/router'; import {environment} from '../../../environments/environment'; import {retry} from 'rxjs/operators'; import {KeycloakService } from 'keycloak-angular'; +import { Observable } from 'rxjs'; const HOST_URLS = environment.hostUrls; @@ -23,7 +24,9 @@ export class RestHelperService { try { const res = await this.http.get(this.getBaseURL() + path, { headers: this.getHeaders() } - ).pipe(retry(3)).toPromise(); + ) + //.pipe(retry(3)) + .toPromise(); return res; } catch (error) { if (error.status === 401) { @@ -34,22 +37,31 @@ export class RestHelperService { } public getAbsoulte(absolutePath: string): Promise { - return this.http.get(absolutePath, {headers: this.getHeaders()}).pipe(retry(3)).toPromise(); + return this.http.get(absolutePath, {headers: this.getHeaders()}) + //.pipe(retry(3)) + .toPromise(); } public put(path: string, body: any): Promise { return this.http.put(this.getBaseURL() + path, body, {headers: this.getHeaders()} - ).pipe(retry(3)).toPromise(); + ) + .toPromise(); } public post(path: string, body: any): Promise { - return this.http.post(this.getBaseURL() + path, body, { headers: this.getHeaders(), observe: 'response' }).pipe(retry(3)).toPromise(); + return this.http.post(this.getBaseURL() + path, body, { headers: this.getHeaders(), observe: 'response' }) + .toPromise(); } - public getCurrentAgent(): Promise { - return this.http.get(this.getCoreBaseURL() + '/currentagent', {headers: this.getHeaders()}).pipe(retry(3)).toPromise(); + public getCurrentAgent(): Observable { + return this.http.get(this.getCoreBaseURL() + '/currentagent', {headers: this.getHeaders()}); + //return this.http.get(this.getCoreBaseURL() + '/auth/login', {headers: this.getHeaders()}); + } + + public getCurrentAgentLocal(): Observable { + return this.http.get(this.getBaseURL() + '/mainAgent', {headers: this.getHeaders()}) } private getHeaders(): HttpHeaders { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 36623fb..6d1f3b1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -26,7 +26,7 @@ import {RecommendationService} from './recommendation/recommendation.service'; MyspacesService, QuestionVoteService, RelationVoteService, - RecommendationService + RecommendationService, ] }) export class SharedModule { diff --git a/src/app/shared/space/space.service.ts b/src/app/shared/space/space.service.ts index dae6876..f2e5e71 100644 --- a/src/app/shared/space/space.service.ts +++ b/src/app/shared/space/space.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {RestHelperService} from '../rest-helper/rest-helper.service'; import {Space} from '../rest-data-model/space'; import {SpaceSubscriber} from '../rest-data-model/spacesubscriber'; -import { HttpResponse } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; @Injectable() export class SpaceService { @@ -11,8 +11,7 @@ export class SpaceService { } public getSpace(id): Promise { - return this.restHelperService.get(`/spaces/${id}`) - .then((res: Space) => { return res; }); + return this.restHelperService.get(`/spaces/${id}`).then((res: Space) => { return res; }); } public postSpace(space: Space): Promise { @@ -20,7 +19,9 @@ export class SpaceService { .then((res) => { // TODO: Hacky hack, needs to be fixed by the backend! let location: string = res.headers.get('location'); - location = location.replace("http", "https"); + if (environment.production) { + location = location.replace("http", "https"); + } return this.restHelperService.getAbsoulte(location) .then((r: Space) => { return r; @@ -30,10 +31,19 @@ export class SpaceService { }) } - public getSpaceSubscribers(id): Promise { - return this.restHelperService.get(`/spaces/${id}/subscribers`).then((res: SpaceSubscriber[]) => { - return res; - }); + public getSpaceSubscribers(spaceId: string): Promise { + // return this.restHelperService.get(`/spaces/${id}/subscribers`).then((res: SpaceSubscriber[]) => { + // return res; + // }); + return this.restHelperService.get(`/spaces/${spaceId}/subscribers`); + } + + public getPublicSpaces(): Promise { + return this.restHelperService.get(`/spaces/public`); + } + + public getSpaceVoteCount(spaceId: string): Promise { + return this.restHelperService.get(`/spaces/${spaceId}/votecount`); } } diff --git a/src/app/space/create-space/create-space.component.css b/src/app/space/create-space/create-space.component.css index 076fe67..d73baf3 100644 --- a/src/app/space/create-space/create-space.component.css +++ b/src/app/space/create-space/create-space.component.css @@ -1,4 +1,9 @@ .container { width: 100%; padding: 10px; +} + +mat-icon { + height: 100%; + vertical-align: bottom; } \ No newline at end of file diff --git a/src/app/space/create-space/create-space.component.html b/src/app/space/create-space/create-space.component.html index 32f6660..3f361ca 100644 --- a/src/app/space/create-space/create-space.component.html +++ b/src/app/space/create-space/create-space.component.html @@ -17,6 +17,12 @@ maxlength="140" required>

+

+ + Private + + help_outline +

diff --git a/src/app/space/create-space/create-space.component.ts b/src/app/space/create-space/create-space.component.ts index 85d11dd..8cda1e3 100644 --- a/src/app/space/create-space/create-space.component.ts +++ b/src/app/space/create-space/create-space.component.ts @@ -17,10 +17,13 @@ export class CreateSpaceComponent { public question = new Question(); public loadingCreateSpace = false; + public tooltipText = "If you set this space to private, only users with an invitation link are permitted to join the space." + constructor(private spaceService: SpaceService, private questionService: QuestionService, private myspacesService: MyspacesService, private router: Router) { this.space.name = ''; this.question.text = ''; + this.space.private = false; } createSpace(): void { diff --git a/src/app/space/space.module.ts b/src/app/space/space.module.ts index 1568d0c..85b2797 100644 --- a/src/app/space/space.module.ts +++ b/src/app/space/space.module.ts @@ -15,7 +15,9 @@ import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { FlexLayoutModule } from '@angular/flex-layout'; - +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatTooltipModule } from '@angular/material/tooltip'; @NgModule({ imports: [ @@ -34,7 +36,10 @@ import { FlexLayoutModule } from '@angular/flex-layout'; MatListModule, MatMenuModule, MatProgressSpinnerModule, - FlexLayoutModule + FlexLayoutModule, + ClipboardModule, + MatCheckboxModule, + MatTooltipModule ], declarations: [CreateSpaceComponent, SubscribedSpacesOverviewComponent], exports: [CreateSpaceComponent, SubscribedSpacesOverviewComponent] diff --git a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.css b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.css index 87285ea..ab4b7e2 100755 --- a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.css +++ b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.css @@ -1,7 +1,65 @@ .container { padding: 10px; + height: 100%; + margin: 0 auto; + max-width: 1264px; + width: 100%; + position: relative; +} + +.main-container { + width: calc(100% - 300px - 24px); +} + +.public-space { + display: flex; +} + +.side-container { + width: 300px; + margin-left: 24px; +} + +.public-space-relation-count { + min-width: 40px; + /*background-color: #3F51B5;*/ + background-color: hsl(140deg 27% 49%); + color: white; + padding: 3px 5px; + text-align: center; + border-radius: 2px; + font-size: 90%; + height: auto; + box-sizing: border-box; +} + +.public-space-link { + color: #33A7FF; + margin-left: 10px; + padding-top: 2px; +} + +.public-space-link:hover { + cursor: pointer; + color: #0074CC; +} + +@media (max-width: 740px) { + .main-container { + width: 100%; + } + + .side-container { + display: none; + } } .title { font-size: 32px; -} \ No newline at end of file +} + +.space-link { + padding: 0; + min-width: unset !important; +} + diff --git a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.html b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.html index 3513014..362ba46 100644 --- a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.html +++ b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.html @@ -1,71 +1,46 @@ -
-

My Spaces

- -
You have not yet subscribed to a space.
+
+
+

My Spaces

+ +
You have not yet subscribed to a space.
-
-
- {{space.space.name}} - +
+
+ {{space.space.name}} + +
-
- -
+ +
-

Recommended For You

- -
- There are no recommendations for you at the moment :( -
-
-
- -
- - - - +
+
+
+

Public Spaces

+
+
+ + {{s.name.length}} + {{s.name}} +
+
diff --git a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.ts b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.ts index 9cb8a76..f8f9451 100644 --- a/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.ts +++ b/src/app/space/subscribed-spaces-overview/subscribed-spaces-overview.component.ts @@ -4,12 +4,12 @@ import {Subscription} from 'rxjs'; import {Space} from '../../shared/rest-data-model/space'; import {MyspacesService} from '../../shared/myspaces/myspaces.service'; import {MatSnackBar} from '@angular/material/snack-bar'; -import {MatIconRegistry } from '@angular/material/icon'; -import {DomSanitizer} from '@angular/platform-browser'; import { RecommenderQuestion } from 'src/app/shared/rest-data-model/recommender-question'; import { NavigationExtras, Router } from '@angular/router'; import { RecommendationService } from 'src/app/shared/recommendation/recommendation.service'; import { AgentService } from 'src/app/shared/agent/agent.service'; +import { Clipboard } from '@angular/cdk/clipboard'; +import { SpaceService } from 'src/app/shared/space/space.service'; @Component({ selector: 'dnor-subscribed-spaces-overview', @@ -17,253 +17,74 @@ import { AgentService } from 'src/app/shared/agent/agent.service'; styleUrls: ['./subscribed-spaces-overview.component.css'] }) export class SubscribedSpacesOverviewComponent implements OnInit, OnDestroy { - public spaces: { space: Space, subscription: SpaceSubscription }[]; + public mySpaces: { space: Space, subscription: SpaceSubscription }[]; public spacesLoaded = false; public recommendationsLoaded = false; public recommenderQuestions: RecommenderQuestion[] = []; - //public bots: {name: string, active: {}}[] = []; - //public botWidgetUrl: string; + public publicSpaces: Space[] = []; private spaceSubscription: Subscription; - // private botUri: string; - constructor(private myspacesService: MyspacesService, private snackBar: MatSnackBar, private router: Router, - private matIconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, private recommendationService: RecommendationService, - private agentService: AgentService) { + constructor(private myspacesService: MyspacesService, private snackBar: MatSnackBar, private router: Router, private recommendationService: RecommendationService, + private agentService: AgentService, private clipboard: Clipboard, private spaceService: SpaceService) { + } + + getDateFormat(dateString: string): string { + let date = new Date(dateString); + let month = date.toLocaleString('en-GB', { month: 'short' }); + return `${month}, ${date.getDay()}, ${date.getFullYear()}, ${date.getHours()}:${date.getMinutes()}`; } ngOnInit() { this.spaceSubscription = this.myspacesService.getMySpacesObservable().subscribe((myspaces) => { this.spacesLoaded = true; - this.spaces = myspaces + this.mySpaces = myspaces }); - this.myspacesService.getMySpaces().then((s) => s); - this.matIconRegistry.addSvgIconInNamespace('img', 'bot', - this.sanitizer.bypassSecurityTrustResourceUrl('assets/bot.svg')); - this.matIconRegistry.addSvgIconInNamespace('img', 'add', - this.sanitizer.bypassSecurityTrustResourceUrl('assets/add.svg')); - this.matIconRegistry.addSvgIconInNamespace('img', 'train', - this.sanitizer.bypassSecurityTrustResourceUrl('assets/train.svg')); - this.matIconRegistry.addSvgIconInNamespace('img', 'play', - this.sanitizer.bypassSecurityTrustResourceUrl('assets/play.svg')); - this.matIconRegistry.addSvgIconInNamespace('img', 'pause', - this.sanitizer.bypassSecurityTrustResourceUrl('assets/pause.svg')); - // TestData for rendering - // let q1 = new Question(); - // q1.text = "What is a question?"; - // q1.timestampCreated = "2021-11-08T15:27:21.198688Z"; - // this.recommenderQuestions = [ - // { - // questionNeighbourIds: ["1", "2", "3"], - // question: q1, - // authorName: "david" - // }, - // { - // questionNeighbourIds: ["1", "2", "3"], - // question: q1, - // authorName: "alice" - // }, - // { - // questionNeighbourIds: ["1", "2", "3"], - // question: q1, - // authorName: "carol" - // } - // ]; - // this.recommendationsLoaded = true; + this.myspacesService.getMySpaces().then((s) => s); this.agentService.getAgent().then((agent) => { - this.recommendationService.getRecommendedQuestions(agent.agentid).then((res: RecommenderQuestion[]) => { + this.recommendationService.getRecommendedQuestions(agent.agentid) + .then((res: RecommenderQuestion[]) => { this.recommenderQuestions = res; }).catch(() => { console.error("error while getting recommendations..."); }).finally(() => { this.recommendationsLoaded = true; }) - }) - - - //this.botWidgetUrl = this.rh.getHostURL() + '/fileservice/v2.2.5/files/sbf'; - //this.botUri = this.rh.getHostURL() + '/SBFManager/bots/distributed-noracle'; + }); - /*const xmlhttp = new XMLHttpRequest(); // new HttpRequest instance - xmlhttp.open('GET', this.botUri); - xmlhttp.setRequestHeader('Accept', 'application/json'); - xmlhttp.send(); - const bots = this.bots; - xmlhttp.onreadystatechange = function(){ - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - const botj = JSON.parse(this.responseText); - for (const bname of Object.keys(botj)) { - var b = {'name': bname, 'active': {}}; - for (const uname of Object.keys(botj[bname]["active"])) { - const bs = botj[bname]["active"][uname] === true; - var bstatus = {}; - bstatus['active'] = bs; - if(bs){ - bstatus['icon'] = 'img:pause'; - }else{ - bstatus['icon'] = 'img:play'; - } - b["active"][uname] = bstatus; - } - bots.push(b); - } - } - }; */ + this.spaceService.getPublicSpaces().then((spaces: Space[]) => { + this.publicSpaces = spaces; + }); } ngOnDestroy() { this.spaceSubscription.unsubscribe(); } - linkClicked(r: RecommenderQuestion): void { - let questionIds = r.questionNeighbourIds; + recClicked(r: RecommenderQuestion): void { + //let questionIds = r.questionNeighbourIds; + let questionIds = []; questionIds.push(r.question.questionId); let navigationExtras: NavigationExtras = { queryParams: { sq: JSON.stringify(questionIds) } } - this.router.navigate(['/spaces', r.question.spaceId], navigationExtras).then(() => { - window.location.reload(); - }); + this.router.navigate(['/spaces', r.question.spaceId], navigationExtras); + } + + publicSpaceClicked(s: Space): void { + // this.router.navigate(['/spaces', space.spaceId], {queryParams: {sq: JSON.stringify([q.questionId])}}); + this.router.navigate(['/spaces', s.spaceId], {queryParams: {pw: s.spaceSecret}}); } copyInviteUrl(myspace: { space: Space, subscription: SpaceSubscription }) { let url = window.location.href; url = url.substring(0, url.indexOf('/myspaces')) + `/spaces/${myspace.space.spaceId}?pw=${myspace.space.spaceSecret}`; - const textArea = document.createElement('textarea'); - // Place in top-left corner of screen regardless of scroll position. - textArea.style.position = 'fixed'; - textArea.style.top = '0'; - textArea.style.left = '0'; - // Ensure it has a small width and height. Setting to 1px / 1em - // doesn't work as this gives a negative w/h on some browsers. - textArea.style.width = '2em'; - textArea.style.height = '2em'; - // We don't need padding, reducing the size if it does flash render. - textArea.style.padding = '0'; - // Clean up any borders. - textArea.style.border = 'none'; - textArea.style.outline = 'none'; - textArea.style.boxShadow = 'none'; - // Avoid flash of white box if rendered for any reason. - textArea.style.background = 'transparent'; - textArea.value = url; - document.body.appendChild(textArea); - textArea.select(); - try { - const successful = document.execCommand('copy'); - if (successful) { - this.snackBar.open('Copied invitation link to clipboard. Paste to share with friends!', 'hide', { - duration: 2000, - }); - } else { - this.copyFallback(url); - } - } catch (err) { - this.copyFallback(url); - } - document.body.removeChild(textArea); - } - - getIcon(bot:{}, spaceId: String){ - if(bot['active'][spaceId]==null){ - return 'img:play'; - } - return bot['active'][spaceId]['icon']; - } - - /*addBot(myspace: { space: Space, subscription: SpaceSubscription }, botObject: {name: String, active: {}}) { - const bot = botObject.name; - var active = false; - if(botObject.active[myspace.space.spaceId]==null){ - botObject['active'][myspace.space.spaceId] = {}; - }else{ - active = botObject['active'][myspace.space.spaceId]['active']; - } - - if (active === false) { - let url = window.location.href; - url = url.substring(0, url.indexOf('/myspaces')) + `/spaces/${myspace.space.spaceId}?pw=${myspace.space.spaceSecret}`; - const textArea = document.createElement('textarea'); - // Place in top-left corner of screen regardless of scroll position. - textArea.style.position = 'fixed'; - textArea.style.top = '0'; - textArea.style.left = '0'; - // Ensure it has a small width and height. Setting to 1px / 1em - // doesn't work as this gives a negative w/h on some browsers. - textArea.style.width = '2em'; - textArea.style.height = '2em'; - // We don't need padding, reducing the size if it does flash render. - textArea.style.padding = '0'; - // Clean up any borders. - textArea.style.border = 'none'; - textArea.style.outline = 'none'; - textArea.style.boxShadow = 'none'; - // Avoid flash of white box if rendered for any reason. - textArea.style.background = 'transparent'; - textArea.value = url; - document.body.appendChild(textArea); - textArea.select(); - - const xmlhttp = new XMLHttpRequest(); // new HttpRequest instance - xmlhttp.open('POST', this.rh.getHostURL() + '/SBFManager/join/' + bot); - xmlhttp.setRequestHeader('Content-Type', 'application/json'); - xmlhttp.send(JSON.stringify({basePath: environment.hostUrls[0] + '/distributed-noracle', - joinPath: 'agents/$botId/spacesubscriptions', - spaceId: myspace.space.spaceId, spaceSecret: myspace.space.spaceSecret})); - const snackBar = this.snackBar; - const bots = this.bots; - xmlhttp.onreadystatechange = function(){ - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - snackBar.open('Bot added!', 'hide', { - duration: 2000, - }); - bots.forEach(function(element) { - if (element.name === bot) { - element.active[myspace.space.spaceId]['icon'] = 'img:pause'; - element.active[myspace.space.spaceId]['active'] = true; - } - }); - }else if (xmlhttp.readyState === 4) { - snackBar.open('Bot could not be added!', 'hide', { - duration: 2000, - }); - } - }; - - document.body.removeChild(textArea); - }else { - const botUri = this.botUri; - - const snackBar = this.snackBar; - this.bots.forEach(element => { - if (element.name === bot) { - const xmlhttp = new XMLHttpRequest(); // new HttpRequest instance - xmlhttp.open('DELETE', botUri + '/' + bot + '/' + myspace.space.spaceId); - xmlhttp.send(); - xmlhttp.onreadystatechange = function(){ - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - snackBar.open('Bot paused!', 'hide', { - duration: 2000, - }); - element.active[myspace.space.spaceId]['icon'] = 'img:play'; - element.active[myspace.space.spaceId]['active'] = false; - }else if (xmlhttp.readyState === 4) { - snackBar.open('Bot could not be paused!', 'hide', { - duration: 2000, - }); - } - }; - } - }); - } - }*/ - - private copyFallback(url: string) { - window.prompt('Could not copy to clipboard automatically. Please copy the following invitation link manually:', url); + this.clipboard.copy(url); + this.snackBar.open('Copied invitation link to clipboard. Paste to share with friends!', 'Ok'); } getStringifiedParamArray(subscription: SpaceSubscription): string { diff --git a/src/assets/default-avatar.png b/src/assets/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..e7596cc4275b2825936307a14638918420b7efe1 GIT binary patch literal 10056 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 luUp3_p^Uf