Skip to content
Snippets Groups Projects
Forked from an inaccessible project.
comment-list.component.ts 21.24 KiB
import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Comment } from '../../../models/comment';
import { CommentService } from '../../../services/http/comment.service';
import { TranslateService } from '@ngx-translate/core';
import { LanguageService } from '../../../services/util/language.service';
import { MatDialog } from '@angular/material/dialog';
import { User } from '../../../models/user';
import { Vote } from '../../../models/vote';
import { UserRole } from '../../../models/user-roles.enum';
import { Room } from '../../../models/room';
import { RoomService } from '../../../services/http/room.service';
import { VoteService } from '../../../services/http/vote.service';
import { NotificationService } from '../../../services/util/notification.service';
import { CorrectWrong } from '../../../models/correct-wrong.enum';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { EventService } from '../../../services/util/event.service';
import { Subscription } from 'rxjs';
import { AppComponent } from '../../../app.component';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthenticationService } from '../../../services/http/authentication.service';
import { TitleService } from '../../../services/util/title.service';
import { ModeratorsComponent } from '../../creator/_dialogs/moderators/moderators.component';
import { TagsComponent } from '../../creator/_dialogs/tags/tags.component';
import { DeleteCommentsComponent } from '../../creator/_dialogs/delete-comments/delete-comments.component';
import { Export } from '../../../models/export';
import { BonusTokenService } from '../../../services/http/bonus-token.service';
import { ModeratorService } from '../../../services/http/moderator.service';
import { CommentFilter, Period } from '../../../utils/filter-options';
import { CreateCommentWrapper } from '../../../utils/CreateCommentWrapper';
import { TopicCloudAdminService } from '../../../services/util/topic-cloud-admin.service';
import { RoomDataService } from '../../../services/util/room-data.service';
import { WsRoomService } from '../../../services/websockets/ws-room.service';

export interface CommentListData {
  comments: Comment[];
  currentFilter: CommentFilter;
  room: Room;
}

@Component({
  selector: 'app-comment-list',
  templateUrl: './comment-list.component.html',
  styleUrls: ['./comment-list.component.scss'],
})
export class CommentListComponent implements OnInit, OnDestroy {
  @ViewChild('searchBox') searchField: ElementRef;
  @Input() user: User;
  @Input() roomId: string;
  shortId: string;
  AppComponent = AppComponent;
  comments: Comment[] = [];
  commentsFilteredByTime: Comment[] = [];
  room: Room;
  hideCommentsList = false;
  filteredComments: Comment[];
  userRole: UserRole;
  deviceType: string;
  isSafari: string;
  isLoading = true;
  voteasc = 'voteasc';
  votedesc = 'votedesc';
  time = 'time';
  currentSort: string;
  read = 'read';
  unread = 'unread';
  favorite = 'favorite';
  correct = 'correct';
  wrong = 'wrong';
  ack = 'ack';
  bookmark = 'bookmark';
  moderator = 'moderator';
  lecturer = 'lecturer';
  tag = 'tag';
  selectedTag = '';
  userNumber = 'userNumber';
  keyword = 'keyword';
  selectedKeyword = '';
  answer = 'answer';
  unanswered = 'unanswered';
  owner = 'owner';
  currentFilter = '';
  currentFilterCompare: any = null;
  commentVoteMap = new Map<string, Vote>();
  scroll = false;
  scrollExtended = false;
  searchInput = '';
  search = false;
  searchPlaceholder = '';
  moderationEnabled = true;
  directSend = true;
  thresholdEnabled = false;
  newestComment: string;
  freeze = false;
  commentStream: Subscription;
  periodsList = Object.values(Period);
  headerInterface = null;
  period: Period = Period.twoWeeks;
  fromNow: number;
  moderatorIds: string[];
  commentsEnabled: boolean;
  userNumberSelection: number = 0;
  createCommentWrapper: CreateCommentWrapper = null;
  private _subscriptionEventServiceTagConfig = null;
  private _subscriptionEventServiceRoomData = null;
  private _subscriptionRoomService = null;

