diff --git a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html index aadff087cfc38dcbd323e9067faa3f68484aba41..cf04bef1a22cb1d9be4f35206be3f1cc4a872aaa 100644 --- a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html +++ b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html @@ -22,8 +22,6 @@ </mat-label> </ars-row> </span> - <span *ngIf="keywords.length > 0"></span> - <ars-row class="list-container"> <div fxLayout="row" fxLayoutAlign="center center" fxFill> <mat-progress-spinner *ngIf="isLoading" mode="indeterminate"></mat-progress-spinner> @@ -61,10 +59,7 @@ </mat-list> </ars-row> <ars-row> - <span *ngIf="keywords.length <= 0 && !this.isLoading"> - <p>{{ 'spacy-dialog.empty-nouns' | translate }}</p> - </span> - <span *ngIf="!langSupported"> + <span *ngIf="!isLoading && (!langSupported || !hasKeywordsFromSpacy)"> <p class="manual-input-title">{{ 'spacy-dialog.add-manually' | translate }}</p> <textarea class="manual-input" [(ngModel)]="manualKeywords" (input)="manualKeywordsToKeywords()"></textarea> </span> diff --git a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts index 45166229b358cbcc4c6a3c2c4765931d45931961..37f836ac17168aaee562b7bd97c3f8dfb40cbc83 100644 --- a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts +++ b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts @@ -29,10 +29,11 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { commentBodyChecked: string; keywords: Keyword[] = []; keywordsOriginal: Keyword[] = []; + hasKeywordsFromSpacy = false; isLoading = false; langSupported: boolean; manualKeywords = ''; - _concurrentEdits = 0 + _concurrentEdits = 0; constructor( protected langService: LanguagetoolService, @@ -84,6 +85,7 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { ) .subscribe(words => { this.keywords = words; + this.hasKeywordsFromSpacy = this.keywords.length > 0; //deep copy this.keywordsOriginal = [...words]; for (let i = 0; i < this.keywordsOriginal.length; i++) { @@ -92,6 +94,7 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { }, () => { this.keywords = []; this.keywordsOriginal = []; + this.hasKeywordsFromSpacy = false; }, () => { this.isLoading = false; }); @@ -150,6 +153,6 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { onEditChange(change: number) { this._concurrentEdits += change; - this.appDialogActionButtons.confirmButtonDisabled = (this._concurrentEdits > 0) + this.appDialogActionButtons.confirmButtonDisabled = (this._concurrentEdits > 0); } } 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 cbbc0d8ebbf31b4d022a62cf117a359b4aa1cf2c..1337a08d1e37606c118c08f6ee6244e999e15e97 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts @@ -10,6 +10,11 @@ export interface TopicCloudAdminData { profanityFilter: ProfanityFilter; blacklistIsActive: boolean; keywordORfulltext: KeywordOrFulltext; + minQuestions: number; + minQuestioners: number; + minUpvotes: number; + startDate: string; + endDate: string; } export enum KeywordOrFulltext { 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 3f14fe3bc5a0a5cf50739694b81f137e2ae3e3c8..c282c94d642cebf163ce3dbe80f6999d87389856 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 @@ -28,9 +28,15 @@ </mat-radio-group> </mat-card> - <mat-card style="background: none;"> - <mat-slide-toggle [(ngModel)]="considerVotes"> - {{'topic-cloud-dialog.consider-votes' | translate}} + <mat-slide-toggle [(ngModel)]="considerVotes"> + {{'topic-cloud-dialog.consider-votes' | translate}} + </mat-slide-toggle> + <div *ngIf="isCreatorOrMod"> + <mat-slide-toggle (change)="changeProfanityFilter()" [(ngModel)]="profanityFilter"> + {{'topic-cloud-dialog.profanity' | translate}} + </mat-slide-toggle> + <mat-slide-toggle [(ngModel)]="blacklistIsActive"> + {{'topic-cloud-dialog.hide-blacklist-words' | translate}} </mat-slide-toggle> </mat-card> <div *ngIf="isCreatorOrMod"> @@ -249,6 +255,48 @@ </mat-tab> </mat-tab-group> </mat-expansion-panel> + <mat-expansion-panel class="color-background"> + <mat-expansion-panel-header class="color-background"> + <mat-panel-title> + {{'topic-cloud-dialog.topic-requirement-title' | translate}} + </mat-panel-title> + </mat-expansion-panel-header> + <mat-form-field class="themeRequirementInput" appearance="fill"> + <mat-label>{{'topic-cloud-dialog.topic-requirement-questions' | translate}}</mat-label> + <input matInput type="number" min="1" [(ngModel)]="minQuestions"> + </mat-form-field> + <mat-form-field class="themeRequirementInput" appearance="fill"> + <mat-label>{{'topic-cloud-dialog.topic-requirement-questioners' | translate}}</mat-label> + <input matInput type="number" min="1" [(ngModel)]="minQuestioners"> + </mat-form-field> + <mat-form-field class="themeRequirementInput" appearance="fill"> + <mat-label>{{'topic-cloud-dialog.topic-requirement-upvotes' | translate}}</mat-label> + <input matInput type="number" min="0" [(ngModel)]="minUpvotes"> + </mat-form-field> + + <table class="themeRequirementInput"> + <tr> + <td> + <mat-form-field class="themeRequirementInput" appearance="fill"> + <mat-label>{{'topic-cloud-dialog.topic-requirement-begin-datetime' | translate}}</mat-label> + <input matInput type="datetime-local" [(ngModel)]="startDate"> + <button *ngIf="startDate" matSuffix mat-icon-button aria-label="Clear" (click)="startDate=''"> + <mat-icon>close</mat-icon> + </button> + </mat-form-field> + </td> + <td> + <mat-form-field class="themeRequirementInput" appearance="fill"> + <mat-label>{{'topic-cloud-dialog.topic-requirement-end-datetime' | translate}}</mat-label> + <input matInput type="datetime-local" [(ngModel)]="endDate"> + <button *ngIf="endDate" matSuffix mat-icon-button aria-label="Clear" (click)="endDate=''"> + <mat-icon>close</mat-icon> + </button> + </mat-form-field> + </td> + </tr> + </table> + </mat-expansion-panel> </mat-accordion> </mat-expansion-panel> </mat-accordion> @@ -301,8 +349,7 @@ </button> </mat-menu> - <mat-card class="color-surface" - *ngIf="(keywords.length === 0 || (searchMode && filteredKeywords.length === 0))&&!isLoading"> + <mat-card class="color-surface" *ngIf="(keywords.length === 0 || (searchMode && filteredKeywords.length === 0))&&!isLoading"> <p class="color-on-surface" fxLayoutAlign="center"> {{'topic-cloud-dialog.no-keywords-note' | translate}} </p> 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 5c4d5977a5aa579947c8d4593d64e54a9115c942..f672fae74a1e3ad92cd954a468e61a3edd90a22f 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 @@ -130,6 +130,11 @@ mat-panel-title, mat-panel-description { margin-left: 8px; } +.themeRequirementInput { + width: 100%; + margin-left: 0 !important; +} + .vertical-center { margin-top: auto; margin-bottom: auto; 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 2cb85515a616ff350d26c6c81b2a328fdba1539e..9d1bed55d7c8b9f4e0c0dd033baddfdf73d4db60 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 @@ -53,7 +53,13 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { en: string[]; }; spacyLabelsAllSelectedDE = true; - isLoading = true; + isLoading: boolean; + minQuestions: string; + minQuestioners: string; + minUpvotes: string; + startDate: string; + endDate: string; + keywords: Keyword[] = []; private topicCloudAdminData: TopicCloudAdminData; private profanityFilter: boolean; @@ -213,19 +219,36 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { profFilter = this.censorLanguageSpecificCheck ? ProfanityFilter.languageSpecific : ProfanityFilter.none; profFilter = this.censorPartialWordsCheck ? ProfanityFilter.partialWords : profFilter; } + let minQuestionersVerified = +this.minQuestioners; + if (Number.isNaN(minQuestionersVerified) || minQuestionersVerified < 1) { + minQuestionersVerified = 1; + } + let minQuestionsVerified = +this.minQuestions; + if (Number.isNaN(minQuestionsVerified) || minQuestionsVerified < 1) { + minQuestionsVerified = 1; + } + let minUpvotesVerified = +this.minUpvotes; + if (Number.isNaN(minUpvotesVerified) || minUpvotesVerified < 0) { + minUpvotesVerified = 0; + } + this.topicCloudAdminData = { + blacklist: [], + wantedLabels: { + de: this.wantedLabels.de, + en: this.wantedLabels.en + }, + considerVotes: this.considerVotes, + profanityFilter: profFilter, + blacklistIsActive: this.blacklistIsActive, + keywordORfulltext: KeywordOrFulltext[this.keywordORfulltext], + minQuestioners: minQuestionersVerified, + minQuestions: minQuestionsVerified, + minUpvotes: minUpvotesVerified, + startDate: this.startDate.length ? this.startDate : null, + endDate: this.endDate.length ? this.endDate : null + }; + this.topicCloudAdminService.setAdminData(this.topicCloudAdminData); } - this.topicCloudAdminData = { - blacklist: [], - wantedLabels: { - de: this.wantedLabels.de, - en: this.wantedLabels.en - }, - considerVotes: this.considerVotes, - profanityFilter: profFilter, - blacklistIsActive: this.blacklistIsActive, - keywordORfulltext: KeywordOrFulltext[this.keywordORfulltext] - }; - this.topicCloudAdminService.setAdminData(this.topicCloudAdminData); } setDefaultAdminData() { @@ -245,6 +268,11 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { de: this.topicCloudAdminData.wantedLabels.de, en: this.topicCloudAdminData.wantedLabels.en }; + this.minQuestioners = String(this.topicCloudAdminData.minQuestioners); + this.minQuestions = String(this.topicCloudAdminData.minQuestions); + this.minUpvotes = String(this.topicCloudAdminData.minUpvotes); + this.startDate = this.topicCloudAdminData.startDate || ''; + this.endDate = this.topicCloudAdminData.endDate || ''; } } diff --git a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts index 2cce403e9e7d0e084aaff4bfb6af3ed7de2ab23f..f9c8931bc5e2d63157e9786125d5d87b1d270346 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts @@ -11,7 +11,8 @@ import { RoomService } from '../../../../services/http/room.service'; import { Comment } from '../../../../models/comment'; import { CommentListData } from '../../comment-list/comment-list.component'; import { TopicCloudAdminService } from '../../../../services/util/topic-cloud-admin.service'; -import { KeywordOrFulltext } from '../topic-cloud-administration/TopicCloudAdminData'; +import { TopicCloudAdminData } from '../topic-cloud-administration/TopicCloudAdminData'; +import { TagCloudDataService } from '../../../../services/util/tag-cloud-data.service'; class CommentsCount { comments: number; @@ -33,8 +34,7 @@ export class TopicCloudFilterComponent implements OnInit { allComments: CommentsCount; filteredComments: CommentsCount; disableCurrentFiltersOptions = false; - private readonly _filter: KeywordOrFulltext; - private readonly _blacklist: string[]; + private readonly _adminData: TopicCloudAdminData; constructor(public dialogRef: MatDialogRef<RoomCreatorPageComponent>, public dialog: MatDialog, @@ -46,9 +46,7 @@ export class TopicCloudFilterComponent implements OnInit { @Inject(MAT_DIALOG_DATA) public data: any, public eventService: EventService) { langService.langEmitter.subscribe(lang => translationService.use(lang)); - const adminData = TopicCloudAdminService.getDefaultAdminData; - this._filter = adminData.keywordORfulltext; - this._blacklist = adminData.blacklist; + this._adminData = TopicCloudAdminService.getDefaultAdminData; } ngOnInit() { @@ -75,40 +73,11 @@ export class TopicCloudFilterComponent implements OnInit { } getCommentCounts(comments: Comment[]): CommentsCount { + const [data, users] = TagCloudDataService.buildDataFromComments(this._adminData, comments); const counts = new CommentsCount(); - const userSet = new Set<number>(); - const keywordSet = new Set<string>(); - - comments.forEach(c => { - if (c.userNumber) { - userSet.add(c.userNumber); - } - let source = c.keywordsFromQuestioner; - if (this._filter === KeywordOrFulltext.both) { - source = !source || !source.length ? c.keywordsFromSpacy : source; - } else if (this._filter === KeywordOrFulltext.fulltext) { - source = c.keywordsFromSpacy; - } - if (source) { - source.forEach(k => { - let isProfanity = false; - const lowerCasedKeyword = k.toLowerCase(); - for (const word of this._blacklist) { - if (lowerCasedKeyword.includes(word)) { - isProfanity = true; - break; - } - } - if (!isProfanity) { - keywordSet.add(k); - } - }); - } - }); - counts.comments = comments.length; - counts.users = userSet.size; - counts.keywords = keywordSet.size; + counts.users = users.size; + counts.keywords = data.size; return counts; } 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 967ff70fc74be02627932e551c1e63d8c00816db..7c595d4335d5f03358c304ba95f5ffbbf33009ca 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -372,6 +372,7 @@ export class CommentListComponent implements OnInit, OnDestroy { this.currentFilter = ''; this.selectedTag = ''; this.selectedKeyword = ''; + this.userNumberSelection = 0; this.sortComments(this.currentSort); return; } 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 a706a462c875130cbac5257b2634e2bc87f9a7ba..88c7ae2ddc2536caff6068df814b0198f1521b6b 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.html +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.html @@ -29,3 +29,12 @@ </mat-drawer-content> </mat-drawer-container> </ars-screen> +<button *ngIf="room && !room.closed" + mat-fab + mat-icon-button + aria-labelledby="add" + class="fab_add_comment" + (click)="createCommentWrapper.openCreateDialog(this.user)" + matTooltip="{{ 'comment-list.add-comment' | translate }}"> + <mat-icon>add</mat-icon> +</button> 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 43a9e2415f6a7e747c6178f3d7d6afa0c16842cc..07159c41de2f1743edea3150c633dc5075804335 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.scss +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.scss @@ -49,3 +49,14 @@ app-tag-cloud-pop-up { ::ng-deep .spacyTagCloud span:hover { cursor: pointer; } + +.fab_add_comment { + position: fixed; + right: 30px; + bottom: 30px; + background-color: var(--primary); + color: var(--on-primary); + transform: scale(1.4); + transition: all 0.1s ease-in-out; + z-index: 2; +} 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 37c401a931362c0a318d7b32b23fb187b34e1a0e..6dbb925b9cbf06410c2a17b8c0d67f021162f20e 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.ts @@ -61,6 +61,7 @@ class TagComment implements CloudData { const colorRegex = /rgba?\((\d+), (\d+), (\d+)(?:, (\d(?:\.\d+)?))?\)/; const transformationScaleKiller = /scale\([^)]*\)/; +const transformationRotationKiller = /rotate\(([^)]*)\)/; type DefaultColors = [ hover: string, w1: string, @@ -176,8 +177,8 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit, A isLoading = true; headerInterface = null; themeSubscription = null; + createCommentWrapper: CreateCommentWrapper = null; private _currentSettings: CloudParameters; - private _createCommentWrapper: CreateCommentWrapper = null; private _subscriptionCommentlist = null; private _calcCanvas: HTMLCanvasElement = null; private _calcRenderContext: CanvasRenderingContext2D = null; @@ -224,7 +225,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit, A this.updateGlobalStyles(); this.headerInterface = this.eventService.on<string>('navigate').subscribe(e => { if (e === 'createQuestion') { - this._createCommentWrapper.openCreateDialog(this.user); + this.createCommentWrapper.openCreateDialog(this.user); } else if (e === 'topicCloudConfig') { this.configurationOpen = !this.configurationOpen; } else if (e === 'topicCloudAdministration') { @@ -263,7 +264,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit, A this.room = room; this.roomId = room.id; this.directSend = this.room.directSend; - this._createCommentWrapper = new CreateCommentWrapper(this.translateService, + this.createCommentWrapper = new CreateCommentWrapper(this.translateService, this.notificationService, this.commentService, this.dialog, this.room); if (!this.authenticationService.hasAccess(this.shortId, UserRole.PARTICIPANT)) { this.roomService.addToHistory(this.room.id); @@ -439,10 +440,14 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit, A this.child.cloudDataHtmlElements.forEach((elem, i) => { const dataElement = this.data[i]; elem.addEventListener('mouseleave', () => { - elem.style.transform = elem.style.transform.replace(transformationScaleKiller, '').trim(); + elem.style.transform = elem.style.transform.replace(transformationScaleKiller, '').trim() + + ' rotate(' + (elem.dataset['tempRotation'] || '0deg') + ')'; this.popup.leave(); }); elem.addEventListener('mouseenter', () => { + const transformMatch = elem.style.transform.match(transformationRotationKiller); + elem.dataset['tempRotation'] = transformMatch ? transformMatch[1] : '0deg'; + elem.style.transform = elem.style.transform.replace(transformationRotationKiller, '').trim(); this.popup.enter(elem, dataElement.text, dataElement.tagData, (this._currentSettings.hoverTime + this._currentSettings.hoverDelay) * 1_000); }); diff --git a/src/app/services/util/tag-cloud-data.service.ts b/src/app/services/util/tag-cloud-data.service.ts index 8509c510737cb4a2c2f5a462346bdc371a2b07e7..c3c3418f4fd06ecd44e7a9b2fa7e2feeb0ab67cc 100644 --- a/src/app/services/util/tag-cloud-data.service.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { TopicCloudAdminData } from '../../components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData'; -import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { TopicCloudAdminService } from './topic-cloud-admin.service'; import { CommentFilter } from '../../utils/filter-options'; import { TranslateService } from '@ngx-translate/core'; @@ -16,6 +16,7 @@ export interface TagCloudDataTagEntry { cachedDownVotes: number; distinctUsers: Set<number>; firstTimeStamp: Date; + lastTimeStamp: Date; categories: Set<string>; comments: Comment[]; } @@ -47,12 +48,6 @@ export type TagCloudMetaDataCount = [ number // w10 ]; -export enum TagCloudDataSupplyType { - keywords, - fullText, - keywordsAndFullText -} - export enum TagCloudCalcWeightType { byLength, byVotes, @@ -70,7 +65,6 @@ export class TagCloudDataService { private _cachedData: TagCloudData; private _commentSubscription = null; private _roomId = null; - private _supplyType = TagCloudDataSupplyType.keywordsAndFullText; private _calcWeightType = TagCloudCalcWeightType.byLength; private _lastFetchedData: TagCloudData = null; private _lastFetchedComments: Comment[] = null; @@ -102,10 +96,57 @@ export class TagCloudDataService { }); } + static buildDataFromComments(adminData: TopicCloudAdminData, comments: Comment[]): [TagCloudData, Set<number>] { + const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); + const users = new Set<number>(); + for (const comment of comments) { + TopicCloudAdminService.approveKeywordsOfComment(comment, adminData, (keyword) => { + let current: TagCloudDataTagEntry = data.get(keyword); + const commentDate = new Date(comment.timestamp); + if (current === undefined) { + current = { + cachedVoteCount: 0, + cachedUpVotes: 0, + cachedDownVotes: 0, + comments: [], + weight: 0, + adjustedWeight: 0, + distinctUsers: new Set<number>(), + categories: new Set<string>(), + firstTimeStamp: commentDate, + lastTimeStamp: commentDate + }; + 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 - commentDate > 0) { + current.firstTimeStamp = commentDate; + } + // @ts-ignore + if (current.lastTimeStamp - commentDate < 0) { + current.lastTimeStamp = commentDate; + } + current.comments.push(comment); + }); + users.add(comment.userNumber); + } + return [ + new Map<string, TagCloudDataTagEntry>([...data].filter(v => TopicCloudAdminService.isTopicAllowed(adminData, + v[1].comments.length, v[1].distinctUsers.size, v[1].cachedUpVotes, v[1].firstTimeStamp, v[1].lastTimeStamp))), + users + ]; + } + bindToRoom(roomId: string): void { this._currentFilter = CommentFilter.currentFilter; this._roomId = roomId; - this.onReceiveAdminData(TopicCloudAdminService.getDefaultAdminData); this._subscriptionAdminData = this._tagCloudAdmin.getAdminData.subscribe(adminData => { this.onReceiveAdminData(adminData, true); }); @@ -149,7 +190,8 @@ export class TagCloudDataService { adjustedWeight: i - 1, categories: new Set<string>(), distinctUsers: new Set<number>(), - firstTimeStamp: new Date() + firstTimeStamp: new Date(), + lastTimeStamp: new Date() }); } }); @@ -163,17 +205,6 @@ export class TagCloudDataService { return this._cachedData; } - get dataSupplyType(): TagCloudDataSupplyType { - return this._supplyType; - } - - set dataSupplyType(type: TagCloudDataSupplyType) { - if (this._supplyType !== type) { - this._supplyType = type; - this.rebuildTagData(); - } - } - set weightCalcType(type: TagCloudCalcWeightType) { if (type !== this._calcWeightType) { this._calcWeightType = type; @@ -256,7 +287,6 @@ export class TagCloudDataService { private onReceiveAdminData(data: TopicCloudAdminData, update = false) { this._adminData = data; this._calcWeightType = this._adminData.considerVotes ? TagCloudCalcWeightType.byLengthAndVotes : TagCloudCalcWeightType.byLength; - this._supplyType = this._adminData.keywordORfulltext as unknown as TagCloudDataSupplyType; if (update) { this.rebuildTagData(); } @@ -292,64 +322,9 @@ export class TagCloudDataService { return; } const currentMeta = this._isDemoActive ? this._lastMetaData : this._currentMetaData; - const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); - const users = new Set<number>(); const filteredComments = this._lastFetchedComments.filter(comment => this._currentFilter.checkComment(comment)); currentMeta.commentCount = filteredComments.length; - for (const comment of filteredComments) { - let keywords = comment.keywordsFromQuestioner; - if (this._supplyType === TagCloudDataSupplyType.keywordsAndFullText) { - if (!keywords || !keywords.length) { - keywords = comment.keywordsFromSpacy; - } - } else if (this._supplyType === TagCloudDataSupplyType.fullText) { - keywords = comment.keywordsFromSpacy; - } - if (!keywords) { - keywords = []; - } - for (const keyword of keywords) { - const lowerCaseKeyWord = keyword.toLowerCase(); - let profanity = false; - for (const word of this._adminData.blacklist) { - if (lowerCaseKeyWord.includes(word)) { - profanity = true; - break; - } - } - if (profanity) { - continue; - } - let current = data.get(keyword); - if (current === undefined) { - 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); - } + const [data, users] = TagCloudDataService.buildDataFromComments(this._adminData, filteredComments); let minWeight = null; let maxWeight = null; for (const value of data.values()) { diff --git a/src/app/services/util/topic-cloud-admin.service.ts b/src/app/services/util/topic-cloud-admin.service.ts index 8b787e474077972eb676bde33272e5b8d33943a1..839a24b50aa5e848ae90557d6bb3455abe60c6ab 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -8,16 +8,17 @@ import { RoomService } from '../http/room.service'; import { ProfanityFilter, Room } from '../../models/room'; import { TranslateService } from '@ngx-translate/core'; import { NotificationService } from './notification.service'; -import { Observable, Subject } from 'rxjs'; import { WsRoomService } from '..//websockets/ws-room.service'; import { ProfanityFilterService } from './profanity-filter.service'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { Comment } from '../../models/comment'; @Injectable({ providedIn: 'root', }) export class TopicCloudAdminService { private static readonly adminKey = 'Topic-Cloud-Admin-Data'; - private adminData: Subject<TopicCloudAdminData>; + private adminData: BehaviorSubject<TopicCloudAdminData>; private blacklist: Subject<string[]>; constructor(private roomService: RoomService, @@ -26,8 +27,6 @@ export class TopicCloudAdminService { private profanityFilterService: ProfanityFilterService, private notificationService: NotificationService) { this.blacklist = new Subject<string[]>(); - this.adminData = new Subject<TopicCloudAdminData>(); - this.wsRoomService.getRoomStream(localStorage.getItem('roomId')).subscribe(msg => { const message = JSON.parse(msg.body); const room = message.payload.changes; @@ -35,10 +34,45 @@ export class TopicCloudAdminService { this.blacklist.next(room.blacklist ? JSON.parse(room.blacklist) : []); } }); + this.adminData = new BehaviorSubject<TopicCloudAdminData>(TopicCloudAdminService.getDefaultAdminData); + } + + static approveKeywordsOfComment(comment: Comment, config: TopicCloudAdminData, keywordFunc: (string) => void) { + let source = comment.keywordsFromQuestioner; + if (config.keywordORfulltext === KeywordOrFulltext.both) { + source = !source || !source.length ? comment.keywordsFromSpacy : source; + } else if (config.keywordORfulltext === KeywordOrFulltext.fulltext) { + source = comment.keywordsFromSpacy; + } + if (!source) { + return; + } + for (const keyword of source) { + let isProfanity = false; + const lowerCasedKeyword = keyword.toLowerCase(); + for (const word of config.blacklist) { + if (lowerCasedKeyword.includes(word)) { + isProfanity = true; + break; + } + } + if (!isProfanity) { + keywordFunc(keyword); + } + } + } + + static isTopicAllowed(config: TopicCloudAdminData, comments: number, users: number, + upvotes: number, firstTimeStamp: Date, lastTimeStamp: Date) { + return !((config.minQuestions > comments) || + (config.minQuestioners > users) || + (config.minUpvotes > upvotes) || + (config.startDate && new Date(config.startDate) > firstTimeStamp) || + (config.endDate && new Date(config.endDate) < lastTimeStamp)); } static get getDefaultAdminData(): TopicCloudAdminData { - let data = JSON.parse(localStorage.getItem(this.adminKey)); + let data: TopicCloudAdminData = JSON.parse(localStorage.getItem(this.adminKey)); if (!data) { data = { blacklist: [], @@ -49,7 +83,12 @@ export class TopicCloudAdminService { considerVotes: true, profanityFilter: ProfanityFilter.none, blacklistIsActive: true, - keywordORfulltext: KeywordOrFulltext.both + keywordORfulltext: KeywordOrFulltext.both, + minQuestioners: 1, + minQuestions: 1, + minUpvotes: 0, + startDate: null, + endDate: null }; } return data; diff --git a/src/app/utils/filter-options.ts b/src/app/utils/filter-options.ts index 9268cbcf7cdb9cfab1ba03bc72c50e772f977386..d2791cb73b89eedd7b2f5e338f3870b100913957 100644 --- a/src/app/utils/filter-options.ts +++ b/src/app/utils/filter-options.ts @@ -32,7 +32,7 @@ export class CommentFilter { timeStampUntil = 0; periodSet: Period = Period.twoWeeks; - timeStampNow = 0; + timeStampNow = 0; constructor(obj?: any) { if (obj) { diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index d139a60913cd19406ba00de93321936e2730b990..5c7697fc7c458cd8247fc99650b10b9ed03578b0 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -87,7 +87,6 @@ "de": "Deutsch", "en": "Englisch", "fr": "Französisch", - "empty-nouns": "Keine Nomen enthalten", "select-all": "Alles auswählen", "lang-button-hint": "Ausgewählte Sprache für die Rechtschreibprüfung", "select-all-hint": "Alle Stichwörter auswählen", @@ -95,7 +94,8 @@ "edit-keyword-hint": "Stichwort editieren", "editing-done-hint": "Editierung abschliessen", "force-language-selection": "Automatische Spracherkennung unpräzise, bitte gewählte Sprache prüfen!", - "add-manually": "Geben Sie bitte die Stichwörter unten mit separatem Komma ein" + "add-manually": "Geben Sie bitte die Stichwörter unten mit separatem Komma ein", + "select-keywords": "Wählen Sie die Stichwörter für Ihre Frage aus" }, "comment-page": { "a11y-comment_delete": "Löscht diese Frage", @@ -417,7 +417,13 @@ }, "dialog-comment": { "read-more": "Mehr lesen", - "read-less": "Weniger lesen" + "read-less": "Weniger lesen", + "topic-requirement-title": "Themen Anforderung", + "topic-requirement-questions": "Minimale Anzahl an Fragen", + "topic-requirement-questioners": "Minimale Anzahl an Fragensteller*innen", + "topic-requirement-upvotes": "Minimale Anzahl an Upvotes", + "topic-requirement-begin-datetime": "Frühester Kommentar", + "topic-requirement-end-datetime": "Letzter Kommentar" }, "tag-cloud": { "demo-data-topic": "Thema %d", diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index e6c7cc0aeef3ec4b54db5c988073489008ce7394..463c5fa9632a241d6a8a1066f5c16a50c68a70e6 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -88,7 +88,6 @@ "de": "German", "en": "English", "fr": "French", - "empty-nouns": "No nouns included", "select-all": "Select all", "lang-button-hint": "Selected language for spell check", "select-all-hint": "Select all keywords", @@ -96,7 +95,8 @@ "edit-keyword-hint": "Edit keyword", "editing-done-hint": "Finish editing", "force-language-selection": "Language detection inaccurate, please check language settings!", - "add-manually": "You can manually enter the keywords separated with a comma " + "add-manually": "You can manually enter the keywords separated with a comma", + "select-keywords": "Choose the keywords for your question" }, "comment-page": { "a11y-comment_delete": "Deletes this question", @@ -425,7 +425,13 @@ }, "dialog-comment": { "read-more": "read more", - "read-less": "read less" + "read-less": "read less", + "topic-requirement-title": "Topic requirement", + "topic-requirement-questions": "Minimum number of questions", + "topic-requirement-questioners": "Minimum number of questioners", + "topic-requirement-upvotes": "Minimum number of upvotes", + "topic-requirement-begin-datetime": "Earliest comment", + "topic-requirement-end-datetime": "Last comment" }, "tag-cloud-config":{ "general":"General", @@ -477,8 +483,8 @@ "highestWeight-tooltip": "show x tags with the highest weight", "rotate-weight": "Rotate some entries of this weight class randomly by x degrees", "font":"Font", - "reset-btn": "Reset", - "font-family-tooltip": "Select font", + "reset-btn": "Reset", + "font-family-tooltip": "Select font", "bold-notation-tooltip": "Select font-thickness bold", "font-style-bold" : "Bold", "font-style-italic": "Italic", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index 8db95744642ad14565f4bb52afdff6b7621bc5fd..fb3f73fcb18a89ae63fe49d4617b1b9f874456f6 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -92,7 +92,6 @@ "de": "Deutsch", "en": "Englisch", "fr": "Französisch", - "empty-nouns": "Keine Nomen enthalten", "select-all": "Alles auswählen", "lang-button-hint": "Ausgewählte Sprache für die Rechtschreibprüfung", "select-all-hint": "Alle Stichwörter auswählen", @@ -300,7 +299,13 @@ "word-is-not-profanity": "Wort ist nicht profan", "words-in-profanity": "Profane Wörter", "language": "Sprache", - "preview": "Vorschau" + "preview": "Vorschau", + "topic-requirement-title": "Themen Anforderung", + "topic-requirement-questions": "Minimale Anzahl an Fragen", + "topic-requirement-questioners": "Minimale Anzahl an Fragensteller*innen", + "topic-requirement-upvotes": "Minimale Anzahl an Upvotes", + "topic-requirement-begin-datetime": "Frühester Kommentar", + "topic-requirement-end-datetime": "Letzter Kommentar" }, "topic-cloud-confirm-dialog": { "cancel": "Abbrechen", diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index 79aca63883ac07fc63317c78853254e61edf9da0..17c661d8959354ebb34c7379c67c8d62d56d6cb9 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -102,7 +102,6 @@ "de": "German", "en": "English", "fr": "French", - "empty-nouns": "No nouns included", "select-all": "Select all", "lang-button-hint": "Selected language for spell check", "select-all-hint": "Select all keywords", @@ -306,7 +305,13 @@ "word-is-not-profanity": "Word is not profane", "words-in-profanity": "Words in profanity filter", "language": "Language", - "preview": "Preview" + "preview": "Preview", + "topic-requirement-title": "Topic requirement", + "topic-requirement-questions": "Minimum number of questions", + "topic-requirement-questioners": "Minimum number of questioners", + "topic-requirement-upvotes": "Minimum number of upvotes", + "topic-requirement-begin-datetime": "Earliest comment", + "topic-requirement-end-datetime": "Last comment" }, "topic-cloud-confirm-dialog":{ "cancel": "Cancel", @@ -372,7 +377,7 @@ "rotate-weight": "Rotate some entries of this weight class randomly by x degrees", "font":"Font", "reset-btn": "Reset", - "font-family-tooltip": "Select font", + "font-family-tooltip": "Select font", "bold-notation-tooltip": "Select font-thickness bold", "font-style-bold" : "Bold", "font-style-italic": "Italic",