diff --git a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts index df77ec4f0b719af90b159d2a4eef17c2e9dfcc40..3f18d73fd46f226e8395da1ab794fe147a131d04 100644 --- a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts +++ b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts @@ -4,7 +4,6 @@ import { ViewCommentDataComponent } from '../../view-comment-data/view-comment-d import { NotificationService } from '../../../../services/util/notification.service'; import { LanguageService } from '../../../../services/util/language.service'; import { TranslateService } from '@ngx-translate/core'; -import { WriteCommentComponent } from '../../write-comment/write-comment.component'; import { ExplanationDialogComponent } from '../explanation-dialog/explanation-dialog.component'; import { DeepLService, FormalityType, TargetLang } from '../../../../services/http/deep-l.service'; import { Observable } from 'rxjs'; @@ -95,7 +94,7 @@ export class DeepLDialogComponent implements OnInit, AfterViewInit { text: this.data.improvedText, view: this.improved }; - this.radioButtonValue = this.normalValue; + this.radioButtonValue = this.improvedValue; } ngAfterViewInit() { 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 42b959a8fb6bccabd7877bcaa4a58e774069cccb..e0cc85d7b401aa39ada5ae2dee8264bcbab9d69c 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/TopicCloudAdminData.ts @@ -1,5 +1,26 @@ import { ProfanityFilter } from '../../../../models/room'; +export interface TopicCloudAdminDataScoring { + score: number; +} + +export enum TopicCloudAdminDataScoringKey { + countComments = 'countComments', + countUsers = 'countUsers', + countSelectedByQuestioner = 'countSelectedByQuestioner', + countKeywordByModerator = 'countKeywordByModerator', + countKeywordByCreator = 'countKeywordByCreator', + countCommentsAnswered = 'countCommentsAnswered', + summedUpvotes = 'summedUpvotes', + summedDownvotes = 'summedDownvotes', + summedVotes = 'summedVotes', + cappedSummedVotes = 'cappedSummedVotes' +} + +export type TopicCloudAdminDataScoringObject = { + [key in TopicCloudAdminDataScoringKey]: TopicCloudAdminDataScoring; +}; + export interface TopicCloudAdminData { blacklist: string[]; wantedLabels: { @@ -15,8 +36,65 @@ export interface TopicCloudAdminData { minUpvotes: number; startDate: string; endDate: string; + scorings: TopicCloudAdminDataScoringObject; } +export const ensureDefaultScorings = (data: TopicCloudAdminData) => { + if (!data.scorings) { + data.scorings = {} as TopicCloudAdminDataScoringObject; + } + for (const option of Object.keys(TopicCloudAdminDataScoringKey)) { + if (data.scorings[option]) { + continue; + } + switch (option) { + case TopicCloudAdminDataScoringKey.cappedSummedVotes: + data.scorings[option] = { + score: 0.1 + }; + break; + case TopicCloudAdminDataScoringKey.countUsers: + data.scorings[option] = { + score: 0.5 + }; + break; + case TopicCloudAdminDataScoringKey.countCommentsAnswered: + case TopicCloudAdminDataScoringKey.countKeywordByCreator: + case TopicCloudAdminDataScoringKey.countKeywordByModerator: + case TopicCloudAdminDataScoringKey.countSelectedByQuestioner: + data.scorings[option] = { + score: 1 + }; + break; + default: + data.scorings[option] = { + score: 0 + }; + break; + } + } +}; + +export type TopicCloudAdminDataScoringPreset = { + [key in TopicCloudAdminDataScoringKey]: { + min: number; + max: number; + }; +}; + +export const keywordsScoringMinMax: TopicCloudAdminDataScoringPreset = { + countComments: { min: -5, max: 5 }, + countUsers: { min: -5, max: 5 }, + countSelectedByQuestioner: { min: -5, max: 5 }, + countKeywordByModerator: { min: -5, max: 5 }, + countKeywordByCreator: { min: -5, max: 5 }, + countCommentsAnswered: { min: -5, max: 5 }, + summedUpvotes: { min: -5, max: 5 }, + summedDownvotes: { min: -5, max: 5 }, + summedVotes: { min: -5, max: 5 }, + cappedSummedVotes: { min: -5, max: 5 } +}; + export enum KeywordOrFulltext { keyword, fulltext, @@ -40,33 +118,33 @@ export class Labels { } const deLabels: Label[] = [ - {tag: 'sb', label: 'Subjekt', enabledByDefault: true}, - {tag: 'op', label: 'Präpositionalobjekt', enabledByDefault: true}, - {tag: 'og', label: 'Genitivobjekt', enabledByDefault: true}, - {tag: 'da', label: 'Dativobjekt', enabledByDefault: true}, - {tag: 'oa', label: 'Akkusativobjekt', enabledByDefault: true}, - {tag: 'ROOT', label: 'Satzkernelement', enabledByDefault: true}, - {tag: 'pd', label: 'Prädikat', enabledByDefault: false}, - {tag: 'ag', label: 'Genitivattribut', enabledByDefault: false}, - {tag: 'app', label: 'Apposition', enabledByDefault: false}, - {tag: 'nk', label: 'Nomen Kernelement', enabledByDefault: false}, - {tag: 'mo', label: 'Modifikator', enabledByDefault: false}, - {tag: 'cj', label: 'Konjunktor', enabledByDefault: false}, - {tag: 'par', label: 'Klammerzusatz', enabledByDefault: false} + { tag: 'sb', label: 'Subjekt', enabledByDefault: true }, + { tag: 'op', label: 'Präpositionalobjekt', enabledByDefault: true }, + { tag: 'og', label: 'Genitivobjekt', enabledByDefault: true }, + { tag: 'da', label: 'Dativobjekt', enabledByDefault: true }, + { tag: 'oa', label: 'Akkusativobjekt', enabledByDefault: true }, + { tag: 'ROOT', label: 'Satzkernelement', enabledByDefault: true }, + { tag: 'pd', label: 'Prädikat', enabledByDefault: false }, + { tag: 'ag', label: 'Genitivattribut', enabledByDefault: false }, + { tag: 'app', label: 'Apposition', enabledByDefault: false }, + { tag: 'nk', label: 'Nomen Kernelement', enabledByDefault: false }, + { tag: 'mo', label: 'Modifikator', enabledByDefault: false }, + { tag: 'cj', label: 'Konjunktor', enabledByDefault: false }, + { tag: 'par', label: 'Klammerzusatz', enabledByDefault: false } ]; const enLabels: Label[] = [ - {tag: 'nsubj', label: 'Nominal subject', enabledByDefault: true}, - {tag: 'pobj', label: 'Object of preposition', enabledByDefault: true}, - {tag: 'dobj', label: 'Direct object', enabledByDefault: true}, - {tag: 'compound', label: 'Compound', enabledByDefault: true}, - {tag: 'nsubjpass', label: 'Passive nominal subject', enabledByDefault: true}, - {tag: 'ROOT', label: 'Sentence kernel element', enabledByDefault: true}, - {tag: 'nummod', label: 'Numeric modifier', enabledByDefault: false}, - {tag: 'amod', label: 'Adjectival modifier', enabledByDefault: false}, - {tag: 'npadvmod', label: 'Noun phrase as adverbial modifier', enabledByDefault: false}, - {tag: 'conj', label: 'Conjunct', enabledByDefault: false}, - {tag: 'intj', label: 'Interjection', enabledByDefault: false} + { tag: 'nsubj', label: 'Nominal subject', enabledByDefault: true }, + { tag: 'pobj', label: 'Object of preposition', enabledByDefault: true }, + { tag: 'dobj', label: 'Direct object', enabledByDefault: true }, + { tag: 'compound', label: 'Compound', enabledByDefault: true }, + { tag: 'nsubjpass', label: 'Passive nominal subject', enabledByDefault: true }, + { tag: 'ROOT', label: 'Sentence kernel element', enabledByDefault: true }, + { tag: 'nummod', label: 'Numeric modifier', enabledByDefault: false }, + { tag: 'amod', label: 'Adjectival modifier', enabledByDefault: false }, + { tag: 'npadvmod', label: 'Noun phrase as adverbial modifier', enabledByDefault: false }, + { tag: 'conj', label: 'Conjunct', enabledByDefault: false }, + { tag: 'intj', label: 'Interjection', enabledByDefault: false } ]; 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 222f911ca6e74c857377ccd66b5be3d498e777a8..bc7cb8d6563d74fe1872b5a72f7be745b16748ce 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,11 +28,43 @@ </mat-radio-group> </mat-card> - <mat-card style="background: none; margin-bottom: 10px;"> - <mat-slide-toggle [(ngModel)]="considerVotes"> - {{'topic-cloud-dialog.consider-votes' | translate}} - </mat-slide-toggle> - </mat-card> + <mat-accordion> + <mat-expansion-panel class="color-background margin-top margin-bottom keyword-scoring"> + <mat-expansion-panel-header class="color-background"> + <mat-panel-title> + {{'topic-cloud-dialog.keyword-scoring-header' | translate}} + <mat-icon class="help-explanation" + matTooltip="{{'topic-cloud-dialog.keyword-scoring-header-info' | translate}}">help + </mat-icon> + </mat-panel-title> + </mat-expansion-panel-header> + <ng-container *ngFor="let option of scoringOptions"> + <ars-row fxLayout="row"> + <label + id="keyword-scoring-{{option}}">{{'topic-cloud-dialog.keyword-scoring-' + option | translate}}</label> + <mat-icon class="help-explanation" + matTooltip="{{'topic-cloud-dialog.keyword-scoring-' + option + '-info' | translate}}">help + </mat-icon> + <ars-fill></ars-fill> + <label>{{scorings[option].score}}</label> + </ars-row> + <mat-slider + [min]="scoringMinMax[option].min" + [max]="scoringMinMax[option].max" + [(ngModel)]="scorings[option].score" + [step]="0.1" + [thumbLabel]="true" + aria-labelledby="keyword-scoring-{{option}}"> + </mat-slider> + </ng-container> + <button mat-button class="themeRequirementInput reset" + [disabled]="isDefaultScoring()" + (click)="setDefaultScoring()"> + {{'topic-cloud-dialog.topic-requirement-reset' | translate}} + </button> + </mat-expansion-panel> + </mat-accordion> + <div *ngIf="isCreatorOrMod"> <mat-card style="background: none; margin-bottom: 10px;"> <mat-slide-toggle (change)="showMessage('words-will-be-overwritten', $event.checked)" @@ -339,7 +371,7 @@ <p [ngClass]="{'animation-blink': searchMode}" matTooltip="{{'topic-cloud-dialog.keyword-counter' | translate}}"> {{searchMode ? filteredKeywords.length : - selectedTabIndex === 0 ? keywords.size : blacklistKeywords.length}}</p> + selectedTabIndex === 0 ? keywords.size : blacklistKeywords.length}}</p> </div> <div class="margin-left vertical-center"> <button [ngClass]="{'animation-blink': sortMode!=='alphabetic'}" mat-icon-button [matMenuTriggerFor]="sortMenu"> 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 d2d7016945f138f7beb5eee32166f0a01caa6b09..1887a126062e4152b1d0adee851b47de72ad894b 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 @@ -186,3 +186,19 @@ mat-dialog-content { color: var(--on-secondary); } } + +.keyword-scoring { + width: 100%; + + mat-slider { + width: 100%; + } +} + +.help-explanation { + width: 1.2em; + height: 1.2em; + line-height: 1.2em; + font-size: 1.2em; + margin: auto 0 auto 0.25em; +} 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 a21c7bce9b54ab2ed9e3a07d073e5c911a8fb2dd..4744be85b9d6509aa19f3e435ddb72ceec9ec197 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 @@ -7,7 +7,13 @@ import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../../services/util/language.service'; import { TopicCloudAdminService } from '../../../../services/util/topic-cloud-admin.service'; import { ProfanityFilterService } from '../../../../services/util/profanity-filter.service'; -import { TopicCloudAdminData, Labels, spacyLabels, KeywordOrFulltext } from './TopicCloudAdminData'; +import { + TopicCloudAdminData, + Labels, + spacyLabels, + KeywordOrFulltext, + TopicCloudAdminDataScoringObject, TopicCloudAdminDataScoringKey, keywordsScoringMinMax, ensureDefaultScorings +} from './TopicCloudAdminData'; import { User } from '../../../../models/user'; import { Comment } from '../../../../models/comment'; import { CommentService } from '../../../../services/http/comment.service'; @@ -60,8 +66,12 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { startDate: string; endDate: string; selectedTabIndex = 0; + scorings: TopicCloudAdminDataScoringObject; + scoringOptions = Object.keys(TopicCloudAdminDataScoringKey); + scoringMinMax = keywordsScoringMinMax; keywords: Map<string, Keyword> = new Map<string, Keyword>(); + defaultScorings: TopicCloudAdminDataScoringObject; private topicCloudAdminData: TopicCloudAdminData; private profanityFilter: boolean; private censorPartialWordsCheck: boolean; @@ -84,6 +94,9 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); + const emptyData = {} as TopicCloudAdminData; + ensureDefaultScorings(emptyData); + this.defaultScorings = emptyData.scorings; } ngOnInit(): void { @@ -194,14 +207,14 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { /** * Returns a lambda which closes the dialog on call. */ - buildCloseDialogActionCallback(): () => void { + buildCloseDialogActionCallback(): () => void { return () => this.ngOnDestroy(); } /** * Returns a lambda which executes the dialog dedicated action on call. */ - buildSaveActionCallback(): () => void { + buildSaveActionCallback(): () => void { return () => this.save(); } @@ -313,7 +326,8 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { minQuestions: minQuestionsVerified, minUpvotes: minUpvotesVerified, startDate: this.startDate.length ? this.startDate : null, - endDate: this.endDate.length ? this.endDate : null + endDate: this.endDate.length ? this.endDate : null, + scorings: this.scorings }; this.topicCloudAdminService.setAdminData(this.topicCloudAdminData, true, this.data.user.role); } @@ -340,6 +354,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { this.minUpvotes = String(this.topicCloudAdminData.minUpvotes); this.startDate = this.topicCloudAdminData.startDate || ''; this.endDate = this.topicCloudAdminData.endDate || ''; + this.scorings = this.topicCloudAdminData.scorings; } } @@ -480,7 +495,7 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { if (this.selectedTabIndex === 0) { const entries = [...this.keywords.entries()]; this.filteredKeywords = entries.filter(([_, keyword]) => - keyword.keyword.toLowerCase().includes(this.searchedKeyword.toLowerCase()) + keyword.keyword.toLowerCase().includes(this.searchedKeyword.toLowerCase()) ).map(e => e[1]); } else { this.filteredKeywords = this.blacklistKeywords.filter(keyword => @@ -595,6 +610,25 @@ export class TopicCloudAdministrationComponent implements OnInit, OnDestroy { return ''; } } + + isDefaultScoring(): boolean { + for (const key of Object.keys(this.defaultScorings)) { + const subObject = this.defaultScorings[key]; + const refSubObject = this.scorings[key]; + for (const subKey in subObject) { + if (subObject[subKey] !== refSubObject[subKey]) { + return false; + } + } + } + return true; + } + + setDefaultScoring() { + for (const key of Object.keys(this.defaultScorings)) { + this.scorings[key] = { ...this.defaultScorings[key] }; + } + } } interface Keyword { 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 d1422670f2dc924aca61425ba711bec69d1ce5f6..bac19fa7a99d5c893ab1956a506364860e06b40f 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 @@ -19,6 +19,7 @@ import { Room } from '../../../../models/room'; import { ThemeService } from '../../../../../theme/theme.service'; import { Theme } from '../../../../../theme/Theme'; import { ExplanationDialogComponent } from '../explanation-dialog/explanation-dialog.component'; +import { ModeratorService } from '../../../../services/http/moderator.service'; import { UserRole } from '../../../../models/user-roles.enum'; import { RoomDataService } from '../../../../services/util/room-data.service'; import { Subscription } from 'rxjs'; @@ -57,6 +58,7 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { private _room: Room; private currentTheme: Theme; private _subscriptionCommentUpdates: Subscription; + private _currentModerators: string[]; constructor(public dialogRef: MatDialogRef<RoomCreatorPageComponent>, public dialog: MatDialog, @@ -68,6 +70,7 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { @Inject(MAT_DIALOG_DATA) public data: any, public eventService: EventService, private topicCloudAdminService: TopicCloudAdminService, + private moderatorService: ModeratorService, private themeService: ThemeService, private roomDataService: RoomDataService) { langService.langEmitter.subscribe(lang => translationService.use(lang)); @@ -86,7 +89,10 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { this._room = data.room; this.roomDataService.getRoomData(data.room.id).subscribe(roomData => { this.comments = roomData; - this.commentsLoadedCallback(true); + this.moderatorService.get(data.room.id).subscribe(moderators => { + this._currentModerators = moderators.map(moderator => moderator.accountId); + this.commentsLoadedCallback(true); + }); }); this._subscriptionCommentUpdates = this.roomDataService.receiveUpdates([{ finished: true }]) .subscribe(_ => this.commentsLoadedCallback()); @@ -101,6 +107,9 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { } commentsLoadedCallback(isNew = false) { + if (!this._currentModerators) { + return; + } this.allComments = this.getCommentCounts(this.comments); this.filteredComments = this.getCommentCounts(this.comments.filter(comment => this.tmpFilter.checkComment(comment))); if (isNew) { @@ -136,7 +145,8 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { } getCommentCounts(comments: Comment[]): CommentsCount { - const [data, users] = TagCloudDataService.buildDataFromComments(this._adminData, comments); + const [data, users] = TagCloudDataService + .buildDataFromComments(this._room.ownerId, this._currentModerators, this._adminData, comments); const counts = new CommentsCount(); counts.comments = comments.length; counts.users = users.size; 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 54c9b00f57a357f45ba18bc33fefbcf0ba05f3a9..9515eec1bae969a021716c2d6830d2c4a36b9aaa 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.ts @@ -32,6 +32,7 @@ import { SmartDebounce } from '../../../utils/smart-debounce'; import { Theme } from '../../../../theme/Theme'; import { MatDrawer } from '@angular/material/sidenav'; import { DeviceInfoService } from '../../../services/util/device-info.service'; +import { SyncFence } from '../../../utils/SyncFence'; class CustomPosition implements Position { left: number; @@ -70,6 +71,9 @@ const transformationRotationKiller = /rotate\(([^)]*)\)/; const maskedCharsRegex = /[“â€â€˜â€™â€žâ€šÂ«Â»â€¹â€ºã€Žã€ï¹ƒï¹„「ã€ï¹ï¹‚",《》〈〉'`#&]|(\s(lu|li’u)(?=\s))|(^lu\s)|(\sli’u$)/gm; +const CONDITION_ROOM = 0; +const CONDITION_BUILT = 1; + @Component({ selector: 'app-tag-cloud', templateUrl: './tag-cloud.component.html', @@ -113,6 +117,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { private _calcFont: string = null; private readonly _smartDebounce = new SmartDebounce(50, 1_000); private _currentTheme: Theme; + private _syncFenceBuildCloud: SyncFence; constructor(private commentService: CommentService, private langService: LanguageService, @@ -138,6 +143,8 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.question = localStorage.getItem('tag-cloud-question'); this._calcCanvas = document.createElement('canvas'); this._calcRenderContext = this._calcCanvas.getContext('2d'); + this._syncFenceBuildCloud = new SyncFence(2, + () => this.dataManager.bindToRoom(this.roomId, this.room.ownerId, this.userRole)); } private static getCurrentCloudParameters(): CloudParameters { @@ -215,6 +222,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.roomService.addToHistory(this.room.id); this.authenticationService.setAccess(this.shortId, UserRole.PARTICIPANT); } + this._syncFenceBuildCloud.resolveCondition(CONDITION_ROOM); }); }); }); @@ -231,7 +239,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { ngAfterContentInit() { this._calcFont = window.getComputedStyle(document.getElementById('tagCloudComponent')).fontFamily; - setTimeout(() => this.dataManager.bindToRoom(this.roomId, this.userRole)); + setTimeout(() => this._syncFenceBuildCloud.resolveCondition(CONDITION_BUILT)); this.dataManager.updateDemoData(this.translateService); this.setCloudParameters(TagCloudComponent.getCurrentCloudParameters(), false); } @@ -384,6 +392,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { admin.minUpvotes = data.admin.minUpvotes; admin.startDate = data.admin.startDate; admin.endDate = data.admin.endDate; + admin.scorings = data.admin.scorings; data.admin = undefined; this.topicCloudAdmin.setAdminData(admin, false, this.userRole); if (this.deviceInfo.isCurrentlyMobile) { diff --git a/src/app/services/util/profanity-filter.service.ts b/src/app/services/util/profanity-filter.service.ts index 73045faea036f26f3e2854f0ce53f4ffad7f5bc9..abf28369532eab09f468ca218f89e5bb6004e69f 100644 --- a/src/app/services/util/profanity-filter.service.ts +++ b/src/app/services/util/profanity-filter.service.ts @@ -17,7 +17,16 @@ export class ProfanityFilterService { badNL.splice(badNL.indexOf('nicht'), 1); const badDE = BadWords['de']; badDE.splice(badDE.indexOf('ische'), 1); - this.profanityWords = BadWords['en'] + badDE.push('frage'); + badDE.push('antwort'); + badDE.push('aufgabe'); + badDE.push('hallo'); + badDE.push('test'); + badDE.push('bzw'); + badDE.push('muss'); + const badEN = BadWords['en']; + badEN.push('more to come'); + this.profanityWords = badEN .concat(badDE) .concat(BadWords['fr']) .concat(BadWords['ar']) @@ -65,7 +74,7 @@ export class ProfanityFilterService { localStorage.removeItem(this.profanityKey); } - filterProfanityWords(str: string, censorPartialWordsCheck: boolean, censorLanguageSpecificCheck: boolean, lang?: string){ + filterProfanityWords(str: string, censorPartialWordsCheck: boolean, censorLanguageSpecificCheck: boolean, lang?: string) { let filteredString = str; let profWords = []; if (censorLanguageSpecificCheck) { diff --git a/src/app/services/util/tag-cloud-data.service.ts b/src/app/services/util/tag-cloud-data.service.ts index 2e4e038f25ce664a4ef8b47e965ef12406a151c2..f4ddb4012a1a4232e7c5f1f2cb47b88d1dfa8aaa 100644 --- a/src/app/services/util/tag-cloud-data.service.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -10,6 +10,7 @@ import { SpacyKeyword } from '../http/spacy.service'; import { UserRole } from '../../models/user-roles.enum'; import { CloudParameters } from '../../utils/cloud-parameters'; import { SmartDebounce } from '../../utils/smart-debounce'; +import { ModeratorService } from '../http/moderator.service'; export interface TagCloudDataTagEntry { weight: number; @@ -26,6 +27,8 @@ export interface TagCloudDataTagEntry { generatedByQuestionerCount: number; taggedCommentsCount: number; answeredCommentsCount: number; + commentsByCreator: number; + commentsByModerators: number; } export interface TagCloudMetaData { @@ -71,7 +74,6 @@ export class TagCloudDataService { private _metaDataBus: BehaviorSubject<TagCloudMetaData>; private _commentSubscription = null; private _roomId = null; - private _calcWeightType = TagCloudCalcWeightType.byLength; private _lastFetchedData: TagCloudData = null; private _lastFetchedComments: Comment[] = null; private _lastMetaData: TagCloudMetaData = null; @@ -80,10 +82,13 @@ export class TagCloudDataService { private _adminData: TopicCloudAdminData = null; private _subscriptionAdminData: Subscription; private _currentFilter: CommentFilter; + private _currentModerators: string[]; + private _currentOwner: string; private readonly _smartDebounce = new SmartDebounce(200, 3_000); constructor(private _tagCloudAdmin: TopicCloudAdminService, - private _roomDataService: RoomDataService) { + private _roomDataService: RoomDataService, + private _moderatorService: ModeratorService) { this._isDemoActive = false; this._isAlphabeticallySorted = false; this._dataBus = new BehaviorSubject<TagCloudData>(null); @@ -98,7 +103,10 @@ export class TagCloudDataService { this._metaDataBus = new BehaviorSubject<TagCloudMetaData>(null); } - static buildDataFromComments(adminData: TopicCloudAdminData, comments: Comment[]): [TagCloudData, Set<number>] { + static buildDataFromComments(roomOwner: string, + moderators: string[], + adminData: TopicCloudAdminData, + comments: Comment[]): [TagCloudData, Set<number>] { const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); const users = new Set<number>(); for (const comment of comments) { @@ -121,7 +129,9 @@ export class TagCloudDataService { lastTimeStamp: commentDate, generatedByQuestionerCount: 0, taggedCommentsCount: 0, - answeredCommentsCount: 0 + answeredCommentsCount: 0, + commentsByCreator: 0, + commentsByModerators: 0 }; data.set(keyword.text, current); } @@ -133,6 +143,11 @@ export class TagCloudDataService { current.generatedByQuestionerCount += +isFromQuestioner; current.taggedCommentsCount += +!!comment.tag; current.answeredCommentsCount += +!!comment.answer; + if (comment.creatorId === roomOwner) { + ++current.commentsByCreator; + } else if (moderators.includes(comment.creatorId)) { + ++current.commentsByModerators; + } if (comment.tag) { current.categories.add(comment.tag); } @@ -155,12 +170,17 @@ export class TagCloudDataService { ]; } - bindToRoom(roomId: string, userRole: UserRole): void { + bindToRoom(roomId: string, roomOwner: string, userRole: UserRole): void { if (this._subscriptionAdminData) { throw new Error('Room already bound.'); } + this._currentModerators = null; this._currentFilter = CommentFilter.currentFilter; this._roomId = roomId; + this._currentOwner = roomOwner; + this._moderatorService.get(roomId).subscribe(moderators => { + this._currentModerators = moderators.map(moderator => moderator.accountId); + }); this._lastFetchedComments = null; this._subscriptionAdminData = this._tagCloudAdmin.getAdminData.subscribe(adminData => { this.onReceiveAdminData(adminData, true); @@ -212,7 +232,9 @@ export class TagCloudDataService { lastTimeStamp: new Date(), generatedByQuestionerCount: 0, taggedCommentsCount: 0, - answeredCommentsCount: 0 + answeredCommentsCount: 0, + commentsByCreator: 0, + commentsByModerators: 0 }); } }); @@ -226,17 +248,6 @@ export class TagCloudDataService { return this._dataBus.value; } - set weightCalcType(type: TagCloudCalcWeightType) { - if (type !== this._calcWeightType) { - this._calcWeightType = type; - this.rebuildTagData(); - } - } - - get weightCalcType(): TagCloudCalcWeightType { - return this._calcWeightType; - } - get demoActive(): boolean { return this._isDemoActive; } @@ -306,7 +317,6 @@ export class TagCloudDataService { private onReceiveAdminData(data: TopicCloudAdminData, update = false) { this._adminData = data; - this._calcWeightType = this._adminData.considerVotes ? TagCloudCalcWeightType.byLengthAndVotes : TagCloudCalcWeightType.byLength; if (update) { this.rebuildTagData(); } @@ -330,20 +340,17 @@ export class TagCloudDataService { } private calculateWeight(tagData: TagCloudDataTagEntry): number { - const value = Math.max(tagData.cachedVoteCount, 0); - const additional = (tagData.distinctUsers.size - 1) * 0.5 + - tagData.comments.reduce((acc, comment) => acc + +!!comment.createdFromLecturer, 0) + - tagData.generatedByQuestionerCount + - tagData.taggedCommentsCount + - tagData.answeredCommentsCount; - switch (this._calcWeightType) { - case TagCloudCalcWeightType.byVotes: - return value + additional; - case TagCloudCalcWeightType.byLengthAndVotes: - return value / 10.0 + tagData.comments.length + additional; - default: - return tagData.comments.length + additional; - } + const scorings = this._adminData.scorings; + return tagData.comments.length * scorings.countComments.score + + tagData.distinctUsers.size * scorings.countUsers.score + + tagData.generatedByQuestionerCount * scorings.countSelectedByQuestioner.score + + tagData.commentsByModerators * scorings.countKeywordByModerator.score + + tagData.commentsByCreator * scorings.countKeywordByCreator.score + + tagData.answeredCommentsCount * scorings.countCommentsAnswered.score + + tagData.cachedUpVotes * scorings.summedUpvotes.score + + tagData.cachedDownVotes * scorings.summedDownvotes.score + + tagData.cachedVoteCount * scorings.summedVotes.score + + Math.max(tagData.cachedVoteCount, 0) * scorings.cappedSummedVotes.score; } private rebuildTagData() { @@ -353,13 +360,14 @@ export class TagCloudDataService { const currentMeta = this._isDemoActive ? this._lastMetaData : this._currentMetaData; const filteredComments = this._lastFetchedComments.filter(comment => this._currentFilter.checkComment(comment)); currentMeta.commentCount = filteredComments.length; - const [data, users] = TagCloudDataService.buildDataFromComments(this._adminData, filteredComments); + const [data, users] = TagCloudDataService + .buildDataFromComments(this._currentOwner, this._currentModerators, this._adminData, filteredComments); let minWeight = null; let maxWeight = null; for (const value of data.values()) { value.weight = this.calculateWeight(value); - minWeight = Math.min(value.weight, minWeight || value.weight); - maxWeight = Math.max(value.weight, maxWeight || value.weight); + minWeight = Math.min(value.weight, minWeight === null ? value.weight : minWeight); + maxWeight = Math.max(value.weight, maxWeight === null ? value.weight : maxWeight); } //calculate weight counts and adjusted weights const same = minWeight === maxWeight; diff --git a/src/app/services/util/topic-cloud-admin.service.ts b/src/app/services/util/topic-cloud-admin.service.ts index eea4ab450faf91e970d0c232a52a3142a674aeeb..9875cef1ac7c0c23f1525b932f98f0cc7f7c7bbd 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { + ensureDefaultScorings, KeywordOrFulltext, spacyLabels, TopicCloudAdminData @@ -47,7 +48,8 @@ export class TopicCloudAdminService { minQuestions: admin.minQuestions, minUpvotes: admin.minUpvotes, startDate: admin.startDate, - endDate: admin.endDate + endDate: admin.endDate, + scorings: admin.scorings }; room.tagCloudSettings = JSON.stringify(settings); } @@ -120,9 +122,11 @@ export class TopicCloudAdminService { minQuestions: 1, minUpvotes: 0, startDate: null, - endDate: null + endDate: null, + scorings: null }; } + ensureDefaultScorings(data); return data; } diff --git a/src/app/utils/SyncFence.ts b/src/app/utils/SyncFence.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbe6079a65f6badff2f81e7c4fd6b533f7a9b576 --- /dev/null +++ b/src/app/utils/SyncFence.ts @@ -0,0 +1,17 @@ +export class SyncFence { + private readonly _conditions: boolean[]; + + constructor(public readonly conditionCount: number, + public readonly satisfyCallback: () => void) { + this._conditions = new Array(conditionCount).fill(false); + } + + resolveCondition(index: number) { + if (!this._conditions[index]) { + this._conditions[index] = true; + if (this._conditions.every(condition => condition)) { + this.satisfyCallback(); + } + } + } +} diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index e0e01240dcb5f5c9f6dc9070c56749594eb5400e..8478bd9d4e5b0f847258520a4124ed519182e679 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -416,7 +416,28 @@ "question-count-plural": "Fragen", "edit-keyword-tip": "Neues Thema", "no-keywords-note": "Es gibt keine Themen.", - "consider-votes": "Bewertungen der Fragen berücksichtigen", + "keyword-scoring-header": "Bewertung der Stichwörter", + "keyword-scoring-header-info": "Einstellbare Multiplikatoren für die Gewichtung der Schlüsselwörter", + "keyword-scoring-countComments": "Anzahl Kommentare", + "keyword-scoring-countComments-info": "Anzahl der Kommentare, aus denen die Schlüsselwörter extrahiert wurden", + "keyword-scoring-countUsers": "Anzahl Fragensteller*innen", + "keyword-scoring-countUsers-info": "Anzahl exklusiver Fragensteller*innen, die dieses Schlüsselwort verwendet haben", + "keyword-scoring-countSelectedByQuestioner": "Anzahl verifiziert", + "keyword-scoring-countSelectedByQuestioner-info": "Anzahl der Verifizierungen eines Schlüsselworts durch einen Fragesteller*in", + "keyword-scoring-countKeywordByModerator": "Anzahl Schlüsselwörter von Moderatoren", + "keyword-scoring-countKeywordByModerator-info": "Anzahl der Verwendungen dieses Schlüsselworts durch Moderatoren", + "keyword-scoring-countKeywordByCreator": "Anzahl Schlüsselwörter vom Ersteller", + "keyword-scoring-countKeywordByCreator-info": "Anzahl der Verwendungen dieses Schlüsselworts durch den Ersteller", + "keyword-scoring-countCommentsAnswered": "Anzahl beantwortete Kommentare", + "keyword-scoring-countCommentsAnswered-info": "Anzahl der Kommentare, die dieses Stichwort enthalten und beantwortet wurden", + "keyword-scoring-summedUpvotes": "Anzahl aller Upvotes", + "keyword-scoring-summedUpvotes-info": "Anzahl aller Upvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-summedDownvotes": "Anzahl aller Downvotes", + "keyword-scoring-summedDownvotes-info": "Anzahl aller Downvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-summedVotes": "Summe aller Votes", + "keyword-scoring-summedVotes-info": "Summe aus den Up- und Downvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-cappedSummedVotes": "Summe aller Votes (>= 0)", + "keyword-scoring-cappedSummedVotes-info": "(Siehe Summe aller Votes). Die Summe kann nicht unter null fallen und wird minimal 0", "profanity": "Vulgäre Wörter mit »***« überschreiben", "hide-blacklist-words": "Themen aus der Blacklist verbergen", "sort-alpha": "Alphabetisch", diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index bdd01c3c69a22db64f32a77ddd8e5d41ee4c520c..3c0a3983993b6705eb691ee9b5b8793705b5d6fa 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -426,7 +426,28 @@ "question-count-plural": "Questions", "edit-keyword-tip": "New topic", "no-keywords-note": "There are no topics.", - "consider-votes": "Consider Votes", + "keyword-scoring-header": "Keyword weighting", + "keyword-scoring-header-info": "Adjustable multipliers for the weighting of keywords", + "keyword-scoring-countComments": "Number of comments", + "keyword-scoring-countComments-info": "Number of comments from which the keywords have been extracted", + "keyword-scoring-countUsers": "Number of questioners", + "keyword-scoring-countUsers-info": "Number of exclusive questioners who used this keyword", + "keyword-scoring-countSelectedByQuestioner": "Number verified", + "keyword-scoring-countSelectedByQuestioner-info": "Number of times a keyword has been verified by a questioner", + "keyword-scoring-countKeywordByModerator": "Number of keywords from moderators", + "keyword-scoring-countKeywordByModerator-info": "Number of times this keyword has been used by moderators", + "keyword-scoring-countKeywordByCreator": "Number of keywords from creator", + "keyword-scoring-countKeywordByCreator-info": "Number of times this keyword has been used by the creator", + "keyword-scoring-countCommentsAnswered": "Number of answered comments", + "keyword-scoring-countCommentsAnswered-info": "Number of comments that have this keyword and have been answered", + "keyword-scoring-summedUpvotes": "Number of all upvotes", + "keyword-scoring-summedUpvotes-info": "Number of all upvotes made on the comments with this keyword", + "keyword-scoring-summedDownvotes": "Number of all downvotes", + "keyword-scoring-summedDownvotes-info": "Number of all downvotes made on the comments with this keyword", + "keyword-scoring-summedVotes": "Sum of all votes", + "keyword-scoring-summedVotes-info": "Sum of the upvotes and downvotes made on the comments with this keyword", + "keyword-scoring-cappedSummedVotes": "Sum of all votes (>= 0)", + "keyword-scoring-cappedSummedVotes-info": "(See the sum of all votes). The sum cannot fall below zero and becomes a minimum of 0", "profanity": "Censor profanity", "hide-blacklist-words": "Hide blacklist keywords", "sort-alpha": "Alphabetically", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index 2dd52369fb210779bbd27fdb4312d73d381b9047..e9e12de312d487a699f8f140c96bf195fb21dde6 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -321,7 +321,28 @@ "question-count-plural": "Fragen", "edit-keyword-tip": "Neues Thema", "no-keywords-note": "Es gibt keine Themen.", - "consider-votes": "Bewertungen der Fragen berücksichtigen", + "keyword-scoring-header": "Bewertung der Stichwörter", + "keyword-scoring-header-info": "Einstellbare Multiplikatoren für die Gewichtung der Schlüsselwörter", + "keyword-scoring-countComments": "Anzahl Kommentare", + "keyword-scoring-countComments-info": "Anzahl der Kommentare, aus denen die Schlüsselwörter extrahiert wurden", + "keyword-scoring-countUsers": "Anzahl Fragensteller*innen", + "keyword-scoring-countUsers-info": "Anzahl exklusiver Fragensteller*innen, die dieses Schlüsselwort verwendet haben", + "keyword-scoring-countSelectedByQuestioner": "Anzahl verifiziert", + "keyword-scoring-countSelectedByQuestioner-info": "Anzahl der Verifizierungen eines Schlüsselworts durch einen Fragesteller*in", + "keyword-scoring-countKeywordByModerator": "Anzahl Schlüsselwörter von Moderatoren", + "keyword-scoring-countKeywordByModerator-info": "Anzahl der Verwendungen dieses Schlüsselworts durch Moderatoren", + "keyword-scoring-countKeywordByCreator": "Anzahl Schlüsselwörter vom Ersteller", + "keyword-scoring-countKeywordByCreator-info": "Anzahl der Verwendungen dieses Schlüsselworts durch den Ersteller", + "keyword-scoring-countCommentsAnswered": "Anzahl beantwortete Kommentare", + "keyword-scoring-countCommentsAnswered-info": "Anzahl der Kommentare, die dieses Stichwort enthalten und beantwortet wurden", + "keyword-scoring-summedUpvotes": "Anzahl aller Upvotes", + "keyword-scoring-summedUpvotes-info": "Anzahl aller Upvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-summedDownvotes": "Anzahl aller Downvotes", + "keyword-scoring-summedDownvotes-info": "Anzahl aller Downvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-summedVotes": "Summe aller Votes", + "keyword-scoring-summedVotes-info": "Summe aus den Up- und Downvotes, die auf den Kommentaren mit diesem Schlüsselwort gemacht worden sind", + "keyword-scoring-cappedSummedVotes": "Summe aller Votes (>= 0)", + "keyword-scoring-cappedSummedVotes-info": "(Siehe Summe aller Votes). Die Summe kann nicht unter null fallen und wird minimal 0", "profanity": "Vulgäre Wörter mit »***« überschreiben", "hide-blacklist-words": "Themen aus der Blacklist verbergen", "sort-alpha": "Alphabetisch", diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index 7ae49a217986f530b6919a89667a1a0620c3861b..a3fbef81d5d1c897b18aac267b8a972f8c03038c 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -327,7 +327,28 @@ "question-count-plural": "Questions", "edit-keyword-tip": "New topic", "no-keywords-note": "There are no topics.", - "consider-votes": "Consider Votes", + "keyword-scoring-header": "Keyword weighting", + "keyword-scoring-header-info": "Adjustable multipliers for the weighting of keywords", + "keyword-scoring-countComments": "Number of comments", + "keyword-scoring-countComments-info": "Number of comments from which the keywords have been extracted", + "keyword-scoring-countUsers": "Number of questioners", + "keyword-scoring-countUsers-info": "Number of exclusive questioners who used this keyword", + "keyword-scoring-countSelectedByQuestioner": "Number verified", + "keyword-scoring-countSelectedByQuestioner-info": "Number of times a keyword has been verified by a questioner", + "keyword-scoring-countKeywordByModerator": "Number of keywords from moderators", + "keyword-scoring-countKeywordByModerator-info": "Number of times this keyword has been used by moderators", + "keyword-scoring-countKeywordByCreator": "Number of keywords from creator", + "keyword-scoring-countKeywordByCreator-info": "Number of times this keyword has been used by the creator", + "keyword-scoring-countCommentsAnswered": "Number of answered comments", + "keyword-scoring-countCommentsAnswered-info": "Number of comments that have this keyword and have been answered", + "keyword-scoring-summedUpvotes": "Number of all upvotes", + "keyword-scoring-summedUpvotes-info": "Number of all upvotes made on the comments with this keyword", + "keyword-scoring-summedDownvotes": "Number of all downvotes", + "keyword-scoring-summedDownvotes-info": "Number of all downvotes made on the comments with this keyword", + "keyword-scoring-summedVotes": "Sum of all votes", + "keyword-scoring-summedVotes-info": "Sum of the upvotes and downvotes made on the comments with this keyword", + "keyword-scoring-cappedSummedVotes": "Sum of all votes (>= 0)", + "keyword-scoring-cappedSummedVotes-info": "(See the sum of all votes). The sum cannot fall below zero and becomes a minimum of 0", "profanity": "Censor profanity", "hide-blacklist-words": "Hide blacklist keywords", "sort-alpha": "Alphabetically",