  constructor(
    private commentService: CommentService,
    private translateService: TranslateService,
    public dialog: MatDialog,
    protected langService: LanguageService,
    protected roomService: RoomService,
    protected voteService: VoteService,
    private authenticationService: AuthenticationService,
    private notificationService: NotificationService,
    public eventService: EventService,
    public liveAnnouncer: LiveAnnouncer,
    private route: ActivatedRoute,
    private router: Router,
    private titleService: TitleService,
    private translationService: TranslateService,
    private bonusTokenService: BonusTokenService,
    private moderatorService: ModeratorService,
    private topicCloudAdminService: TopicCloudAdminService,
    private roomDataService: RoomDataService,
    private wsRoomService: WsRoomService
  ) {
    langService.langEmitter.subscribe(lang => {
      translateService.use(lang);
      this.translateService.get('comment-list.search').subscribe(msg => {
        this.searchPlaceholder = msg;
      });
    });
  }

  initNavigation() {
    this._subscriptionEventServiceTagConfig = this.eventService.on<string>('setTagConfig').subscribe(tag => {
      this.setTimePeriod(Period.all);
      this.clickedOnKeyword(tag);
    });
    this._subscriptionEventServiceRoomData = this.eventService.on<string>('pushCurrentRoomData').subscribe(_ => {
      this.eventService.broadcast('currentRoomData', {
        currentFilter: this.getCurrentFilter(),
        comments: this.comments,
        room: this.room
      } as CommentListData);
    });
    const navigation = {};
    const nav = (b, c) => navigation[b] = c;
    nav('createQuestion', () => this.createCommentWrapper.openCreateDialog(this.user));
    nav('moderator', () => {
      const dialogRef = this.dialog.open(ModeratorsComponent, {
        width: '400px',
      });
      dialogRef.componentInstance.roomId = this.room.id;
    });
    nav('tags', () => {
      const updRoom = JSON.parse(JSON.stringify(this.room));
      const dialogRef = this.dialog.open(TagsComponent, {
        width: '400px',
      });
      let tags = [];
      if (this.room.tags !== undefined) {
        tags = this.room.tags;
      }
      dialogRef.componentInstance.tags = tags;
      dialogRef.afterClosed()
        .subscribe(result => {
          if (!result || result === 'abort') {
            return;
          } else {
            updRoom.tags = result;
            this.roomService.updateRoom(updRoom)
              .subscribe((room) => {
                  this.room = room;
                  this.translateService.get('room-page.changes-successful').subscribe(msg => {
                    this.notificationService.show(msg);
                  });
                },
                error => {
                  this.translateService.get('room-page.changes-gone-wrong').subscribe(msg => {
                    this.notificationService.show(msg);
                  });
                });
          }
        });
    });
    nav('deleteQuestions', () => {
      const dialogRef = this.dialog.open(DeleteCommentsComponent, {
        width: '400px',
      });
      dialogRef.componentInstance.roomId = this.roomId;
      dialogRef.afterClosed()
        .subscribe(result => {
          if (result === 'delete') {
            this.translationService.get('room-page.comments-deleted').subscribe(msg => {
              this.notificationService.show(msg);
            });
            this.commentService.deleteCommentsByRoomId(this.roomId).subscribe();
          }
        });
    });
    nav('exportQuestions', () => {
      const exp: Export = new Export(
        this.room,
        this.commentService,
        this.bonusTokenService,
        this.translationService,
        'comment-list',
        this.notificationService);
      exp.exportAsCsv();
    });
    this.headerInterface = this.eventService.on<string>('navigate').subscribe(e => {
      if (navigation.hasOwnProperty(e)) {
        navigation[e]();
      }
    });
  }

