diff --git a/src/app/components/moderator/moderator-comment-list/moderator-comment-list.component.ts b/src/app/components/moderator/moderator-comment-list/moderator-comment-list.component.ts index 0e29ee970bd2b16919df89f5644df6a2e054d98d..a43c004ddd59b02b636c793290dabb893e6181f0 100644 --- a/src/app/components/moderator/moderator-comment-list/moderator-comment-list.component.ts +++ b/src/app/components/moderator/moderator-comment-list/moderator-comment-list.component.ts @@ -329,6 +329,7 @@ export class ModeratorCommentListComponent implements OnInit, OnDestroy { c.id = payload.id; c.timestamp = payload.timestamp; c.creatorId = payload.creatorId; + c.keywordsFromQuestioner = JSON.parse(payload.keywordsFromQuestioner); c.userNumber = this.commentService.hashCode(c.creatorId); this.comments = this.comments.concat(c); break; diff --git a/src/app/components/shared/_dialogs/cloud-configuration/cloud-configuration.component.ts b/src/app/components/shared/_dialogs/cloud-configuration/cloud-configuration.component.ts index 94f2117a5e6498a1234e130d54c39cc4b0d2b192..1115225d8b676ee6c8b855373117f2d0d9e5fe3f 100644 --- a/src/app/components/shared/_dialogs/cloud-configuration/cloud-configuration.component.ts +++ b/src/app/components/shared/_dialogs/cloud-configuration/cloud-configuration.component.ts @@ -1,9 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { TagCloudComponent } from '../../tag-cloud/tag-cloud.component'; -import { TagCloudMetaDataCount } from '../../tag-cloud/tag-cloud.data-manager'; import { CloudParameters, CloudTextStyle } from '../../tag-cloud/tag-cloud.interface'; import { WeightClass } from './weight-class.interface'; +import { TagCloudMetaDataCount } from '../../../../services/util/tag-cloud-data.service'; @Component({ selector: 'app-cloud-configuration', diff --git a/src/app/components/shared/_dialogs/room-create/room-create.component.ts b/src/app/components/shared/_dialogs/room-create/room-create.component.ts index 5c83def0486ec96696029d41e50134b895e0378e..1e49b428fcd067fe53775b70b635676eac133457 100644 --- a/src/app/components/shared/_dialogs/room-create/room-create.component.ts +++ b/src/app/components/shared/_dialogs/room-create/room-create.component.ts @@ -74,6 +74,7 @@ export class RoomCreateComponent implements OnInit { newRoom.name = longRoomName; newRoom.abbreviation = '00000000'; newRoom.description = ''; + newRoom.blacklist = '[]'; if (this.hasCustomShortId && this.customShortIdName && this.customShortIdName.length > 0) { if (!new RegExp('[1-9a-z,A-Z,\s,\-,\.,\_,\~]+').test(this.customShortIdName) || this.customShortIdName.startsWith(' ') || this.customShortIdName.endsWith(' ')) { diff --git a/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts b/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts index 43437d2f46dd6320149bfce02e6b02ccf39310ed..5d0dbabc488342451031004c7321b514b6b9b60d 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts @@ -1,13 +1,58 @@ -export interface TopicCloudAdminData{ +export interface TopicCloudAdminData { blacklist: string[]; + wantedLabels: { + de: string[]; + en: string[]; + }; considerVotes: boolean; profanityFilter: boolean; blacklistIsActive: boolean; keywordORfulltext: KeywordOrFulltext; } -export enum KeywordOrFulltext{ +export enum KeywordOrFulltext { keyword, fulltext, both } + +export interface Label { + readonly tag: string; + readonly label: string; +} + +export class Labels { + readonly de: Label[]; + readonly en: Label[]; + + constructor(_de: Label[], _en: Label[]) { + this.de = _de; + this.en = _en; + } +} + +const deLabels: Label[] = [ + {tag: 'sb', label: 'Subjekt'}, + {tag: 'pd', label: 'Prädikat'}, + {tag: 'og', label: 'Genitivobjekt'}, + {tag: 'ag', label: 'Genitivattribut'}, + {tag: 'app', label: 'Apposition'}, + {tag: 'da', label: 'Dativobjekt'}, + {tag: 'oa', label: 'Akkusativobjekt'}, + {tag: 'nk', label: 'Noun Kernel Element'}, + {tag: 'mo', label: 'Modifikator'}, + {tag: 'cj', label: 'Konjunktor'} +]; + +const enLabels: Label[] = [ + {tag: 'no', label: 'Noun'}, + {tag: 'pro', label: 'Pronoun'}, + {tag: 've', label: 'Verb'}, + {tag: 'adj', label: 'Adjective'}, + {tag: 'adv', label: 'AdverbDVERB'}, + {tag: 'pre', label: 'Preposition'}, + {tag: 'con', label: 'Conjunction'}, + {tag: 'int', label: 'Interjection'} +]; + +export const spacyLabels = new Labels(deLabels, enLabels); diff --git a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.html b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.html index bc7471ca4f14d7ec2bb3e8d56cb80218bd7086de..d1e5756904aa02809fb953506f86cf83ef7b7e75 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.html +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.html @@ -11,9 +11,7 @@ <mat-label class="color-on-surface"> {{"topic-cloud-dialog.select-choice" | translate}} </mat-label> - <mat-radio-group - class="radio-button-group" - [(ngModel)]="keywordORfulltext"> + <mat-radio-group class="radio-button-group" [(ngModel)]="keywordORfulltext"> <mat-radio-button checked="true" [value]="keywordOrFulltextENUM[0]" class="radio-button-item"> {{"topic-cloud-dialog.keyword" | translate}} </mat-radio-button> @@ -37,7 +35,7 @@ </mat-slide-toggle> <mat-accordion class="new-profanity-word" multi> - <mat-expansion-panel class="color-background" (opened)="enterProfanityWord=true; focusInput('profanity-word-input')" + <mat-expansion-panel class="color-background" (opened)="enterProfanityWord=true; focusInput('bad-word-input')" (closed)="enterProfanityWord = false"> <mat-expansion-panel-header class="color-background"> <mat-panel-title> @@ -65,12 +63,13 @@ <div> <button mat-raised-button *ngIf="getProfanityList().length > 0" class="primaryBackground" (click)="showProfanityList=!showProfanityList"> - {{showProfanityList ? ('topic-cloud-dialog.hide-profanity-list' | translate) : ('topic-cloud-dialog.show-profanity-list' | translate)}} + {{showProfanityList ? ('topic-cloud-dialog.hide-profanity-list' | translate) : + ('topic-cloud-dialog.show-profanity-list' | translate)}} </button> </div> </mat-expansion-panel> - <mat-expansion-panel class="color-background" + <mat-expansion-panel class="color-background margin-bottom" (opened)="enterBlacklistWord = true; focusInput('blacklist-word-input')" (closed)="enterBlacklistWord = false"> <mat-expansion-panel-header class="color-background"> @@ -87,9 +86,8 @@ <button mat-stroked-button color="primary" class="margin-left" (click)="addBlacklistWord()"> {{'topic-cloud-dialog.add-word' | translate}} </button> - - <mat-list role="list" *ngIf="showBlacklistWordList" class="margin-bottom"> - <mat-list-item class="color-on-surface" *ngFor="let word of getBlacklist()" role="listitem">{{word}} + <mat-list role="list" *ngIf="showBlacklistWordList && blacklist.length > 0" class="margin-bottom"> + <mat-list-item class="color-on-surface" *ngFor="let word of blacklist" role="listitem">{{word}} <button style="margin-left: auto" mat-icon-button class="red" (click)="removeWordFromBlacklist(word)"> <mat-icon mat-list-icon style="margin-bottom: 6px;">delete</mat-icon> </button> @@ -97,12 +95,53 @@ </mat-list> <div> - <button mat-raised-button *ngIf="getBlacklist().length > 0" class="primaryBackground" + <button mat-raised-button class="primaryBackground" *ngIf="blacklist.length > 0" (click)="showBlacklistWordList=!showBlacklistWordList"> - {{showBlacklistWordList ? ('topic-cloud-dialog.hide-blacklist' | translate) : ('topic-cloud-dialog.show-blacklist' | translate)}} + {{showBlacklistWordList ? ('topic-cloud-dialog.hide-blacklist' | translate) : + ('topic-cloud-dialog.show-blacklist' | translate)}} </button> </div> </mat-expansion-panel> + <mat-expansion-panel class="color-background"> + <mat-expansion-panel-header class="color-background"> + <mat-panel-title> + Spacy labels + </mat-panel-title> + </mat-expansion-panel-header> + + <mat-tab-group animationDuration="0ms" mat-stretch-tabs mat-align-tabs="center"> + <mat-tab label="{{'topic-cloud-dialog.german' | translate}}"> + <mat-selection-list *ngIf="wantedLabels" [(ngModel)]="wantedLabels.de"> + + <mat-option class="color-on-surface" (click)="selectAllDE(); allSelectedDE = !allSelectedDE"> + <mat-label> + <mat-icon>playlist_add_check</mat-icon> + {{'topic-cloud-dialog.select-all' | translate}} + </mat-label> + </mat-option> + + <mat-list-option [value]="label.tag" class="color-on-surface" *ngFor="let label of spacyLabels.de"> + {{label.label + " (" + label.tag + ")"}} + </mat-list-option> + </mat-selection-list> + </mat-tab> + <mat-tab label="{{'topic-cloud-dialog.english' | translate}}"> + <mat-selection-list *ngIf="wantedLabels" [(ngModel)]="wantedLabels.en"> + + <mat-option class="color-on-surface" (click)="selectAllEN(); allSelectedEN = !allSelectedEN"> + <mat-label> + <mat-icon>playlist_add_check</mat-icon> + {{'topic-cloud-dialog.select-all' | translate}} + </mat-label> + </mat-option> + + <mat-list-option [value]="label.tag" class="color-on-surface" *ngFor="let label of spacyLabels.en"> + {{label.label + " (" + label.tag + ")"}} + </mat-list-option> + </mat-selection-list> + </mat-tab> + </mat-tab-group> + </mat-expansion-panel> </mat-accordion> </mat-expansion-panel> </mat-accordion> @@ -123,12 +162,14 @@ </div> <div fxLayoutAlign="center center" style="margin-left: auto; font-weight: bold;"> - <mat-icon [ngClass]="{'animation-blink': searchMode}">sell</mat-icon> - <p class="margin-left" [ngClass]="{'animation-blink': searchMode}">{{searchMode ? filteredKeywords.length : keywords.length}}</p> + <mat-icon svgIcon="comment_tag" + [ngClass]="{'animation-blink': searchMode}" + class="oldtypo-h2 comment_tag-icon"></mat-icon> + <p [ngClass]="{'animation-blink': searchMode}">{{searchMode ? filteredKeywords.length : + keywords.length}}</p> </div> <div class="margin-left vertical-center"> - <button [ngClass]="{'animation-blink': sortMode!=='alphabetic'}" mat-icon-button - [matMenuTriggerFor]="sortMenu"> + <button [ngClass]="{'animation-blink': sortMode!=='alphabetic'}" mat-icon-button [matMenuTriggerFor]="sortMenu"> <mat-icon>sort</mat-icon> </button> </div> @@ -151,15 +192,13 @@ </button> </mat-menu> - <mat-accordion> - <mat-expansion-panel class="color-surface" hideToggle *ngIf="searchMode && filteredKeywords.length === 0"> - <mat-expansion-panel-header class="color-surface"> - <mat-panel-title fxLayoutAlign="center"> - {{'topic-cloud-dialog.no-keywords-note' | translate}} - </mat-panel-title> - </mat-expansion-panel-header> - </mat-expansion-panel> + <mat-card class="color-surface" *ngIf="keywords.length === 0 || (searchMode && filteredKeywords.length === 0)"> + <p class="color-on-surface" fxLayoutAlign="center"> + {{'topic-cloud-dialog.no-keywords-note' | translate}} + </p> + </mat-card> + <mat-accordion> <mat-expansion-panel class="color-surface" (opened)="panelOpenState = true" (closed)="panelOpenState = edit = false" *ngFor="let keyword of (searchMode ? filteredKeywords : keywords); let i = index" [attr.data-index]="i"> @@ -168,13 +207,13 @@ {{profanityFilter ? getKeywordWithoutProfanity(keyword.keyword) : keyword.keyword}} </mat-panel-title> <mat-panel-description> - {{keyword.questions.length}} - {{'topic-cloud-dialog.question-count-'+(keyword.questions.length > 1 ? 'plural' : 'singular') | translate}} + {{keyword.comments.length}} + {{'topic-cloud-dialog.question-count-'+(keyword.comments.length > 1 ? 'plural' : 'singular') | translate}} </mat-panel-description> </mat-expansion-panel-header> - <div *ngFor="let question of keyword.questions"> + <div *ngFor="let question of keyword.comments"> <mat-divider></mat-divider> - <app-topic-dialog-comment [question]="question" [keyword]="keyword.keyword" [maxShowedCharachters]="140" + <app-topic-dialog-comment [question]="question.body" [keyword]="keyword.keyword" [maxShowedCharachters]="140" [isCollapsed]="!panelOpenState" [profanityFilter]="profanityFilter"></app-topic-dialog-comment> </div> diff --git a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss index f44532d248f95cfcc58ff91faad7bb06a4d2e4e3..c283bcb67cc19596c035738be223da967e7815ce 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss @@ -16,6 +16,10 @@ margin-bottom: 16px; } +.margin-top { + margin-top: 16px; +} + .primary { color: var(--primary); background: none; @@ -134,3 +138,6 @@ mat-dialog-content { max-height: 80vh!important; } +.comment_tag-icon { + height: 18px !important; +} diff --git a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.ts b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.ts index b5b55d9ca573918ab7ab771be1fb8778b82eb7d7..4540506808389e7cdfe49e046a9a3562d4b4b4d9 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.ts @@ -1,15 +1,18 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { TagCloudComponent } from '../../tag-cloud/tag-cloud.component'; +import { Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { NotificationService } from '../../../../services/util/notification.service'; import { TopicCloudConfirmDialogComponent } from '../topic-cloud-confirm-dialog/topic-cloud-confirm-dialog.component'; -import { AuthenticationService } from '../../../../services/http/authentication.service'; import { UserRole } from '../../../../models/user-roles.enum'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../../services/util/language.service'; import { TopicCloudAdminService } from '../../../../services/util/topic-cloud-admin.service'; -import { TopicCloudAdminData } from './TopicCloudAdminData'; +import { TopicCloudAdminData, Labels, spacyLabels } from './TopicCloudAdminData'; import { KeywordOrFulltext } from './TopicCloudAdminData'; +import { User } from '../../../../models/user'; +import { Comment } from '../../../../models/comment'; +import { CommentService } from '../../../../services/http/comment.service'; +import { WsCommentServiceService } from '../../../../services/websockets/ws-comment-service.service'; +import { TSMap } from 'typescript-map'; @Component({ selector: 'app-topic-cloud-administration', @@ -21,6 +24,8 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { public considerVotes: boolean; public profanityFilter: boolean; public blacklistIsActive: boolean; + blacklist: string[] = []; + blacklistSubscription = undefined; keywordOrFulltextENUM = KeywordOrFulltext; newKeyword = undefined; edit = false; @@ -38,95 +43,87 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { showSettingsPanel = false; keywordORfulltext: string = undefined; userRole: UserRole; - keywords: Keyword[] = [ - { - keywordID: 1, - keyword: 'Cloud', - questions: [ - 'Wieviel speicherplatz steht mir in der Cloud zur verfügung?', - 'Sollen wir die Tag Cloud implementieren?', - // eslint-disable-next-line max-len - 'Wie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegungWie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegungWie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegungWie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegungWie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegungWie genau ist die Cloud aufgebaut? Wieviel speicherplatz steht mir in der Cloud zur verfuegung', - ] - }, - { - keywordID: 2, - keyword: 'SWT', - questions: [ - 'Muss man fuer das Modul SWT bestanden haben?' - ] - }, - { - keywordID: 3, - keyword: 'Frage', - questions: [ - 'Das ist eine Lange Frage mit dem Thema \'frage\'', - 'Ich habe eine Frage, sind Fragen zum thema \'Frage\' auch erlaubt?', - 'Ich wollte Fragen ob sie gerne Sachen gefragt werden', - 'Langsam geht mir die Fragerei mit den ganzen Fragen auf den Geist Frage' - ] - }, - { - keywordID: 4, - keyword: 'Klausur', - questions: [ - 'Darf man in der Klausur hilfmittel verwenden?', - 'An welchem Termin findet die Klausur statt?' - ] - }, - { - keywordID: 5, - keyword: 'Diskrete Math', - questions: [ - 'wann wird die nächste veranstaltung stattfinden?', - 'gibt es heute übung?' - ] - }, - { - keywordID: 6, - keyword: 'Arsch', - questions: [ - 'Das ist eine Testfrage fuer den Profanity Filter, du Arschloch', - 'Englisch: Fuck you!', - 'Deutsch: Fick dich!', - 'Französisch: Gros con!', - 'Türkisch: Orospu çocuğu!', - 'Arabisch: عاهرة!', - 'Russisch: Муда!', - 'Multi language: Ficken, Fuck, con', - 'Custom: Nieder mit KQC' - ] - }, - ]; + allSelectedDE = true; + allSelectedEN = true; + spacyLabels: Labels; + wantedLabels: { + de: string[]; + en: string[]; + }; + + keywords: Keyword[] = []; private topicCloudAdminData: TopicCloudAdminData; - constructor(public cloudDialogRef: MatDialogRef<TagCloudComponent>, + constructor( + @Inject(MAT_DIALOG_DATA) public data: Data, + public cloudDialogRef: MatDialogRef<TopicCloudAdministrationComponent>, public confirmDialog: MatDialog, private notificationService: NotificationService, - private authenticationService: AuthenticationService, private translateService: TranslateService, private langService: LanguageService, - private topicCloudAdminService: TopicCloudAdminService) { + private topicCloudAdminService: TopicCloudAdminService, + private commentService: CommentService, + private wsCommentServiceService: WsCommentServiceService) { this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); } ngOnInit(): void { + this.wsCommentServiceService.getCommentStream(localStorage.getItem('roomId')).subscribe(_ => this.initKeywords()); + this.blacklistSubscription = this.topicCloudAdminService.getBlacklist().subscribe(list => this.blacklist = list); + this.isCreatorOrMod = this.data ? (this.data.user.role !== UserRole.PARTICIPANT) : true; this.translateService.use(localStorage.getItem('currentLang')); - this.checkIfUserIsModOrCreator(); - this.checkIfThereAreQuestions(); - this.sortQuestions(); + this.spacyLabels = spacyLabels; + this.wantedLabels = undefined; this.setDefaultAdminData(); + this.initKeywords(); } ngOnDestroy(){ this.setAdminData(); + if(this.blacklistSubscription !== undefined){ + this.blacklistSubscription.unsubscribe(); + } + } + + initKeywords(){ + this.commentService.getFilteredComments(localStorage.getItem('roomId')).subscribe(comments => { + this.keywords = []; + comments.map(comment => { + const keywords = this.keywordORfulltext === KeywordOrFulltext[0] ? comment.keywordsFromQuestioner : comment.keywordsFromSpacy; + keywords.map(_keyword => { + const existingKey = this.checkIfKeywordExists(_keyword); + if (existingKey){ + existingKey.vote += comment.score; + if (this.checkIfCommentExists(existingKey.comments, comment.id)){ + existingKey.comments.push(comment); + } + } else { + const keyword: Keyword = { + keyword: _keyword, + comments: [comment], + vote: comment.score + }; + this.keywords.push(keyword); + } + }); + }); + this.sortQuestions(); + }); + } + + checkIfCommentExists(comments: Comment[], id: string): boolean{ + return comments.filter(comment => comment.id === id).length === 0; } - setAdminData(){ + setAdminData() { this.topicCloudAdminData = { - blacklist: this.topicCloudAdminService.getBlacklistWords(this.profanityFilter, this.blacklistIsActive), + blacklist: [], + wantedLabels: { + de: this.wantedLabels.de, + en: this.wantedLabels.en + }, considerVotes: this.considerVotes, profanityFilter: this.profanityFilter, blacklistIsActive: this.blacklistIsActive, @@ -136,12 +133,16 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { } setDefaultAdminData() { - this.topicCloudAdminData = this.topicCloudAdminService.getAdminData; + this.topicCloudAdminData = this.topicCloudAdminService.getDefaultAdminData; if (this.topicCloudAdminData) { this.considerVotes = this.topicCloudAdminData.considerVotes; this.profanityFilter = this.topicCloudAdminData.profanityFilter; this.blacklistIsActive = this.topicCloudAdminData.blacklistIsActive; this.keywordORfulltext = KeywordOrFulltext[this.topicCloudAdminData.keywordORfulltext]; + this.wantedLabels = { + de: this.topicCloudAdminData.wantedLabels.de, + en: this.topicCloudAdminData.wantedLabels.en + }; } } @@ -150,11 +151,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { } getProfanityList() { - return this.topicCloudAdminService.getProfanityList(); - } - - getBlacklist() { - return this.topicCloudAdminService.getBlacklist(); + return this.topicCloudAdminService.getCustomProfanityList(); } sortQuestions(sortMode?: string) { @@ -167,27 +164,22 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { this.keywords.sort((a, b) => a.keyword.localeCompare(b.keyword)); break; case 'questionsCount': - this.keywords.sort((a, b) => b.questions.length - a.questions.length); + this.keywords.sort((a, b) => b.comments.length - a.comments.length); break; case 'voteCount': - console.log('not implemented!, sorting with question count'); - this.keywords.sort((a, b) => b.questions.length - a.questions.length); + this.keywords.sort((a, b) => b.vote - a.vote); break; } } - checkIfUserIsModOrCreator() { - this.isCreatorOrMod = this.authenticationService.getRole() === UserRole.CREATOR || - this.authenticationService.getRole() === UserRole.EDITING_MODERATOR || - this.authenticationService.getRole() === UserRole.EXECUTIVE_MODERATOR; - } - checkIfThereAreQuestions() { if (this.keywords.length === 0){ - this.translateService.get('topic-cloud-dialog.nokeyword-note').subscribe(msg => { + this.translateService.get('topic-cloud-dialog.no-keywords-note').subscribe(msg => { this.notificationService.show(msg); }); - this.cloudDialogRef.close(); + setTimeout(() => { + this.cloudDialogRef.close(); + }, 0); } } @@ -198,38 +190,68 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { }, 0); } - deleteKeyword(key: Keyword): void{ - this.keywords.map(keyword => { - if (keyword.keywordID === key.keywordID) { - this.keywords.splice(this.keywords.indexOf(keyword, 0), 1); - } + deleteKeyword(key: Keyword, message?: string): void{ + key.comments.map(comment => { + const changes = new TSMap<string, any>(); + let keywords = comment.keywordsFromQuestioner; + keywords.splice(keywords.indexOf(key.keyword, 0), 1); + changes.set('keywordsFromQuestioner', JSON.stringify(keywords)); + keywords = comment.keywordsFromSpacy; + keywords.splice(keywords.indexOf(key.keyword, 0), 1); + changes.set('keywordsFromSpacy', JSON.stringify(keywords)); + this.updateComment(comment, changes, message); }); - if (this.keywords.length === 0) { - this.cloudDialogRef.close(); - } + if (this.searchMode === true){ - /* update filtered array if it is searchmode */ this.searchKeyword(); } } + updateComment(updatedComment: Comment, changes: TSMap<string, any>, messageTranslate?: string){ + this.commentService.patchComment(updatedComment, changes).subscribe(_ => { + if (messageTranslate){ + this.translateService.get('topic-cloud-dialog.' + messageTranslate).subscribe(msg => { + this.notificationService.show(msg); + }); + } + }, + error => { + this.translateService.get('topic-cloud-dialog.changes-gone-wrong').subscribe(msg => { + this.notificationService.show(msg); + }); + }); + } + cancelEdit(): void { this.edit = false; this.newKeyword = undefined; } confirmEdit(key: Keyword): void { - for (const keyword of this.keywords){ - if (keyword.keywordID === key.keywordID) { - const key2 = this.checkIfKeywordExists(this.newKeyword.trim().toLowerCase()); - if (key2){ - this.openConfirmDialog('merge-message', 'merge', keyword, key2); - } else { - keyword.keyword = this.newKeyword.trim(); + const key2 = this.checkIfKeywordExists(this.newKeyword); + if (key2){ + this.openConfirmDialog('merge-message', 'merge', key, key2); + } else { + key.comments.map(comment => { + const changes = new TSMap<string, any>(); + let keywords = comment.keywordsFromQuestioner; + for (let i = 0; i < keywords.length; i++){ + if (keywords[i].toLowerCase() === key.keyword.toLowerCase()){ + keywords[i] = this.newKeyword.trim(); + } } - break; - } + changes.set('keywordsFromQuestioner', JSON.stringify(keywords)); + keywords = comment.keywordsFromSpacy; + for (let i = 0; i < keywords.length; i++){ + if (keywords[i].toLowerCase() === key.keyword.toLowerCase()){ + keywords[i] = this.newKeyword.trim(); + } + } + changes.set('keywordsFromSpacy', JSON.stringify(keywords)); + this.updateComment(comment, changes, 'keyword-edit'); + }); } + this.edit = false; this.newKeyword = undefined; this.sortQuestions(); @@ -246,7 +268,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { confirmDialogRef.afterClosed().subscribe(result => { if (result === 'delete') { - this.deleteKeyword(keyword); + this.deleteKeyword(keyword, 'keyword-delete'); } else if (result === 'merge') { this.mergeKeywords(keyword, mergeTarget); } @@ -266,8 +288,17 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { mergeKeywords(key1: Keyword, key2: Keyword) { if (key1 !== undefined && key2 !== undefined){ - key1.questions.map(question => { - key2.questions.push(question); + key1.comments.map(comment => { + if (this.checkIfCommentExists(key2.comments, comment.id)){ + const changes = new TSMap<string, any>(); + let keywords = comment.keywordsFromQuestioner; + keywords.push(key2.keyword); + changes.set('keywordsFromQuestioner', JSON.stringify(keywords)); + keywords = comment.keywordsFromSpacy; + keywords.push(key2.keyword); + changes.set('keywordsFromSpacy', JSON.stringify(keywords)); + this.updateComment(comment, changes); + } }); this.deleteKeyword(key1); } @@ -275,7 +306,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { checkIfKeywordExists(key: string): Keyword { for(const keyword of this.keywords){ - if(keyword.keyword.toLowerCase() === key){ + if(keyword.keyword.toLowerCase() === key.trim().toLowerCase()){ return keyword; } } @@ -297,7 +328,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { } addBlacklistWord() { - this.topicCloudAdminService.addToBlacklistWordList(this.newBlacklistWord); + this.topicCloudAdminService.addWordToBlacklist(this.newBlacklistWord); this.newBlacklistWord = undefined; if (this.searchMode){ this.searchKeyword(); @@ -312,13 +343,40 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { this.topicCloudAdminService.removeWordFromBlacklist(word); } - refreshAllLists(){ + refreshAllLists() { this.searchKeyword(); } + + selectAllDE() { + if (this.allSelectedDE) { + this.wantedLabels.de = [] + } else { + this.wantedLabels.de = []; + this.spacyLabels.de.forEach(label => { + this.wantedLabels.de.push(label.tag); + }); + } + } + + selectAllEN() { + if (this.allSelectedEN) { + this.wantedLabels.en = []; + this.spacyLabels.en.forEach(label => { + this.wantedLabels.en.push(label.tag); + }); + } else { + this.wantedLabels.en = [] + } + } } interface Keyword { - keywordID: number; keyword: string; - questions: string[]; + comments: Comment[]; + vote: number; } + +export interface Data{ + user: User; +} + 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 e6708d4e7568808724ff1354fd08f45a2145fc79..9c3ef6cf85eca46ec4d7f812f56a19f6f8fdcb99 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -366,6 +366,7 @@ export class CommentListComponent implements OnInit, OnDestroy { c.timestamp = payload.timestamp; c.tag = payload.tag; c.creatorId = payload.creatorId; + c.keywordsFromQuestioner = JSON.parse(payload.keywordsFromQuestioner); c.userNumber = this.commentService.hashCode(c.creatorId); this.commentService.getComment(c.id).subscribe(e => { c.number = e.number; diff --git a/src/app/components/shared/header/header.component.ts b/src/app/components/shared/header/header.component.ts index 65d387666d2b9ac11a2935dfbf73971d1b319a73..c3842163ab6ec31498753ccb4abf41f0ae106357 100644 --- a/src/app/components/shared/header/header.component.ts +++ b/src/app/components/shared/header/header.component.ts @@ -23,7 +23,7 @@ import { MotdService } from '../../../services/http/motd.service'; import { TopicCloudFilterComponent } from '../_dialogs/topic-cloud-filter/topic-cloud-filter.component'; import { RoomService } from '../../../services/http/room.service'; import { Room } from '../../../models/room'; -import { TagCloudMetaData } from '../tag-cloud/tag-cloud.data-manager'; +import { TagCloudMetaData } from '../../../services/util/tag-cloud-data.service'; @Component({ selector: 'app-header', diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 0fe7715ac88a1eada7caf515b44614ca0fbeed45..9e1fa76e707e4fe232456bbe490910b5f1b96b51 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -36,6 +36,7 @@ import { TopicCloudAdministrationComponent } from './_dialogs/topic-cloud-admini import { TopicDialogCommentComponent } from './dialog/topic-dialog-comment/topic-dialog-comment.component'; import { TopicCloudFilterComponent } from './_dialogs/topic-cloud-filter/topic-cloud-filter.component'; import { SpacyDialogComponent } from './_dialogs/spacy-dialog/spacy-dialog.component'; +import { TagCloudPopUpComponent } from './tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component'; @NgModule({ imports: [ @@ -77,7 +78,8 @@ import { SpacyDialogComponent } from './_dialogs/spacy-dialog/spacy-dialog.compo TopicCloudAdministrationComponent, TopicDialogCommentComponent, TopicCloudFilterComponent, - SpacyDialogComponent + SpacyDialogComponent, + TagCloudPopUpComponent ], exports: [ RoomJoinComponent, @@ -93,7 +95,8 @@ import { SpacyDialogComponent } from './_dialogs/spacy-dialog/spacy-dialog.compo CommentComponent, DialogActionButtonsComponent, UserBonusTokenComponent, - CloudConfigurationComponent + CloudConfigurationComponent, + TagCloudPopUpComponent ] }) export class SharedModule { diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html new file mode 100644 index 0000000000000000000000000000000000000000..65e367adb959663a8e22fe6966624e26368b9c44 --- /dev/null +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html @@ -0,0 +1,39 @@ +<div #popupContainer + class="popupContainer" + (focusout)="onFocus($event)" + tabindex="0"> + <span> + <mat-icon matTooltip="{{'tag-cloud.overview-question-topic-tooltip' | translate}}">comment</mat-icon> + <p> + {{tagData && tagData.comments.length}} + </p> + <mat-icon matTooltip="{{'tag-cloud.overview-questioners-topic-tooltip' | translate}}">person</mat-icon> + <p> + {{tagData && tagData.distinctUsers.size}} + </p> + <mat-icon matTooltip="{{'tag-cloud.upvote-topic' | translate}}">thumb_up</mat-icon> + <p> + {{tagData && tagData.cachedUpVotes}} + </p> + <mat-icon matTooltip="{{'tag-cloud.downvote-topic' | translate}}">thumb_down</mat-icon> + <p> + {{tagData && tagData.cachedDownVotes}} + </p> + <button *ngIf="user && user.role >= 1" mat-button (click)="addBlacklistWord()"> + <mat-icon matTooltip="{{'tag-cloud.blacklist-topic' | translate}}">gavel</mat-icon> + </button> + </span> + <br> + <span> + <mat-icon matTooltip="{{'tag-cloud.period-since-first-comment' | translate}}">date_range</mat-icon> + <p> + {{timePeriodText}} + </p> + </span> + <div *ngIf="categories && categories.length"> + <p>Kategorien:</p> + <ul> + <li *ngFor="let category of categories">{{category}}</li> + </ul> + </div> +</div> diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..057e27b57a667a064649da4332cadcb7b5a40733 --- /dev/null +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss @@ -0,0 +1,118 @@ +$popup-arrow-size: 20px; +$popup-arrow-border-size: 2px; +$popup-arrow-half-size: $popup-arrow-size / 2; +$popup-arrow-offset: $popup-arrow-half-size + $popup-arrow-border-size; +$header-size: 67px; + +.popupContainer { + visibility: hidden; + border-radius: 25px; + border: 2px solid #000; + background-color: var(--dialog); + padding: $popup-arrow-half-size; + position: absolute; + box-shadow: 0 0 10px var(--dialog); + box-sizing: border-box; + z-index: 3; + color: var(--on-dialog); + transform: translateY(-$header-size); + + &:focus { + outline: none; + } + + &::after { + position: absolute; + content: ''; + width: $popup-arrow-size; + height: $popup-arrow-size; + background-color: var(--dialog); + border-top: 0 solid #000; + border-right: $popup-arrow-border-size solid #000; + border-left: 0 solid #000; + border-bottom: $popup-arrow-border-size solid #000; + } + + &.down { + visibility: unset; + transform: translate(-50%, calc(-100% - #{$header-size + $popup-arrow-offset})); + + &::after { + top: 100%; + left: 50%; + margin-top: -$popup-arrow-half-size; + margin-left: -$popup-arrow-half-size; + transform: rotate(45deg); + } + } + + &.left { + visibility: unset; + transform: translate($popup-arrow-offset, calc(-50% - #{$header-size})); + + &::after { + top: 50%; + right: 100%; + margin-top: -$popup-arrow-half-size; + margin-right: -$popup-arrow-half-size; + transform: rotate(135deg); + } + } + + &.up { + visibility: unset; + transform: translate(-50%, $popup-arrow-offset - $header-size); + + &::after { + bottom: 100%; + left: 50%; + margin-bottom: -$popup-arrow-half-size; + margin-left: -$popup-arrow-half-size; + transform: rotate(225deg); + } + } + + &.right { + visibility: unset; + transform: translate(calc(-100% - #{$popup-arrow-offset}), calc(-50% - #{$header-size})); + + &::after { + top: 50%; + left: 100%; + margin-top: -$popup-arrow-half-size; + margin-left: -$popup-arrow-half-size; + transform: rotate(315deg); + } + } +} + +div > p { + margin-bottom: 1px; +} + +span { + margin-right: 5px; + + & > mat-icon { + margin: -1px 0px 0px 12px; + vertical-align: middle; + } + + & > p { + display: inline; + font-weight: 600; + vertical-align: middle; + } +} + +ul { + margin: 0; +} + +button { + color: var(--red); + min-width: unset; + margin-left: 0; + padding-left: 10px; + padding-right: 10px; +} diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.spec.ts b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..be4c0a85dfd377daf9509432ffd793ae52fed9fe --- /dev/null +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.spec.ts @@ -0,0 +1,26 @@ +/*import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TagCloudPopUpComponent } from './tag-cloud-pop-up.component'; + +describe('TagCloudPopUpComponent', () => { + let component: TagCloudPopUpComponent; + let fixture: ComponentFixture<TagCloudPopUpComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TagCloudPopUpComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TagCloudPopUpComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); +*/ diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b3210c4330c47da7851821ffbf8b9e268d176f0 --- /dev/null +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts @@ -0,0 +1,233 @@ +import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { LanguageService } from '../../../../services/util/language.service'; +import { TagCloudComponent } from '../tag-cloud.component'; +import { AuthenticationService } from '../../../../services/http/authentication.service'; +import { User } from '../../../../models/user'; +import { TagCloudDataTagEntry } from '../../../../services/util/tag-cloud-data.service'; + +const CLOSE_TIME = 1500; + +@Component({ + selector: 'app-tag-cloud-pop-up', + templateUrl: './tag-cloud-pop-up.component.html', + styleUrls: ['./tag-cloud-pop-up.component.scss'] +}) +export class TagCloudPopUpComponent implements OnInit, AfterViewInit { + + @Input() parent: TagCloudComponent; + @ViewChild('popupContainer') popupContainer: ElementRef; + tag: string; + tagData: TagCloudDataTagEntry; + categories: string[]; + timePeriodText: string; + user: User; + private _popupHoverTimer: number; + private _popupCloseTimer: number; + + constructor(private langService: LanguageService, + private translateService: TranslateService, + private authenticationService: AuthenticationService) { + this.langService.langEmitter.subscribe(lang => { + this.translateService.use(lang); + }); + } + + ngOnInit(): void { + this.timePeriodText = '...'; + this.authenticationService.watchUser.subscribe(newUser => { + if (newUser) { + this.user = newUser; + } + }); + } + + ngAfterViewInit() { + const html = this.popupContainer.nativeElement as HTMLDivElement; + html.addEventListener('mouseenter', () => { + clearTimeout(this._popupCloseTimer); + }); + html.addEventListener('mouseleave', () => { + this._popupCloseTimer = setTimeout(() => { + this.close(); + }, CLOSE_TIME); + }); + } + + onFocus(event) { + if (!this.popupContainer.nativeElement.contains(event.target)) { + this.close(); + } + } + + leave(): void { + clearTimeout(this._popupHoverTimer); + this._popupCloseTimer = setTimeout(() => { + this.close(); + }, CLOSE_TIME); + } + + enter(elem: HTMLElement, tag: string, tagData: TagCloudDataTagEntry, hoverDelayInMs: number): void { + clearTimeout(this._popupCloseTimer); + clearTimeout(this._popupHoverTimer); + this._popupHoverTimer = setTimeout(() => { + this.tag = tag; + this.tagData = tagData; + this.categories = Array.from(tagData.categories.keys()); + this.calculateDateText(() => { + this.position(elem); + }); + }, hoverDelayInMs); + } + + addBlacklistWord(): void { + this.parent.dataManager.blockWord(this.tag); + this.close(); + } + + close(): void { + const html = this.popupContainer.nativeElement as HTMLDivElement; + html.classList.remove('up', 'down', 'right', 'left'); + } + + private position(elem: HTMLElement) { + const html = this.popupContainer.nativeElement as HTMLDivElement; + const popup = html.getBoundingClientRect(); + const tag = elem.getBoundingClientRect(); + const boundingBox = elem.parentElement.getBoundingClientRect(); + // calculate the free space to the left, right, top and bottom from tag + const spaceLeft = tag.x + tag.width / 2; + const spaceRight = boundingBox.right - tag.right + tag.width / 2; + const spaceTop = tag.y - boundingBox.y; + const spaceBottom = boundingBox.bottom - tag.bottom; + // set flags if tag is near bounding box + const isLeft = spaceLeft <= popup.width / 2.0; + const isRight = spaceRight <= popup.width / 2.0; + const isTop = spaceTop <= popup.height; + const isBottom = spaceBottom <= popup.height; + + // try to make a decision where to place the popup outgoing from tag with checks if we are at a border of the viewport + enum PopupPosition { + top, + bottom, + left, + right + } + + let dockingPosition; + if (isLeft && isTop && !isBottom && !isRight) { + dockingPosition = PopupPosition.right; + } else if (isTop && !isLeft && !isRight && !isBottom) { + dockingPosition = PopupPosition.bottom; + } else if (isRight && isTop && !isLeft && !isBottom) { + dockingPosition = PopupPosition.left; + } else if (isLeft && !isTop && !isRight && !isBottom) { + dockingPosition = PopupPosition.right; + } else if (!isLeft && !isTop && !isRight && !isBottom) { + // default docking when all sides offer enough space + dockingPosition = PopupPosition.top; + } else if (isRight && !isTop && !isLeft && !isBottom) { + dockingPosition = PopupPosition.left; + } else if (isLeft && isBottom && !isTop && !isRight) { + dockingPosition = PopupPosition.right; + } else if (!isLeft && isBottom && !isTop && !isRight) { + dockingPosition = PopupPosition.top; + } else if (!isLeft && isBottom && isTop && !isRight) { + dockingPosition = PopupPosition.left; + } else { + /* + * Find solution for small screens when all sides produce unpleasant results + */ + dockingPosition = PopupPosition.top; + } + html.classList.remove('left', 'right', 'up', 'down'); + if (dockingPosition === PopupPosition.bottom) { + html.style.top = tag.bottom + 'px'; + html.style.left = tag.x + tag.width / 2 + 'px'; + html.classList.add('up'); + } else if (dockingPosition === PopupPosition.top) { + html.style.top = tag.y + 'px'; + html.style.left = tag.x + tag.width / 2 + 'px'; + html.classList.add('down'); + } else if (dockingPosition === PopupPosition.left) { + html.style.top = tag.top + tag.height / 2 + 'px'; + html.style.left = tag.x + 'px'; + html.classList.add('right'); + } else if (dockingPosition === PopupPosition.right) { + html.style.top = tag.top + tag.height / 2 + 'px'; + html.style.left = tag.right + 'px'; + html.classList.add('left'); + } + html.focus(); + } + + private calculateDateText(afterInit: () => void): void { + const subscriber = (e: string) => { + this.timePeriodText = e; + if (afterInit) { + setTimeout(afterInit); + } + }; + // @ts-ignore + const diffMs = Date.now() - Date.parse(this.tagData.firstTimeStamp); + const seconds = Math.floor(diffMs / 1_000); + if (seconds < 60) { + // few seconds + this.translateService.get('tag-cloud-popup.few-seconds').subscribe(subscriber); + return; + } + const minutes = Math.floor(seconds / 60); + if (minutes < 5) { + // few minutes + this.translateService.get('tag-cloud-popup.few-minutes').subscribe(subscriber); + return; + } else if (minutes < 60) { + // x minutes + this.translateService.get('tag-cloud-popup.some-minutes', { + minutes + }).subscribe(subscriber); + return; + } + const hours = Math.floor(minutes / 60); + if (hours === 1) { + // 1 hour + this.translateService.get('tag-cloud-popup.one-hour').subscribe(subscriber); + return; + } else if (hours < 24) { + // x hours + this.translateService.get('tag-cloud-popup.some-hours', { + hours + }).subscribe(subscriber); + return; + } + const days = Math.floor(hours / 24); + if (days === 1) { + // 1 day + this.translateService.get('tag-cloud-popup.one-day').subscribe(subscriber); + return; + } else if (days < 7) { + // x days + this.translateService.get('tag-cloud-popup.some-days', { + days + }).subscribe(subscriber); + return; + } + const weeks = Math.floor(days / 7); + if (weeks === 1) { + // 1 week + this.translateService.get('tag-cloud-popup.one-week').subscribe(subscriber); + return; + } else if (weeks < 12) { + // x weeks + this.translateService.get('tag-cloud-popup.some-weeks', { + weeks + }).subscribe(subscriber); + return; + } + const months = Math.floor(weeks / 4); + // x months + this.translateService.get('tag-cloud-popup.some-months', { + months + }).subscribe(subscriber); + } +} diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.html b/src/app/components/shared/tag-cloud/tag-cloud.component.html index 509971aa8bae17c92eb112e1b9e6ad0cf07c01fe..6f6afd01783495837b2d1ac015d6a7644755fc2c 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.html +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.html @@ -5,13 +5,14 @@ </mat-drawer> <mat-drawer-content> <ars-fill ars-flex-box> + <app-tag-cloud-pop-up [parent]="this"></app-tag-cloud-pop-up> <div [ngClass]="{'hidden': !isLoading}" fxLayout="row" fxLayoutAlign="center center" fxFill> <mat-progress-spinner *ngIf="isLoading" mode="indeterminate"></mat-progress-spinner> </div> <angular-tag-cloud + id="tagCloudComponent" class="spacyTagCloud" (window:resize)="onResize($event)" - (afterInit)="initTagCloud()" (clicked)="openTags($event)" [data]="data" [width]="options.width" diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.scss b/src/app/components/shared/tag-cloud/tag-cloud.component.scss index adfc5f5d2534942cfe624e704b87e16e83ba393d..7304a3c38ffadd188d72504dc7e86d20eef15cb3 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.scss +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.scss @@ -1,3 +1,6 @@ +$header-size: 67px; +$margin: 15px; + .mat-drawer.mat-drawer-push { background-color: var(--background); } @@ -7,16 +10,16 @@ mat-drawer { } ars-fill { - width: calc(100% - 30px); - height: calc(100% - 30px); - margin: 15px; + width: calc(100% - #{2 * $margin}); + height: calc(100% - #{2 * $margin}); + margin: $margin; } mat-drawer-container { - height: calc(100% - 67px); + height: calc(100% - #{$header-size}); width: 100%; position: fixed; - margin-top: 67px; + margin-top: $header-size; } mat-drawer-content { @@ -31,3 +34,12 @@ mat-drawer-content { ::ng-deep mat-progress-spinner circle { stroke: var(--on-background) !important; } + +app-tag-cloud-pop-up { + width: max-content; + height: max-content; +} + +::ng-deep .spacyTagCloud span { + user-select: none !important; +} diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.ts b/src/app/components/shared/tag-cloud/tag-cloud.component.ts index 74565ecbcdfea3530e546eb9ffd9743ec8139562..451e5bb905137f85500b9aa7824f08e4aef690dc 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.ts @@ -23,8 +23,10 @@ import { ThemeService } from '../../../../theme/theme.service'; import { cloneParameters, CloudParameters, CloudTextStyle, CloudWeightSettings } from './tag-cloud.interface'; import { TopicCloudAdministrationComponent } from '../_dialogs/topic-cloud-administration/topic-cloud-administration.component'; import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; -import { TagCloudDataManager } from './tag-cloud.data-manager'; import { CreateCommentWrapper } from '../../../utils/CreateCommentWrapper'; +import { TopicCloudAdminService } from '../../../services/util/topic-cloud-admin.service'; +import { TagCloudPopUpComponent } from './tag-cloud-pop-up/tag-cloud-pop-up.component'; +import { TagCloudDataService, TagCloudDataTagEntry } from '../../../services/util/tag-cloud-data.service'; class CustomPosition implements Position { left: number; @@ -34,23 +36,26 @@ class CustomPosition implements Position { public relativeTop: number) { } - updatePosition(width: number, height: number, text: string, style: CSSStyleDeclaration) { - const offsetY = parseFloat(style.height) / 2; - const offsetX = parseFloat(style.width) / 2; + updatePosition(width: number, height: number, metrics: TextMetrics) { + const offsetY = (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) / 2; + const offsetX = metrics.width / 2; this.left = width * this.relativeLeft - offsetX; this.top = height * this.relativeTop - offsetY; } } class TagComment implements CloudData { - constructor(public color: string, - public external: boolean, - public link: string, - public position: Position, + + constructor(public text: string, public rotate: number, - public text: string, - public tooltip: string, - public weight: number) { + public weight: number, + public tagData: TagCloudDataTagEntry, + public index: number, + public color: string = null, + public external: boolean = false, + public link: string = null, + public position: Position = null, + public tooltip: string = null) { } } @@ -129,6 +134,7 @@ const getDefaultCloudParameters = (): CloudParameters => { export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild(TCloudComponent, {static: false}) child: TCloudComponent; + @ViewChild(TagCloudPopUpComponent) popup: TagCloudPopUpComponent; @Input() user: User; @Input() roomId: string; room: Room; @@ -157,10 +163,12 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { //Subscriptions headerInterface = null; themeSubscription = null; - readonly dataManager: TagCloudDataManager; private _currentSettings: CloudParameters; private _createCommentWrapper: CreateCommentWrapper = null; private _subscriptionCommentlist = null; + private _calcCanvas: HTMLCanvasElement = null; + private _calcRenderContext: CanvasRenderingContext2D = null; + private _calcFont: string = null; constructor(private commentService: CommentService, private langService: LanguageService, @@ -173,13 +181,16 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { protected roomService: RoomService, private themeService: ThemeService, private wsCommentService: WsCommentServiceService, - private router: Router) { + private topicCloudAdmin: TopicCloudAdminService, + private router: Router, + public dataManager: TagCloudDataService) { this.roomId = localStorage.getItem('roomId'); this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); - this.dataManager = new TagCloudDataManager(wsCommentService, commentService); this._currentSettings = TagCloudComponent.getCurrentCloudParameters(); + this._calcCanvas = document.createElement('canvas'); + this._calcRenderContext = this._calcCanvas.getContext('2d'); } private static getCurrentCloudParameters(): CloudParameters { @@ -207,7 +218,10 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { } else if (e === 'topicCloudAdministration') { this.dialog.open(TopicCloudAdministrationComponent, { minWidth: '50%', - maxHeight: '80%' + maxHeight: '80%', + data: { + user: this.user + } }); } }); @@ -251,25 +265,20 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { ngAfterViewInit() { document.getElementById('footer_rescale').style.display = 'none'; + this._calcFont = window.getComputedStyle(document.getElementById('tagCloudComponent')).fontFamily; + this.dataManager.bindToRoom(this.roomId); + this.dataManager.updateDemoData(this.translateService); + this.setCloudParameters(TagCloudComponent.getCurrentCloudParameters(), false); } ngOnDestroy() { document.getElementById('footer_rescale').style.display = 'block'; this.headerInterface.unsubscribe(); this.themeSubscription.unsubscribe(); - this.dataManager.deactivate(); - } - - initTagCloud() { - this.dataManager.activate(this.roomId); - this.dataManager.updateDemoData(this.translateService); - this.setCloudParameters(TagCloudComponent.getCurrentCloudParameters(), false); - setTimeout(() => { - this.redraw(); - }); + this.dataManager.unbindRoom(); } - get tagCloudDataManager(): TagCloudDataManager { + get tagCloudDataManager(): TagCloudDataService { return this.dataManager; } @@ -325,7 +334,7 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { if (rotation === null || this._currentSettings.randomAngles) { rotation = Math.floor(Math.random() * 30 - 15); } - newElements.push(new TagComment(null, true, null, null, rotation, tag, 'TODO', tagData.weight)); + newElements.push(new TagComment(tag, rotation, tagData.weight, tagData, newElements.length)); } } } @@ -339,6 +348,7 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { newElements[i].position = new CustomPosition((k + 1) / (size + 1), (line + 1) / (lines + 1)); } } + this.updateAlphabeticalPosition(newElements); } this.data = newElements; setTimeout(() => { @@ -349,31 +359,23 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { updateTagCloud(dataUpdated = false) { this.isLoading = true; if (this._currentSettings.sortAlphabetically && this.data.length) { - if (dataUpdated || !this.child.cloudDataHtmlElements || !this.child.cloudDataHtmlElements.length) { - this.child.reDraw(); - } - const width = this.child.calculatedWidth; - const height = this.child.calculatedHeight; - this.data.forEach((e, i) => { - (e.position as CustomPosition).updatePosition(width, height, e.text, - window.getComputedStyle(this.child.cloudDataHtmlElements[i])); - }); + this.updateAlphabeticalPosition(this.data); } const debounceTime = 1_000; const current = new Date().getTime(); const diff = current - this.lastDebounceTime; if (diff >= debounceTime) { - this.redraw(); + this.redraw(dataUpdated); } else { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { - this.redraw(); + this.redraw(dataUpdated); }, debounceTime - diff); } } openTags(tag: CloudData): void { - if(this._subscriptionCommentlist !== null){ + if (this._subscriptionCommentlist !== null) { return; } this._subscriptionCommentlist = this.eventService.on('commentListCreated').subscribe(() => { @@ -384,17 +386,42 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { this.router.navigate(['../'], {relativeTo: this.route}); } - private redraw(): void { + private updateAlphabeticalPosition(elements: TagComment[]): void { + const sizes = new Array(10); + const fontRange = (this._currentSettings.fontSizeMax - this._currentSettings.fontSizeMin) / 10; + for (let i = 1; i <= 10; i++) { + sizes[i - 1] = (this._currentSettings.fontSizeMin + fontRange * i).toFixed(0) + '%'; + } + const width = this.child.calculatedWidth; + const height = this.child.calculatedHeight; + elements.forEach((e, i) => { + this._calcRenderContext.font = sizes[e.tagData.adjustedWeight] + ' ' + this._calcFont; + (e.position as CustomPosition).updatePosition(width, height, this._calcRenderContext.measureText(e.text)); + }); + } + + private redraw(dataUpdate: boolean): void { if (this.child === undefined) { return; } this.lastDebounceTime = new Date().getTime(); - this.child.reDraw(); this.isLoading = false; + if (!dataUpdate) { + this.child.reDraw(); + } // This should fix the hover bug (scale was not turned off sometimes) - this.child.cloudDataHtmlElements.forEach(elem => { + if (this.dataManager.currentData === null) { + return; + } + this.child.cloudDataHtmlElements.forEach((elem, i) => { + const dataElement = this.data[i]; elem.addEventListener('mouseleave', () => { elem.style.transform = elem.style.transform.replace(transformationScaleKiller, '').trim(); + this.popup.leave(); + }); + elem.addEventListener('mouseenter', () => { + this.popup.enter(elem, dataElement.text, dataElement.tagData, + (this._currentSettings.hoverTime + this._currentSettings.hoverDelay) * 1_000); }); }); } @@ -425,7 +452,8 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { (this._currentSettings.fontSizeMin + fontRange * i).toFixed(0) + '%; }', rules.length); } customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span:hover, .spacyTagCloud > span:hover > a { color: ' + - this._currentSettings.fontColor + '; }', rules.length); + this._currentSettings.fontColor + '; background-color: ' + + this._currentSettings.backgroundColor + '; }', rules.length); customTagCloudStyles.sheet.insertRule('.spacyTagCloudContainer { background-color: ' + this._currentSettings.backgroundColor + '; }', rules.length); } diff --git a/src/app/models/comment.ts b/src/app/models/comment.ts index 2c00741a2d8fcf731699596822cea5716a524ebc..c857181c08a6730ac351acfd3c5ecbce31b83d52 100644 --- a/src/app/models/comment.ts +++ b/src/app/models/comment.ts @@ -1,5 +1,4 @@ import { CorrectWrong } from './correct-wrong.enum'; -import { ViewChild } from '@angular/core'; export class Comment { id: string; @@ -22,6 +21,8 @@ export class Comment { number: number; keywordsFromQuestioner: string[]; keywordsFromSpacy: string[]; + upvotes: number; + downvotes: number; constructor(roomId: string = '', creatorId: string = '', @@ -39,7 +40,9 @@ export class Comment { answer: string = '', userNumber: number = 0, keywordsFromQuestioner: string[] = [], - keywordsFromSpacy: string[] = []) { + keywordsFromSpacy: string[] = [], + upvotes = 0, + downvotes = 0) { this.id = ''; this.roomId = roomId; this.creatorId = creatorId; @@ -59,5 +62,7 @@ export class Comment { this.userNumber = userNumber; this.keywordsFromQuestioner = keywordsFromQuestioner; this.keywordsFromSpacy = keywordsFromSpacy; + this.upvotes = upvotes; + this.downvotes = downvotes; } } diff --git a/src/app/models/room.ts b/src/app/models/room.ts index e83d37ce5b7980f6243cc482754bf1876d594211..1eaaa39ff1471e370993f4eaf2d317e8a86e953b 100644 --- a/src/app/models/room.ts +++ b/src/app/models/room.ts @@ -8,6 +8,7 @@ export class Room { abbreviation: string; name: string; description: string; + blacklist: string; closed: boolean; moderated: boolean; directSend: boolean; @@ -20,6 +21,7 @@ export class Room { abbreviation: string = '', name: string = '', description: string = '', + blacklist: string = '[]', closed: boolean = false, moderated: boolean = true, directSend: boolean = true, @@ -32,6 +34,7 @@ export class Room { this.abbreviation = abbreviation; this.name = name; this.description = description; + this.blacklist = blacklist; this.closed = closed; this.moderated = moderated; this.directSend = directSend; diff --git a/src/app/services/http/comment.service.ts b/src/app/services/http/comment.service.ts index 9fc98c9699a578543390582d9a0b6dd358bc0ff4..a5a1a0aeff10c0a40c0323305f19d6fc504c6689 100644 --- a/src/app/services/http/comment.service.ts +++ b/src/app/services/http/comment.service.ts @@ -72,15 +72,7 @@ export class CommentService extends BaseHttpService { getComment(commentId: string): Observable<Comment> { const connectionUrl = `${this.apiUrl.base}${this.apiUrl.comment}/${commentId}`; return this.http.get<Comment>(connectionUrl, httpOptions).pipe( - map(comment => { - const newComment = this.parseUserNumber(comment); - newComment.keywordsFromQuestioner = - // @ts-ignore - newComment.keywordsFromQuestioner ? JSON.parse(newComment.keywordsFromQuestioner) as string[] : null; - newComment.keywordsFromSpacy = - // @ts-ignore - newComment.keywordsFromSpacy ? JSON.parse(newComment.keywordsFromSpacy as string[]) : null; - return newComment;}), + map(comment => this.parseComment(comment)), tap(_ => ''), catchError(this.handleError<Comment>('getComment')) ); @@ -95,9 +87,9 @@ export class CommentService extends BaseHttpService { keywordsFromSpacy: JSON.stringify(comment.keywordsFromSpacy), keywordsFromQuestioner: JSON.stringify(comment.keywordsFromQuestioner) }, httpOptions).pipe( - tap(_ => ''), - catchError(this.handleError<Comment>('addComment')) - ); + tap(_ => ''), + catchError(this.handleError<Comment>('addComment')) + ); } deleteComment(commentId: string): Observable<Comment> { @@ -118,17 +110,7 @@ export class CommentService extends BaseHttpService { properties: { roomId: roomId, ack: true }, externalFilters: {} }, httpOptions).pipe( - map(commentList => commentList.map(comment => { - const newComment = this.parseUserNumber(comment); - newComment.keywordsFromQuestioner = - // @ts-ignore - newComment.keywordsFromQuestioner ? JSON.parse(newComment.keywordsFromQuestioner) as string[] : null; - console.log(newComment.keywordsFromQuestioner); - newComment.keywordsFromSpacy = - // @ts-ignore - newComment.keywordsFromSpacy ? JSON.parse(newComment.keywordsFromSpacy as string[]) : null; - return newComment; - })), + map(commentList => commentList.map(comment => this.parseComment(comment))), tap(_ => ''), catchError(this.handleError<Comment[]>('getComments', [])) ); @@ -141,15 +123,7 @@ export class CommentService extends BaseHttpService { externalFilters: {} }, httpOptions).pipe( map(commentList => { - return commentList.map(comment => { - const newComment = this.parseUserNumber(comment); - newComment.keywordsFromQuestioner = - // @ts-ignore - newComment.keywordsFromQuestioner ? JSON.parse(newComment.keywordsFromQuestioner) as string[] : null; - newComment.keywordsFromSpacy = - // @ts-ignore - newComment.keywordsFromSpacy ? JSON.parse(newComment.keywordsFromSpacy as string[]) : null; - return newComment;}); + return commentList.map(comment => this.parseComment(comment)); }), tap(_ => ''), catchError(this.handleError<Comment[]>('getComments', [])) @@ -163,15 +137,7 @@ export class CommentService extends BaseHttpService { externalFilters: {} }, httpOptions).pipe( map(commentList => { - return commentList.map(comment => { - const newComment = this.parseUserNumber(comment); - newComment.keywordsFromQuestioner = - // @ts-ignore - newComment.keywordsFromQuestioner ? JSON.parse(newComment.keywordsFromQuestioner) as string[] : null; - newComment.keywordsFromSpacy = - // @ts-ignore - newComment.keywordsFromSpacy ? JSON.parse(newComment.keywordsFromSpacy as string[]) : null; - return newComment;}); + return commentList.map(comment => this.parseComment(comment)); }), tap(_ => ''), catchError(this.handleError<Comment[]>('getComments', [])) @@ -258,8 +224,11 @@ export class CommentService extends BaseHttpService { } - parseUserNumber(comment: Comment): Comment { + parseComment(comment: Comment): Comment { comment.userNumber = this.hashCode(comment.creatorId); + // make list out of string "array" + comment.keywordsFromQuestioner = comment.keywordsFromQuestioner ? JSON.parse(comment.keywordsFromQuestioner as unknown as string) : null; + comment.keywordsFromSpacy = comment.keywordsFromSpacy ? JSON.parse(comment.keywordsFromSpacy as unknown as string) : null; return comment; } diff --git a/src/app/services/util/tag-cloud-data.service.spec.ts b/src/app/services/util/tag-cloud-data.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee5ff3e99990ea480d89a1d867eb3f7d1236aa89 --- /dev/null +++ b/src/app/services/util/tag-cloud-data.service.spec.ts @@ -0,0 +1,16 @@ +/*import { TestBed } from '@angular/core/testing'; + +import { TagCloudDataService } from './tag-cloud-data.service'; + +describe('TagCloudDataService', () => { + let service: TagCloudDataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TagCloudDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +});*/ diff --git a/src/app/components/shared/tag-cloud/tag-cloud.data-manager.ts b/src/app/services/util/tag-cloud-data.service.ts similarity index 58% rename from src/app/components/shared/tag-cloud/tag-cloud.data-manager.ts rename to src/app/services/util/tag-cloud-data.service.ts index 2c870737338f2f0263899efac73ffdd491369db3..48e81f9512fb43f10f050551c026c574dc538509 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.data-manager.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -1,14 +1,25 @@ -import { Comment } from '../../../models/comment'; -import { Observable, Subject } from 'rxjs'; -import { WsCommentServiceService } from '../../../services/websockets/ws-comment-service.service'; -import { CommentService } from '../../../services/http/comment.service'; -import { CloudParameters } from './tag-cloud.interface'; +import { Injectable } from '@angular/core'; +import { TopicCloudAdminData } from '../../components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { WsCommentServiceService } from '../websockets/ws-comment-service.service'; +import { CommentService } from '../http/comment.service'; +import { TopicCloudAdminService } from './topic-cloud-admin.service'; +import { CommentFilterOptions } from '../../utils/filter-options'; import { TranslateService } from '@ngx-translate/core'; +import { CloudParameters } from '../../components/shared/tag-cloud/tag-cloud.interface'; +import { Comment } from '../../models/comment'; +import { CommentFilterUtils } from '../../utils/filter-comments'; +import { Message } from '@stomp/stompjs'; export interface TagCloudDataTagEntry { weight: number; adjustedWeight: number; cachedVoteCount: number; + cachedUpVotes: number; + cachedDownVotes: number; + distinctUsers: Set<number>; + firstTimeStamp: Date; + categories: Set<string>; comments: Comment[]; } @@ -40,8 +51,8 @@ export type TagCloudMetaDataCount = [ ]; export enum TagCloudDataSupplyType { - fullText, keywords, + fullText, keywordsAndFullText } @@ -51,7 +62,10 @@ export enum TagCloudCalcWeightType { byLengthAndVotes } -export class TagCloudDataManager { +@Injectable({ + providedIn: 'root' +}) +export class TagCloudDataService { private _isDemoActive: boolean; private _isAlphabeticallySorted: boolean; private _dataBus: Subject<TagCloudData>; @@ -66,9 +80,14 @@ export class TagCloudDataManager { private _lastMetaData: TagCloudMetaData = null; private readonly _currentMetaData: TagCloudMetaData; private _demoData: TagCloudData = null; + private _adminData: TopicCloudAdminData = null; + private _currentBlacklist: string[] = []; + private _subscriptionAdminData: Subscription; + private _subscriptionBlacklist: Subscription; constructor(private _wsCommentService: WsCommentServiceService, - private _commentService: CommentService) { + private _commentService: CommentService, + private _tagCloudAdmin: TopicCloudAdminService) { this._isDemoActive = false; this._isAlphabeticallySorted = false; this._dataBus = new Subject<TagCloudData>(); @@ -88,25 +107,32 @@ export class TagCloudDataManager { }); } - activate(roomId: string): void { - if (this._wsCommentSubscription !== null) { - console.error('Tag cloud data manager was already activated!'); - return; - } + bindToRoom(roomId: string): void { this._roomId = roomId; - this.onUpdateData(); - //TODO Optimize for special events => better performance - this._wsCommentSubscription = this._wsCommentService - .getCommentStream(this._roomId).subscribe(e => this.onUpdateData()); + this._subscriptionAdminData = this._tagCloudAdmin.getAdminData.subscribe(adminData => { + this._adminData = adminData; + this._calcWeightType = this._adminData.considerVotes ? TagCloudCalcWeightType.byLengthAndVotes : TagCloudCalcWeightType.byLength; + this._supplyType = this._adminData.keywordORfulltext as unknown as TagCloudDataSupplyType; + this.rebuildTagData(); + }); + this._subscriptionBlacklist = this._tagCloudAdmin.getBlacklist().subscribe(blacklist => { + this._currentBlacklist = blacklist || []; + this.rebuildTagData(); + }); + this.fetchData(); + if (!CommentFilterOptions.readFilter().paused) { + this._wsCommentSubscription = this._wsCommentService + .getCommentStream(this._roomId).subscribe(e => this.onMessage(e)); + } } - deactivate(): void { - if (this._wsCommentSubscription === null) { - console.error('Tag cloud data manager was already deactivated!'); - return; + unbindRoom(): void { + this._subscriptionBlacklist.unsubscribe(); + this._subscriptionAdminData.unsubscribe(); + if (this._wsCommentSubscription !== null) { + this._wsCommentSubscription.unsubscribe(); + this._wsCommentSubscription = null; } - this._wsCommentSubscription.unsubscribe(); - this._wsCommentSubscription = null; } updateDemoData(translate: TranslateService): void { @@ -115,9 +141,14 @@ export class TagCloudDataManager { for (let i = 10; i >= 1; i--) { this._demoData.set(text.replace('%d', '' + i), { cachedVoteCount: 0, + cachedUpVotes: 0, + cachedDownVotes: 0, comments: [], weight: i, - adjustedWeight: i - 1 + adjustedWeight: i - 1, + categories: new Set<string>(), + distinctUsers: new Set<number>(), + firstTimeStamp: new Date() }); } }); @@ -182,6 +213,11 @@ export class TagCloudDataManager { return this._isAlphabeticallySorted; } + blockWord(tag: string): void { + this._tagCloudAdmin.addWordToBlacklist(tag.toLowerCase()); + this.rebuildTagData(); + } + updateConfig(parameters: CloudParameters): boolean { if (parameters.sortAlphabetically !== this._isAlphabeticallySorted) { this._isAlphabeticallySorted = parameters.sortAlphabetically; @@ -223,7 +259,7 @@ export class TagCloudDataManager { return this._lastFetchedData; } - private onUpdateData(): void { + private fetchData(): void { this._commentService.getFilteredComments(this._roomId).subscribe((comments: Comment[]) => { this._lastFetchedComments = comments; if (this._isDemoActive) { @@ -247,6 +283,9 @@ export class TagCloudDataManager { } private rebuildTagData() { + if (!this._lastFetchedComments) { + return; + } const currentMeta = this._isDemoActive ? this._lastMetaData : this._currentMetaData; const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); const users = new Set<number>(); @@ -263,13 +302,43 @@ export class TagCloudDataManager { keywords = []; } for (const keyword of keywords) { - //TODO Check spelling & check profanity + const lowerCaseKeyWord = keyword.toLowerCase(); + let profanity = false; + for (const word of this._currentBlacklist) { + if (lowerCaseKeyWord.includes(word)) { + profanity = true; + break; + } + } + if (profanity) { + continue; + } let current = data.get(keyword); if (current === undefined) { - current = {cachedVoteCount: 0, comments: [], weight: 0, adjustedWeight: 0}; + current = { + cachedVoteCount: 0, + cachedUpVotes: 0, + cachedDownVotes: 0, + comments: [], + weight: 0, + adjustedWeight: 0, + distinctUsers: new Set<number>(), + categories: new Set<string>(), + firstTimeStamp: comment.timestamp + }; data.set(keyword, current); } current.cachedVoteCount += comment.score; + current.cachedUpVotes += comment.upvotes; + current.cachedDownVotes += comment.downvotes; + current.distinctUsers.add(comment.userNumber); + if (comment.tag) { + current.categories.add(comment.tag); + } + // @ts-ignore + if (current.firstTimeStamp - comment.timestamp > 0) { + current.firstTimeStamp = comment.timestamp; + } current.comments.push(comment); } users.add(comment.userNumber); @@ -300,4 +369,60 @@ export class TagCloudDataManager { } } + private onMessage(message: Message): void { + const msg = JSON.parse(message.body); + const payload = msg.payload; + switch (msg.type) { + case 'CommentCreated': + this._commentService.getComment(payload.id).subscribe(c => { + if (CommentFilterUtils.checkComment(c)) { + this._lastFetchedComments.push(c); + this.rebuildTagData(); + } + }); + break; + case 'CommentPatched': + for (const comment of this._lastFetchedComments) { + if (payload.id === comment.id) { + let needRebuild = false; + for (const [key, value] of Object.entries(payload.changes)) { + switch (key) { + case 'score': + comment.score = value as number; + needRebuild = true; + break; + case 'upvotes': + comment.upvotes = value as number; + needRebuild = true; + break; + case 'downvotes': + comment.downvotes = value as number; + needRebuild = true; + break; + case 'ack': + const isNowAck = value as boolean; + if (!isNowAck) { + this._lastFetchedComments = this._lastFetchedComments.filter((el) => el.id !== payload.id); + } + needRebuild = true; + break; + case 'tag': + comment.tag = value as string; + needRebuild = true; + break; + } + } + if (needRebuild) { + this.rebuildTagData(); + } + break; + } + } + break; + case 'CommentDeleted': + this._lastFetchedComments = this._lastFetchedComments.filter((el) => el.id !== payload.id); + this.rebuildTagData(); + break; + } + } } diff --git a/src/app/services/util/topic-cloud-admin.service.ts b/src/app/services/util/topic-cloud-admin.service.ts index 401b26c6e01d5f684f0b305f03fbcd0abddda2d3..6fe470befa0fb87ad3aa1aab50fb33a888f56fcb 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -1,45 +1,49 @@ -import { stringify } from '@angular/compiler/src/util'; import { Injectable } from '@angular/core'; import * as BadWords from 'naughty-words'; // eslint-disable-next-line max-len -import { TopicCloudAdminData, KeywordOrFulltext } from '../../../app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData'; +import { TopicCloudAdminData, KeywordOrFulltext, Labels, spacyLabels } from '../../components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData'; +import { RoomService } from './../../services/http/room.service'; +import { Room } from '../../models/room'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationService } from './notification.service'; +import { Observable, Subject } from 'rxjs'; + @Injectable({ providedIn: 'root', }) export class TopicCloudAdminService { - private badWords = []; + private adminData: Subject<TopicCloudAdminData>; + private blacklist: Subject<string[]>; private profanityWords = []; - private blacklist = []; // should be stored in backend - private profanityKey = 'custom-Profanity-List'; - - constructor() { - this.badWords = BadWords; + private readonly profanityKey = 'custom-Profanity-List'; + private readonly adminKey = 'Topic-Cloud-Admin-Data'; + constructor(private roomService: RoomService, + private translateService: TranslateService, + private notificationService: NotificationService) { + this.blacklist = new Subject<string[]>(); + this.adminData = new Subject<TopicCloudAdminData>(); /* put all arrays of languages together */ - this.profanityWords = this.badWords['en'] - .concat(this.badWords['de']) - .concat(this.badWords['fr']) - .concat(this.badWords['ar']) - .concat(this.badWords['ru']) - .concat(this.badWords['tr']); - } - - getBlacklistWords(profanityFilter: boolean, blacklistFilter: boolean) { - let words = []; - if (profanityFilter) { - // TODO: send only words that are contained in keywords - words = words.concat(this.profanityWords).concat(this.getProfanityList()); - } - if (blacklistFilter && this.blacklist.length > 0) { - words = words.concat(this.blacklist); - } - return words; + this.profanityWords = BadWords['en'] + .concat(BadWords['de']) + .concat(BadWords['fr']) + .concat(BadWords['ar']) + .concat(BadWords['ru']) + .concat(BadWords['tr']); + } + + get getAdminData(): Observable<TopicCloudAdminData>{ + return this.adminData.asObservable(); } - get getAdminData(): TopicCloudAdminData { - let data = JSON.parse(localStorage.getItem('Topic-Cloud-Admin-Data')); + get getDefaultAdminData(): TopicCloudAdminData { + let data = JSON.parse(localStorage.getItem(this.adminKey)); if (!data) { data = { - blacklist: this.profanityWords, + blacklist: [], + wantedLabels: { + de: this.getDefaultSpacyTagsDE(), + en: this.getDefaultSpacyTagsEN() + }, considerVotes: false, profanityFilter: true, blacklistIsActive: false, @@ -49,13 +53,25 @@ export class TopicCloudAdminService { return data; } - setAdminData(adminData: TopicCloudAdminData){ - localStorage.setItem('Topic-Cloud-Admin-Data', JSON.stringify(adminData)); + setAdminData(_adminData: TopicCloudAdminData) { + localStorage.setItem(this.adminKey, JSON.stringify(_adminData)); + this.getBlacklist().subscribe(list => { + _adminData.blacklist = this.getCustomProfanityList().concat(list).concat(this.profanityWords); + this.adminData.next(_adminData); + }); + } + + getBlacklist(): Observable<string[]> { + // TODO: add watcher for another moderators + this.getRoom().subscribe(room => { + this.blacklist.next(JSON.parse(room.blacklist)); + }); + return this.blacklist.asObservable(); } filterProfanityWords(str: string): string { let questionWithProfanity = str; - this.profanityWords.concat(this.getProfanityList()).map((word) => { + this.profanityWords.concat(this.getCustomProfanityList()).map((word) => { questionWithProfanity = questionWithProfanity .toLowerCase() .includes(word.toLowerCase()) @@ -69,15 +85,15 @@ export class TopicCloudAdminService { return questionWithProfanity; } - getProfanityList(): string[] { + getCustomProfanityList(): string[] { const list = localStorage.getItem(this.profanityKey); return list ? list.split(',') : []; } addToProfanityList(word: string) { if (word !== undefined) { - const newList = this.getProfanityList(); - if (newList.includes(word)){ + const newList = this.getCustomProfanityList(); + if (newList.includes(word)) { return; } newList.push(word); @@ -86,33 +102,80 @@ export class TopicCloudAdminService { } removeFromProfanityList(profanityWord: string) { - const list = this.getProfanityList(); + const list = this.getCustomProfanityList(); list.map(word => { - if (word === profanityWord){ + if (word === profanityWord) { list.splice(list.indexOf(word, 0), 1); } }); localStorage.setItem(this.profanityKey, list.toString()); } - removeProfanityList(){ + removeProfanityList() { localStorage.removeItem(this.profanityKey); } - getBlacklist(): string[] { - return this.blacklist; + getRoom(): Observable<Room> { + return this.roomService.getRoom(localStorage.getItem('roomId')); } - addToBlacklistWordList(word: string) { + addWordToBlacklist(word: string) { if (word !== undefined) { - this.blacklist.push(word); + this.getRoom().subscribe(room => { + const newlist = JSON.parse(room.blacklist); + newlist.push(word); + this.updateBlacklist(newlist, room); + }); } } removeWordFromBlacklist(word: string) { - this.blacklist.splice(this.blacklist.indexOf(word), 1); + if (word !== undefined) { + this.getRoom().subscribe(room => { + if (room.blacklist.length > 0){ + const newlist = JSON.parse(room.blacklist); + newlist.splice(newlist.indexOf(word, 0), 1); + this.updateBlacklist(newlist, room); + } + }); + } + } + + updateBlacklist(list: string[], room: Room){ + room.blacklist = JSON.stringify(list); + this.updateRoom(room); + } + + updateRoom(updatedRoom: Room){ + this.roomService.updateRoom(updatedRoom).subscribe(_ => { + this.translateService.get('topic-cloud.changes-successful').subscribe(msg => { + this.notificationService.show(msg); + /* update blacklist for subscribers */ + this.blacklist.next(JSON.parse(updatedRoom.blacklist)); + }); + }, + error => { + this.translateService.get('topic-cloud.changes-gone-wrong').subscribe(msg => { + this.notificationService.show(msg); + }); + }); } + getDefaultSpacyTagsDE(): string[] { + let tags: string[] = []; + spacyLabels.de.forEach(label => { + tags.push(label.tag); + }); + return tags; + } + + getDefaultSpacyTagsEN(): string[] { + let tags: string[] = []; + spacyLabels.en.forEach(label => { + tags.push(label.tag); + }); + return tags; + } private replaceString(str: string, search: string, replace: string) { return str.split(search).join(replace); diff --git a/src/assets/i18n/home/de.json b/src/assets/i18n/home/de.json index 0d824a82c8f071e9a85578d32e4f12cc5ae47e93..e54b40b66a45a4530e80e9aa144bb5ef949697aa 100644 --- a/src/assets/i18n/home/de.json +++ b/src/assets/i18n/home/de.json @@ -311,5 +311,9 @@ }, "qr-dialog": { "session": "Raum" + }, + "topic-cloud": { + "changes-gone-wrong": "Etwas ist schief gelaufen!", + "changes-successful": "Änderungen gespeichert." } } diff --git a/src/assets/i18n/home/en.json b/src/assets/i18n/home/en.json index af3295e5a159ffdcd041c04bee2281fcc2d146d4..b886992ec32e98181f3c3a17c4d455cb38749ba4 100644 --- a/src/assets/i18n/home/en.json +++ b/src/assets/i18n/home/en.json @@ -315,5 +315,9 @@ }, "qr-dialog": { "session": "Key code" + }, + "topic-cloud": { + "changes-gone-wrong": "Something went wrong!", + "changes-successful": "Successfully updated." } } diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index 38e15dac7460deeeeeb0223520ef9774bed14f5b..b431fe33f527cac0d72a4f28f87d4e344aad5461 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -87,7 +87,7 @@ "cancel": "Abbrechen", "delete": "Löschen" }, - "spacy-dialog":{ + "spacy-dialog": { "auto": "auto", "de": "Deutsch", "en": "Englisch", @@ -155,7 +155,6 @@ "sure": "Bist du sicher?", "grammar-check": "Rechtschreibprüfung" }, - "home-page": { "exactly-8": "Ein Raum-Code hat genau 8 Ziffern.", "no-room-found": "Es wurde keine Sitzung mit diesem Raum-Code gefunden.", @@ -218,7 +217,7 @@ "filter-lbl": "Filter-Menü anzeigen", "filter-lbl-favorites": "Favorisierte Fragen filtern", "filter-lbl-approved": "Bejahte Fragen filtern", - "filter-lbl-disapproved":"Verneinte Fragen filtern", + "filter-lbl-disapproved": "Verneinte Fragen filtern", "filter-tags-lbl": "Nach Kategorien filtern", "user-lbl": "Benutzer-Menü öffnen", "slider-lbl": "Fragen-Zoom einstellen", @@ -233,7 +232,25 @@ "tag-cloud": { "config": "Wolkenansicht ändern", "administration": "Wolkenthemen editieren", - "demo-data-topic": "Thema %d" + "demo-data-topic": "Thema %d", + "overview-question-topic-tooltip": "Anzahl gestellter Fragen mit diesem Thema", + "overview-questioners-topic-tooltip": "Anzahl Fragensteller*innen mit diesem Thema", + "period-since-first-comment":"Zeitraum seit erstem Kommentar", + "upvote-topic": "Upvotes für dieses Thema", + "downvote-topic": "Downvotes für dieses Thema", + "blacklist-topic": "Thema zur Blacklist hinzufügen" + }, + "tag-cloud-popup": { + "few-seconds": "wenige Sekunden", + "few-minutes": "wenige Minuten", + "some-minutes": "{{minutes}} Minuten", + "one-hour": "1 Stunde", + "some-hours": "{{hours}} Stunden", + "one-day": "1 Tag", + "some-days": "{{days}} Tage", + "one-week": "1 Woche", + "some-weeks": "{{weeks}} Wochen", + "some-months": "{{months}} Monate" }, "topic-cloud-dialog": { "cancel": "Abbrechen", @@ -263,7 +280,14 @@ "show-blacklist": "Zeige Blackliste", "hide-blacklist": "Verberge Blackliste", "show-profanity-list": "Zeige Schimpfwortliste", - "hide-profanity-list": "Verberge Schimpfwortliste" + "hide-profanity-list": "Verberge Schimpfwortliste", + "keyword-delete": "Stichwort gelöscht", + "keyword-edit": "Stichwort umbenannt", + "keywords-merge": "Stichwörter zusammengefügt", + "changes-gone-wrong": "Etwas ist schiefgelaufen", + "english": "Englisch", + "german": "Deutsch", + "select-all": "Alle auswählen" }, "topic-cloud-confirm-dialog": { "cancel": "Abbrechen", @@ -277,24 +301,24 @@ "read-more": "Mehr lesen", "read-less": "Weniger lesen" }, - "tag-cloud-config":{ - "title":"Tag-Cloud Einstellungen", - "general":"Allgemeine Einstellungen", - "overflow":"Überlauf", - "height":"Höhe", - "random-angle":"Zufallswinkel", - "realign":"Neu ausrichten", - "background":"Hintergrundfarbe", - "word-delay":"Wortverzögerung", - "hover-color":"Schriftfarbe", - "hover-scale":"Hover Skala", - "hover-time":"Hover Zeit", - "hover-title":"Hover Einstellungen", - "hover-delay":"Hover Verzögerung", - "cancel-btn":"Abbruch", - "save-btn":"Speichern", - "font-size-min":"Schriftgröße min", - "font-size-max":"Schriftgröße max", + "tag-cloud-config": { + "title": "Tag-Cloud Einstellungen", + "general": "Allgemeine Einstellungen", + "overflow": "Überlauf", + "height": "Höhe", + "random-angle": "Zufallswinkel", + "realign": "Neu ausrichten", + "background": "Hintergrundfarbe", + "word-delay": "Wortverzögerung", + "hover-color": "Schriftfarbe", + "hover-scale": "Hover Skala", + "hover-time": "Hover Zeit", + "hover-title": "Hover Einstellungen", + "hover-delay": "Hover Verzögerung", + "cancel-btn": "Abbruch", + "save-btn": "Speichern", + "font-size-min": "Schriftgröße min", + "font-size-max": "Schriftgröße max", "select-color": "Farbauswahl", "random-angle-tooltip": "Anordnung der Winkel zufällig generieren", "background-tooltip": "Auswahl der Hintergrundfarbe", diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index 4e481147a7f80aa5276cdb9fba94fd701642b0c0..9821382bbad0a6ff070a20aa026309e4a3913983 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -238,7 +238,25 @@ "tag-cloud": { "config": "Modify cloud view", "administration": "Edit cloud topics", - "demo-data-topic": "Topic %d" + "demo-data-topic": "Topic %d", + "overview-question-topic-tooltip": "Number of questions with this topic", + "overview-questioners-topic-tooltip": "Number of questioners with this topic", + "period-since-first-comment":"Period since first comment", + "upvote-topic": "Upvotes for this topic", + "downvote-topic": "Downvotes for this topic", + "blacklist-topic": "Add topic to blacklist" + }, + "tag-cloud-popup": { + "few-seconds": "few seconds", + "few-minutes": "few minutes", + "some-minutes": "{{minutes}} minutes", + "one-hour": "1 hour", + "some-hours": "{{hours}} hours", + "one-day": "1 day", + "some-days": "{{days}} days", + "one-week": "1 week", + "some-weeks": "{{weeks}} weeks", + "some-months": "{{months}} months" }, "topic-cloud-dialog":{ "edit": "Edit", @@ -268,7 +286,14 @@ "show-blacklist": "Show blacklist", "hide-blacklist": "Hide blacklist", "show-profanity-list": "Show profanity list", - "hide-profanity-list": "Hide profanity list" + "hide-profanity-list": "Hide profanity list", + "keyword-delete": "keyword deleted", + "keyword-edit": "keyword renamed", + "keywords-merge": "keywords merged", + "changes-gone-wrong": "somthing has gone wrong", + "english": "English", + "german": "German", + "select-all": "Select all" }, "topic-cloud-confirm-dialog":{ "cancel": "Cancel",