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/topic-cloud-administration/topic-cloud-administration.component.html b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.html index 06aa818ba806b9e31e753813169f82e597594369..f621877ae21001e348de4ff1dc69bf4c9ead0312 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 @@ -108,10 +108,10 @@ 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 [(ngModel)]="wantedLabels.de"> + <mat-selection-list *ngIf="wantedLabels" [(ngModel)]="wantedLabels.de"> <mat-option class="color-on-surface" (click)="selectAllDE(); allSelectedDE = !allSelectedDE"> <mat-label> @@ -126,7 +126,7 @@ </mat-selection-list> </mat-tab> <mat-tab label="{{'topic-cloud-dialog.english' | translate}}"> - <mat-selection-list [(ngModel)]="wantedLabels.en"> + <mat-selection-list *ngIf="wantedLabels" [(ngModel)]="wantedLabels.en"> <mat-option class="color-on-surface" (click)="selectAllEN(); allSelectedEN = !allSelectedEN"> <mat-label> 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 2e25c141dbb9a8e7b2d708368cc82f26b369c115..8bf801d639de9196d687dd999531981f0cc3cdf0 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -140,7 +140,7 @@ export class CommentListComponent implements OnInit, OnDestroy { dialogRef.componentInstance.roomId = this.room.id; }); this.eventService.on<string>('setTagConfig').subscribe(tag => { - this.clickedOnTag(tag); + this.clickedOnKeyword(tag); }); nav('tags', () => { const updRoom = JSON.parse(JSON.stringify(this.room)); @@ -459,7 +459,6 @@ export class CommentListComponent implements OnInit, OnDestroy { this.sortComments(this.currentSort); return; } - console.log(compare); this.filteredComments = this.commentsFilteredByTime.filter(c => { switch (type) { case this.correct: 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..de8d4523354b49e75560fa72208c99d5c2260e6b --- /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/services/http/comment.service.ts b/src/app/services/http/comment.service.ts index a282312f83c9ea33d4fd7dda7c28a0db004a71f4..a5a1a0aeff10c0a40c0323305f19d6fc504c6689 100644 --- a/src/app/services/http/comment.service.ts +++ b/src/app/services/http/comment.service.ts @@ -72,7 +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 => this.parseUserNumber(comment)), + map(comment => this.parseComment(comment)), tap(_ => ''), catchError(this.handleError<Comment>('getComment')) ); @@ -87,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> { @@ -110,16 +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; - 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', [])) ); @@ -132,7 +123,7 @@ export class CommentService extends BaseHttpService { externalFilters: {} }, httpOptions).pipe( map(commentList => { - return commentList.map(comment => this.parseUserNumber(comment)); + return commentList.map(comment => this.parseComment(comment)); }), tap(_ => ''), catchError(this.handleError<Comment[]>('getComments', [])) @@ -146,7 +137,7 @@ export class CommentService extends BaseHttpService { externalFilters: {} }, httpOptions).pipe( map(commentList => { - return commentList.map(comment => this.parseUserNumber(comment)); + return commentList.map(comment => this.parseComment(comment)); }), tap(_ => ''), catchError(this.handleError<Comment[]>('getComments', [])) @@ -233,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 fdecfe2f88e79ef25650ac1890c6028c3dfa424e..683739b340167b4360bdbfd2fb9e23fd0918cd67 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -162,7 +162,7 @@ export class TopicCloudAdminService { } getDefaultSpacyTagsDE(): string[] { - let tags: string[]; + const tags: string[] = []; spacyLabels.de.forEach(label => { tags.push(label.tag); }); @@ -170,7 +170,7 @@ export class TopicCloudAdminService { } getDefaultSpacyTagsEN(): string[] { - let tags: string[]; + const tags: string[] = []; spacyLabels.en.forEach(label => { tags.push(label.tag); }); diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index c22c7c8276b6f5416f6ab3cdb221d6a2aac2a291..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", @@ -284,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 07df33bb3518eb767082a48c8e43a716f01e4b61..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",