  ngOnInit() {
    this.initNavigation();
    this.authenticationService.watchUser.subscribe(newUser => {
      if (newUser) {
        this.user = newUser;
        if (this.userRole === 0) {
          this.voteService.getByRoomIdAndUserID(this.roomId, this.user.id).subscribe(votes => {
            for (const v of votes) {
              this.commentVoteMap.set(v.commentId, v);
            }
          });
        }
      }
    });
    this.userRole = this.route.snapshot.data.roles[0];
    this.route.params.subscribe(params => {
      this.shortId = params['shortId'];
      this.authenticationService.checkAccess(this.shortId);
      this.authenticationService.guestLogin(UserRole.PARTICIPANT).subscribe(r => {
        this.roomService.getRoomByShortId(this.shortId).subscribe(room => {
          this.room = room;
          this.roomId = room.id;
          this._subscriptionRoomService = this.wsRoomService.getRoomStream(this.roomId).subscribe(msg => {
            const message = JSON.parse(msg.body);
            if (message.type === 'RoomPatched') {
              this.room = message.payload.changes;
              this.roomId = this.room.id;
              this.moderationEnabled = this.room.moderated;
              this.directSend = this.room.directSend;
              this.commentsEnabled = (this.userRole > 0) || !this.room.questionsBlocked;
            }
          });
          this.moderationEnabled = this.room.moderated;
          this.directSend = this.room.directSend;
          this.commentsEnabled = (this.userRole > 0) || !this.room.questionsBlocked;
          this.createCommentWrapper = new CreateCommentWrapper(this.translateService,
            this.notificationService, this.commentService, this.dialog, this.room);
          localStorage.setItem('moderationEnabled', JSON.stringify(this.moderationEnabled));
          if (!this.authenticationService.hasAccess(this.shortId, UserRole.PARTICIPANT)) {
            this.roomService.addToHistory(this.room.id);
            this.authenticationService.setAccess(this.shortId, UserRole.PARTICIPANT);
          }
          this.moderatorService.get(this.roomId).subscribe(list => {
            this.moderatorIds = list.map(m => m.accountId);
            this.moderatorIds.push(this.room.ownerId);

            this.roomDataService.getRoomData(this.room.id).subscribe(comments => {
              if (comments === null) {
                return;
              }
              this.comments = comments;
              this.getComments();
              this.eventService.broadcast('commentListCreated', null);
            });
            this.subscribeCommentStream();
          });
          /**
           if (this.userRole === UserRole.PARTICIPANT) {
            this.openCreateDialog();
          }
           */
        });
      });
    });
    this.currentSort = this.votedesc;
    this.hideCommentsList = false;
    this.translateService.use(localStorage.getItem('currentLang'));
    this.deviceType = localStorage.getItem('deviceType');
    this.isSafari = localStorage.getItem('isSafari');
    this.translateService.get('comment-list.search').subscribe(msg => {
      this.searchPlaceholder = msg;
    });
  }

  ngOnDestroy() {
    if (!this.freeze && this.commentStream) {
      this.commentStream.unsubscribe();
    }
    if (this._subscriptionRoomService) {
      this._subscriptionRoomService.unsubscribe();
    }
    this.titleService.resetTitle();
    if (this.headerInterface) {
      this.headerInterface.unsubscribe();
    }
    if (this._subscriptionEventServiceRoomData) {
      this._subscriptionEventServiceRoomData.unsubscribe();
    }
    if (this._subscriptionEventServiceTagConfig) {
      this._subscriptionEventServiceTagConfig.unsubscribe();
    }
  }

  checkScroll(): void {
    const currentScroll = document.documentElement.scrollTop;
    this.scroll = currentScroll >= 65;
    this.scrollExtended = currentScroll >= 300;
  }

  isScrollButtonVisible(): boolean {
    return !AppComponent.isScrolledTop() && this.comments.length > 10;
  }

  searchComments(): void {
    this.search = true;
    if (this.searchInput) {
      if (this.searchInput.length > 1) {
        this.hideCommentsList = true;
        this.filteredComments = this.comments
          .filter(c => this.checkIfIncludesKeyWord(c.body, this.searchInput)
            || (!!c.answer ? this.checkIfIncludesKeyWord(c.answer, this.searchInput) : false));
      }
    } else if (this.searchInput.length === 0 && this.currentFilter === '') {
      this.hideCommentsList = false;
    }
  }

  checkIfIncludesKeyWord(body: string, keyword: string) {
    return body.toLowerCase().includes(keyword.toLowerCase());
  }

  activateSearch() {
    this.search = true;
    this.searchField.nativeElement.focus();
  }

  getComments(): void {
    if (this.room.threshold) {
      this.thresholdEnabled = true;
    } else {
      this.thresholdEnabled = false;
    }
    this.isLoading = false;
    let commentThreshold;
    if (this.thresholdEnabled) {
      commentThreshold = this.room.threshold;
      if (this.hideCommentsList) {
        this.filteredComments = this.filteredComments.filter(x => x.score >= commentThreshold);
      } else {
        this.setComments(this.comments.filter(x => x.score >= commentThreshold));
      }
    }
    this.setTimePeriod();
  }

