diff --git a/package.json b/package.json index 4e9dac24b66b338b20473d923a369efcdef99a18..9900abe704946d8c4bb67eb95e7604dd7fbe9e04 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "rxjs": "^6.3.3", "rxjs-compat": "^6.2.2", "tslint-eslint-rules": "^5.3.1", + "typescript-map": "0.0.7", "zone.js": "^0.8.26" }, "devDependencies": { diff --git a/proxy.conf.json b/proxy.conf.json index c2e9c8823311057960824399f18aafc86f7620a9..93cc54a82c5dae28c32ca7e3deb83c19663e8fe4 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -1,4 +1,12 @@ { + "/api/comment": { + "target": "http://localhost:8088", + "secure": false, + "pathRewrite": { + "^/api": "" + }, + "logLevel": "debug" + }, "/api": { "target": "http://localhost:8080", "secure": false, diff --git a/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.html b/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.html index ac95224c073f242c673681e05e688c0f5763b55d..a28a981824d16e13c9a6d57de3b8a59f0009fb8c 100644 --- a/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.html +++ b/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.html @@ -1,12 +1,6 @@ <div fxLayout="column" fxLayoutAlign="center" fxLayoutGap="20px"> <div fxLayout="row" fxLayoutAlign="center"> <form> - <mat-form-field class="input-block"> - <input matInput #commentSubject type="text" maxlength="25" - placeholder="{{ 'comment-page.enter-title' | translate}}" onkeypress="return event.keyCode !=13;" - [formControl]="subjectForm"> - <mat-hint align="end">{{commentSubject.value.length}} / 25</mat-hint> - </mat-form-field> <mat-form-field class="input-block"> <textarea matInput #commentBody placeholder="{{ 'comment-page.enter-comment' | translate}}" matAutosizeMinRows=2 matAutosizeMaxRows=5 maxlength="255" [formControl]="bodyForm"></textarea> @@ -15,7 +9,7 @@ <button mat-raised-button color="warn" (click)="onNoClick()">{{ 'comment-page.abort' | translate}}</button> <button mat-raised-button color="accent" - (click)="closeDialog(commentSubject.value, commentBody.value)">{{ 'comment-page.send' | translate}}</button> + (click)="closeDialog(commentBody.value)">{{ 'comment-page.send' | translate}}</button> </form> </div> </div> diff --git a/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.ts b/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.ts index 6e2f2fb70c95335c43892a35bbf823f87307f434..caca1c72a4921ef3610a4c1289d2b28784be762a 100644 --- a/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.ts +++ b/src/app/components/shared/_dialogs/submit-comment/submit-comment.component.ts @@ -1,11 +1,9 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Comment } from '../../../../models/comment'; -import { ActivatedRoute } from '@angular/router'; import { NotificationService } from '../../../../services/util/notification.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; import { CommentPageComponent } from '../../comment-page/comment-page.component'; -import { AuthenticationService } from '../../../../services/http/authentication.service'; import { FormControl, Validators } from '@angular/forms'; import { User } from '../../../../models/user'; @@ -20,16 +18,16 @@ export class SubmitCommentComponent implements OnInit { comment: Comment; user: User; + roomId: string; subjectForm = new FormControl('', [Validators.required]); bodyForm = new FormControl('', [Validators.required]); private date = new Date(Date.now()); - constructor(private route: ActivatedRoute, + constructor( private notification: NotificationService, public dialogRef: MatDialogRef<CommentPageComponent>, private translateService: TranslateService, - protected authenticationService: AuthenticationService, public dialog: MatDialog, private translationService: TranslateService, @Inject(MAT_DIALOG_DATA) public data: any) { @@ -37,28 +35,14 @@ export class SubmitCommentComponent implements OnInit { ngOnInit() { this.translateService.use(localStorage.getItem('currentLang')); - this.user = this.authenticationService.getUser(); } onNoClick(): void { this.dialogRef.close(); } - checkInputData(subject: string, body: string): boolean { - subject = subject.trim(); + checkInputData(body: string): boolean { body = body.trim(); - if (!subject && !body) { - this.translationService.get('comment-page.error-both-fields').subscribe(message => { - this.notification.show(message); - }); - return false; - } - if (!subject) { - this.translationService.get('comment-page.error-title').subscribe(message => { - this.notification.show(message); - }); - return false; - } if (!body) { this.translationService.get('comment-page.error-comment').subscribe(message => { this.notification.show(message); @@ -68,11 +52,10 @@ export class SubmitCommentComponent implements OnInit { return true; } - closeDialog(subject: string, body: string) { - if (this.checkInputData(subject, body) === true) { + closeDialog(body: string) { + if (this.checkInputData(body) === true) { const comment = new Comment(); comment.roomId = localStorage.getItem(`roomId`); - comment.subject = subject; comment.body = body; comment.userId = this.user.id; this.dialogRef.close(comment); diff --git a/src/app/components/shared/comment-list/comment-list.component.ts b/src/app/components/shared/comment-list/comment-list.component.ts index 8b104e1f26a40c48a8d5c2ccd2cef73e1e31df99..b3afb6633a00507a0ce5938ff8bef52a6ec4a214 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -3,6 +3,8 @@ import { Comment } from '../../../models/comment'; import { CommentService } from '../../../services/http/comment.service'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../services/util/language.service'; +import { RxStompService } from '@stomp/ng2-stompjs'; +import { Message } from '@stomp/stompjs'; @Component({ selector: 'app-comment-list', @@ -18,13 +20,18 @@ export class CommentListComponent implements OnInit { constructor(private commentService: CommentService, private translateService: TranslateService, - protected langService: LanguageService) { + protected langService: LanguageService, + private rxStompService: RxStompService) { langService.langEmitter.subscribe(lang => translateService.use(lang)); } ngOnInit() { this.roomId = localStorage.getItem(`roomId`); + this.comments = []; this.hideCommentsList = false; + this.rxStompService.watch(`/topic/${this.roomId}.comment.stream`).subscribe((message: Message) => { + this.parseIncomingMessage(message); + }); this.getComments(); this.translateService.use(localStorage.getItem('currentLang')); } @@ -40,4 +47,39 @@ export class CommentListComponent implements OnInit { searchComments(term: string): void { this.filteredComments = this.comments.filter(c => c.body.toLowerCase().includes(term)); } + + parseIncomingMessage(message: Message) { + const msg = JSON.parse(message.body); + const payload = msg.payload; + if (msg.type === 'CommentCreated') { + const c = new Comment(); + c.roomId = this.roomId; + c.body = payload.body; + c.id = payload.id; + c.creationTimestamp = payload.timestamp; + this.comments = this.comments.concat(c); + } else if (msg.type === 'CommentPatched') { + const c = this.comments.find((comment: Comment) => comment.id === payload.id); + if (c) { + const index = this.comments.indexOf(c); + console.log(index); + const newList = this.comments.slice(0); + const changes = payload.changes; + // ToDo: there must be a better way to update the model + for (const change of changes) { + console.log(change); + if (change.read) { + c.read = change.read; + } else if (change.favorite) { + c.favorite = change.favorite; + } else if (change.correct) { + c.correct = change.correct; + } + } + newList[index] = c; + this.comments = newList; + } + } + } } + diff --git a/src/app/components/shared/comment-page/comment-page.component.ts b/src/app/components/shared/comment-page/comment-page.component.ts index bb09927e14b6cfbc1ad1c9cc155f9106a22622b8..33fd31461df20c963d77b8cb3a6a994d90c8027b 100644 --- a/src/app/components/shared/comment-page/comment-page.component.ts +++ b/src/app/components/shared/comment-page/comment-page.component.ts @@ -1,11 +1,12 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Comment } from '../../../models/comment'; -import { CommentService } from '../../../services/http/comment.service'; +import { User } from '../../../models/user'; import { NotificationService } from '../../../services/util/notification.service'; -import { CommentListComponent } from '../comment-list/comment-list.component'; +import { AuthenticationService } from '../../../services/http/authentication.service'; import { MatDialog } from '@angular/material'; import { SubmitCommentComponent } from '../_dialogs/submit-comment/submit-comment.component'; +import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; @Component({ selector: 'app-comment-page', @@ -13,20 +14,26 @@ import { SubmitCommentComponent } from '../_dialogs/submit-comment/submit-commen styleUrls: ['./comment-page.component.scss'] }) export class CommentPageComponent implements OnInit { - @ViewChild(CommentListComponent) child: CommentListComponent; + roomId: string; + user: User; constructor(private route: ActivatedRoute, - private commentService: CommentService, private notification: NotificationService, - public dialog: MatDialog) { } + public dialog: MatDialog, + private authenticationService: AuthenticationService, + private wsCommentService: WsCommentServiceService) { } ngOnInit(): void { + this.roomId = localStorage.getItem('roomId'); + this.user = this.authenticationService.getUser(); } openSubmitDialog(): void { const dialogRef = this.dialog.open(SubmitCommentComponent, { width: '400px' }); + dialogRef.componentInstance.user = this.user; + dialogRef.componentInstance.roomId = this.roomId; dialogRef.afterClosed() .subscribe(result => { if (result) { @@ -38,19 +45,6 @@ export class CommentPageComponent implements OnInit { } send(comment: Comment): void { - this.commentService.addComment({ - id: '', - roomId: comment.roomId, - userId: comment.userId, - subject: comment.subject, - body: comment.body, - creationTimestamp: comment.creationTimestamp, - read: false, - revision: '' - } as Comment).subscribe(() => { - this.child.getComments(); - this.notification.show(`Comment '${comment.subject}' successfully created.`); - }); - + this.wsCommentService.add(comment); } } diff --git a/src/app/components/shared/comment/comment.component.ts b/src/app/components/shared/comment/comment.component.ts index 0a7b94d7b03ccd714d9ece9b3213bd20b238fbf9..09ecfd90845cc996fdc4100bea9d559d042c0c72 100644 --- a/src/app/components/shared/comment/comment.component.ts +++ b/src/app/components/shared/comment/comment.component.ts @@ -7,6 +7,7 @@ import { CommentService } from '../../../services/http/comment.service'; import { NotificationService } from '../../../services/util/notification.service'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../services/util/language.service'; +import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; @Component({ selector: 'app-comment', @@ -24,7 +25,8 @@ export class CommentComponent implements OnInit { private commentService: CommentService, private notification: NotificationService, private translateService: TranslateService, - protected langService: LanguageService) { + protected langService: LanguageService, + private wsCommentService: WsCommentServiceService) { langService.langEmitter.subscribe(lang => translateService.use(lang)); } ngOnInit() { @@ -35,23 +37,23 @@ export class CommentComponent implements OnInit { } setRead(comment: Comment): void { - comment.read = !comment.read; - this.commentService.updateComment(comment).subscribe(); + this.comment = this.wsCommentService.toggleRead(comment); + // this.commentService.updateComment(comment).subscribe(); } setCorrect(comment: Comment): void { - comment.correct = !comment.correct; - this.commentService.updateComment(comment).subscribe(); + this.comment = this.wsCommentService.toggleCorrect(comment); + // this.commentService.updateComment(comment).subscribe(); } setFavorite(comment: Comment): void { - comment.favorite = !comment.favorite; - this.commentService.updateComment(comment).subscribe(); + this.comment = this.wsCommentService.toggleFavorite(comment); + // this.commentService.updateComment(comment).subscribe(); } delete(comment: Comment): void { this.commentService.deleteComment(comment.id).subscribe(room => { - this.notification.show(`Comment '${comment.subject}' successfully deleted.`); + this.notification.show(`Comment '${comment.body}' successfully deleted.`); }); } } diff --git a/src/app/components/shared/feedback-barometer-page/feedback-barometer-page.component.ts b/src/app/components/shared/feedback-barometer-page/feedback-barometer-page.component.ts index 3305628f67143578ef89083581a8ff0d301341e2..a8b0b4c252a956c0ca9046922ad87e47cb12e33f 100644 --- a/src/app/components/shared/feedback-barometer-page/feedback-barometer-page.component.ts +++ b/src/app/components/shared/feedback-barometer-page/feedback-barometer-page.component.ts @@ -36,14 +36,14 @@ export class FeedbackBarometerPageComponent implements OnInit { ngOnInit() { this.userRole = this.authenticationService.getRole(); - this.rxStompService.watch(`/room/${this.roomId}/feedback.stream`).subscribe((message: Message) => { + this.rxStompService.watch(`/queue/${this.roomId}.feedback.stream`).subscribe((message: Message) => { this.parseIncomingMessage(message); }); const getFeedback = new GetFeedback(); this.rxStompService.publish({ - destination: `/backend/room/${this.roomId}/feedback.query`, + destination: `/backend/queue/${this.roomId}.feedback.query`, body: JSON.stringify(getFeedback) }); } @@ -59,7 +59,7 @@ export class FeedbackBarometerPageComponent implements OnInit { submitFeedback(state: number) { const createFeedback = new CreateFeedback(state); this.rxStompService.publish({ - destination: `/backend/room/${this.roomId}/feedback.command`, + destination: `/backend/queue/${this.roomId}.feedback.command`, body: JSON.stringify(createFeedback) }); } diff --git a/src/app/models/comment.ts b/src/app/models/comment.ts index 14e32bda25f4953022fd905d46062bc33448a9ee..81142f8df808d0eb3ff7c80a59e6a8b92e90ab49 100644 --- a/src/app/models/comment.ts +++ b/src/app/models/comment.ts @@ -3,26 +3,23 @@ export class Comment { roomId: string; userId: string; revision: string; - subject: string; body: string; read: boolean; correct: boolean; favorite: boolean; - creationTimestamp: number; + creationTimestamp: Date; constructor(roomId: string = '', userId: string = '', - subject: string = '', body: string = '', read: boolean = false, correct: boolean = false, favorite: boolean = false, - creationTimestamp: number = 0) { + creationTimestamp: Date = null) { this.id = ''; this.roomId = roomId; this.userId = userId; this.revision = ''; - this.subject = subject; this.body = body; this.read = read; this.correct = correct; diff --git a/src/app/models/messages/create-comment.ts b/src/app/models/messages/create-comment.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca7c5fa54fa4f92461aea73de49b1906f4b2b26a --- /dev/null +++ b/src/app/models/messages/create-comment.ts @@ -0,0 +1,17 @@ +export class CreateComment { + type: string; + payload: { + roomId: string; + creatorId: string; + body: string; + }; + + constructor(roomId: string, creatorId: string, body: string) { + this.type = 'CreateComment'; + this.payload = { + roomId: roomId, + creatorId: creatorId, + body: body + }; + } +} diff --git a/src/app/models/messages/patch-comment.ts b/src/app/models/messages/patch-comment.ts new file mode 100644 index 0000000000000000000000000000000000000000..faec58f4e2d9685438749606abb7b352606140b0 --- /dev/null +++ b/src/app/models/messages/patch-comment.ts @@ -0,0 +1,17 @@ +import { TSMap } from 'typescript-map'; + +export class PatchComment { + type: string; + payload: { + id: string; + changes: TSMap<string, any>; + }; + + constructor(id: string, changes: TSMap<string, any>) { + this.type = 'PatchComment'; + this.payload = { + id: id, + changes: changes + }; + } +} diff --git a/src/app/rx-stomp.config.ts b/src/app/rx-stomp.config.ts index 70326222a0d4eb3bb8de6041633a864871acfda1..aaded217c0dcab07296eaec586af6ac4383fba9a 100644 --- a/src/app/rx-stomp.config.ts +++ b/src/app/rx-stomp.config.ts @@ -18,6 +18,6 @@ export const myRxStompConfig: InjectableRxStompConfig = { // It can be quite verbose, not recommended in production // Skip this key to stop logging to console debug: (msg: string): void => { - // console.log(new Date(), 'STOMP debug: ' + msg); + console.log(new Date(), 'STOMP debug: ' + msg); } }; diff --git a/src/app/services/http/comment.service.ts b/src/app/services/http/comment.service.ts index 5a6b17c0bd419591980b7f0e2ec49550297e6272..d8673aa279dd3247b6afc5c2c36c2057201a47df 100644 --- a/src/app/services/http/comment.service.ts +++ b/src/app/services/http/comment.service.ts @@ -32,8 +32,7 @@ export class CommentService extends BaseHttpService { addComment(comment: Comment): Observable<Comment> { const connectionUrl = this.apiUrl.base + this.apiUrl.comment + '/'; return this.http.post<Comment>(connectionUrl, - { - roomId: comment.roomId, subject: comment.subject, body: comment.body, + { roomId: comment.roomId, body: comment.body, read: comment.read, creationTimestamp: comment.creationTimestamp }, httpOptions).pipe( tap(_ => ''), diff --git a/src/app/services/websockets/ws-comment-service.service.spec.ts b/src/app/services/websockets/ws-comment-service.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..02c67cb49c83d3beda7ce75f172f69541436b821 --- /dev/null +++ b/src/app/services/websockets/ws-comment-service.service.spec.ts @@ -0,0 +1,13 @@ +/* import { TestBed } from '@angular/core/testing'; + +import { WsCommentServiceService } from './ws-comment-service.service'; + +describe('WsCommentServiceService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: WsCommentServiceService = TestBed.get(WsCommentServiceService); + expect(service).toBeTruthy(); + }); +}); +*/ diff --git a/src/app/services/websockets/ws-comment-service.service.ts b/src/app/services/websockets/ws-comment-service.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e8516afeafef627ac38d8ec5ec139863ce067ba --- /dev/null +++ b/src/app/services/websockets/ws-comment-service.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { Comment } from '../../models/comment'; +import { RxStompService } from '@stomp/ng2-stompjs'; +import { CreateComment } from '../../models/messages/create-comment'; +import { PatchComment } from '../../models/messages/patch-comment'; +import { TSMap } from 'typescript-map'; + + +@Injectable({ + providedIn: 'root' +}) +export class WsCommentServiceService { + + constructor(private rxStompService: RxStompService) { } + + add(comment: Comment): void { + const message = new CreateComment(comment.roomId, comment.userId, comment.body); + this.rxStompService.publish({ + destination: `/queue/comment.command.create`, + body: JSON.stringify(message), + headers: { + 'content-type': 'application/json' + } + }); + } + + toggleRead(comment: Comment): Comment { + console.log(comment); + comment.read = !comment.read; + const changes = new TSMap<string, any>(); + changes.set('read', comment.read); + this.patchComment(comment, changes); + return comment; + } + + toggleFavorite(comment: Comment): Comment { + comment.favorite = !comment.favorite; + const changes = new TSMap<string, any>(); + changes.set('favorite', comment.favorite); + this.patchComment(comment, changes); + return comment; + } + + toggleCorrect(comment: Comment): Comment { + comment.correct = !comment.correct; + const changes = new TSMap<string, any>(); + changes.set('correct', comment.correct); + this.patchComment(comment, changes); + return comment; + } + + private patchComment(comment: Comment, changes: TSMap<string, any>): void { + const message = new PatchComment(comment.id, changes); + console.log(message); + this.rxStompService.publish({ + destination: `/queue/comment.command.patch`, + body: JSON.stringify(message), + headers: { + 'content-type': 'application/json' + } + }); + } +}