  getVote(comment: Comment): Vote {
    if (this.userRole === 0) {
      return this.commentVoteMap.get(comment.id);
    }
  }

  closeDialog() {
    this.dialog.closeAll();
  }

  filterComments(type: string, compare?: any): void {
    this.currentFilter = type;
    this.currentFilterCompare = compare;
    if (type === '') {
      this.filteredComments = this.commentsFilteredByTime;
      this.hideCommentsList = false;
      this.currentFilter = '';
      this.selectedTag = '';
      this.selectedKeyword = '';
      this.userNumberSelection = 0;
      this.sortComments(this.currentSort);
      return;
    }
    this.filteredComments = this.commentsFilteredByTime.filter(c => {
      switch (type) {
        case this.correct:
          return c.correct === CorrectWrong.CORRECT ? 1 : 0;
        case this.wrong:
          return c.correct === CorrectWrong.WRONG ? 1 : 0;
        case this.favorite:
          return c.favorite;
        case this.bookmark:
          return c.bookmark;
        case this.read:
          return c.read;
        case this.unread:
          return !c.read;
        case this.tag:
          this.selectedTag = compare;
          return c.tag === compare;
        case this.userNumber:
          return c.userNumber === compare;
        case this.keyword:
          this.selectedKeyword = compare;
          const isInQuestioner = c.keywordsFromQuestioner ? c.keywordsFromQuestioner.findIndex(k => k.lemma === compare) >= 0 : false;
          const isInSpacy = c.keywordsFromSpacy ? c.keywordsFromSpacy.findIndex(k => k.lemma === compare) >= 0 : false;
          return isInQuestioner || isInSpacy;
        case this.answer:
          return c.answer;
        case this.unanswered:
          return !c.answer;
        case this.owner:
          return c.creatorId === this.user.id;
        case this.moderator:
          return c.creatorId === this.user.id && (this.user.role === 2 || this.user.role === 1);
        case this.lecturer:
          return c.creatorId === this.user.id && this.user.role === 3;
      }
    });
    this.hideCommentsList = true;
    this.sortComments(this.currentSort);
  }

  sort(array: any[], type: string): any[] {
    const sortedArray = array.sort((a, b) => {
      if (type === this.voteasc) {
        return (a.score > b.score) ? 1 : (b.score > a.score) ? -1 : 0;
      } else if (type === this.votedesc) {
        return (b.score > a.score) ? 1 : (a.score > b.score) ? -1 : 0;
      } else if (type === this.time) {
        const dateA = new Date(a.timestamp);
        const dateB = new Date(b.timestamp);
        return (+dateB > +dateA) ? 1 : (+dateA > +dateB) ? -1 : 0;
      }
    });
    return sortedArray.sort((a, b) => this.isCreatedByModeratorOrCreator(a) ? -1 : this.isCreatedByModeratorOrCreator(b) ? 1 : 0);
  }

  isCreatedByModeratorOrCreator(comment: Comment): boolean {
    return this.moderatorIds.indexOf(comment.creatorId) > -1;
  }

  sortComments(type: string): void {
    if (this.hideCommentsList === true) {
      this.filteredComments = this.sort(this.filteredComments, type);
    } else {
      this.setComments(this.sort(this.commentsFilteredByTime, type));
    }
    this.currentSort = type;
  }

  clickedOnTag(tag: string): void {
    this.filterComments(this.tag, tag);
  }

  clickedOnKeyword(keyword: string): void {
    this.filterComments(this.keyword, keyword);
  }

  clickedUserNumber(usrNumber: number): void {
    this.userNumberSelection = usrNumber;
    this.filterComments(this.userNumber, usrNumber);
  }

  pauseCommentStream() {
    this.freeze = true;
    this.roomDataService.getRoomData(this.roomId, true).subscribe(comments => {
      if (comments === null) {
        return;
      }
      this.comments = comments;
      this.setComments(comments);
      this.getComments();
    });
    this.commentStream.unsubscribe();
    this.translateService.get('comment-list.comment-stream-stopped').subscribe(msg => {
      this.notificationService.show(msg);
    });
  }

  playCommentStream() {
    this.freeze = false;
    this.roomDataService.getRoomData(this.roomId).subscribe(comments => {
      if (comments === null) {
        return;
      }
      this.comments = comments;
      this.setComments(comments);
      this.getComments();
    });
    this.subscribeCommentStream();
    this.translateService.get('comment-list.comment-stream-started').subscribe(msg => {
      this.notificationService.show(msg);
    });
  }

  subscribeCommentStream() {
    this.commentStream = this.roomDataService.receiveUpdates([
      { type: 'CommentCreated', finished: true },
      { type: 'CommentPatched', subtype: this.favorite },
      { type: 'CommentPatched', subtype: 'score' },
      { finished: true }
    ]).subscribe(update => {
      if (update.type === 'CommentCreated') {
        this.announceNewComment(update.comment.body);
        this.setComments(this.comments);
      } else if (update.type === 'CommentPatched') {
        if (update.subtype === 'score') {
          this.getComments();
        } else if (update.subtype === this.favorite) {
          if (this.user.id === update.comment.creatorId && update.comment.favorite) {
            this.translateService.get('comment-list.comment-got-favorited').subscribe(ret => {
              this.notificationService.show(ret);
            });
          }
        }
      }
      if (update.finished) {
        this.setTimePeriod();
        if (this.hideCommentsList) {
          this.searchComments();
        }
      }
    });
  }

  switchToModerationList(): void {
    this.router.navigate([`/moderator/room/${this.room.shortId}/moderator/comments`]);
  }

  setComments(comments: Comment[]) {
    this.commentsFilteredByTime = comments;
    this.titleService.attachTitle('(' + this.commentsFilteredByTime.length + ')');
  }

  /**
   * Announces a new comment receive.
   */
  public announceNewComment(comment: string) {
    // update variable so text will be fetched to DOM
    this.newestComment = comment;

    // Currently the only possible way to announce the new comment text
    // @see https://github.com/angular/angular/issues/11405
    setTimeout(() => {
      const newCommentText: string = document.getElementById('new-comment').innerText;

      // current live announcer content must be cleared before next read
      this.liveAnnouncer.clear();

      this.liveAnnouncer.announce(newCommentText).catch(err => { /* TODO error handling */
      });
    }, 450);
  }

  public setTimePeriod(period?: Period) {
    if (period) {
      this.period = period;
      this.fromNow = null;
    }
    const currentTime = new Date();
    const hourInSeconds = 3600000;
    let periodInSeconds;
    if (this.period !== Period.all) {
      switch (this.period) {
        case Period.fromNow:
          if (!this.fromNow) {
            this.fromNow = new Date().getTime();
          }
          break;
        case Period.oneHour:
          periodInSeconds = hourInSeconds;
          break;
        case Period.threeHours:
          periodInSeconds = hourInSeconds * 2;
          break;
        case Period.oneDay:
          periodInSeconds = hourInSeconds * 24;
          break;
        case Period.oneWeek:
          periodInSeconds = hourInSeconds * 168;
          break;
        case Period.twoWeeks:
          periodInSeconds = hourInSeconds * 336;
          break;
      }
      this.commentsFilteredByTime = this.comments
        .filter(c => new Date(c.timestamp).getTime() >=
          (this.period === Period.fromNow ? this.fromNow : (currentTime.getTime() - periodInSeconds)));
    } else {
      this.commentsFilteredByTime = this.comments;
    }

    this.filterComments(this.currentFilter, this.currentFilterCompare);
    this.titleService.attachTitle('(' + this.commentsFilteredByTime.length + ')');
  }

  private getCurrentFilter(): CommentFilter {
    const filter = new CommentFilter();
    filter.filterSelected = this.currentFilter;
    filter.paused = this.freeze;
    filter.periodSet = this.period;
    filter.keywordSelected = this.selectedKeyword;
    filter.tagSelected = this.selectedTag;
    filter.userNumberSelected = this.userNumberSelection;

    if (filter.periodSet === Period.fromNow) {
      filter.timeStampNow = new Date().getTime();
    }

    return filter;
  }
}