import { Component, OnInit, Input, EventEmitter, Output, ViewChild, OnDestroy, ChangeDetectionStrategy, ElementRef, ChangeDetectorRef } from '@angular/core'; import { Info, EventType, InfoResponse, EventJson, FileEventJson, MassTranslationEventJson } from '@ucap-webmessenger/protocol-event'; import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; import { UserInfo, RoomInfo, RoomType } from '@ucap-webmessenger/protocol-room'; import { NGXLogger } from 'ngx-logger'; import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; import { DatePipe } from '@angular/common'; import moment from 'moment'; import { FileDownloadItem } from '@ucap-webmessenger/api'; import { TranslateService } from '@ngx-translate/core'; import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; import { Observable, Subscription } from 'rxjs'; import { VirtualScrollerComponent, IPageInfo } from 'ngx-virtual-scroller'; @Component({ selector: 'ucap-chat-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class MessagesComponent implements OnInit, OnDestroy { @Input() loginRes$: Observable; @Input() roomInfo$: Observable; @Input() eventList$: Observable[]>; @Input() newEventList$: Observable[]>; @Input() searchedList$: Observable[]>; @Input() eventInfoStatus$: Observable; @Input() eventRemained$: Observable; @Input() sessionVerInfo: VersionInfo2Response; @Input() userInfos$: Observable; @Input() isShowUnreadCount = true; @Input() clearReadHere: boolean; @Input() minShowReadHere = 10; @Input() translationSimpleview = false; @Input() searchingMode = false; @Output() openProfile = new EventEmitter(); @Output() moreEvent = new EventEmitter(); @Output() massDetail = new EventEmitter(); @Output() massTranslationDetail = new EventEmitter<{ message: Info; contentsType: string; }>(); @Output() fileViewer = new EventEmitter(); @Output() save = new EventEmitter<{ fileInfo: FileEventJson; fileDownloadItem: FileDownloadItem; type: string; }>(); @Output() contextMenu = new EventEmitter<{ event: MouseEvent; message: Info; type?: string; }>(); @Output() scrollUp = new EventEmitter(); @Output() yReachEnd = new EventEmitter(); @Output() yReachStart = new EventEmitter(); @Output() existNewMessage = new EventEmitter>(); @ViewChild('chatMessagesContainer', { static: false }) chatMessagesContainer: ElementRef; @ViewChild(PerfectScrollbarDirective, { static: false }) psChatContent: PerfectScrollbarDirective; @ViewChild(VirtualScrollerComponent, { static: false }) private virtualScroller: VirtualScrollerComponent; storedScrollItem: Info; // 이전대화를 불러올 경우 현재 스크롤 포지션 유지를 위한 값. 0 이면 초기로딩. scrollUpInitalized = false; // ps 에서 초기 로딩시 scroll reach start 이벤트 발생 버그를 우회하기 위한 init 값으로 scrollUp 에 의해 true 로 된다. firstCheckReadHere = true; initRoomLastEventSeq: number; baseEventSeq = 0; loginRes: LoginResponse; loginResSubscription: Subscription; roomInfo: RoomInfo; roomInfoSubscription: Subscription; eventList: Info[]; eventListSubscription: Subscription; newEventList: Info[]; newEventListSubscription: Subscription; searchedList: Info[]; searchedListSubscription: Subscription; eventInfoStatus: InfoResponse; eventInfoStatusSubscription: Subscription; eventRemained: boolean; eventRemainedSubscription: Subscription; userInfos: UserInfo[]; userInfosSubscription: Subscription; EventType = EventType; profileImageRoot: string; moment = moment; existReadHere = false; fixScreen = false; constructor( private logger: NGXLogger, private datePipe: DatePipe, private changeDetectorRef: ChangeDetectorRef, private translateService: TranslateService ) {} ngOnInit() { this.profileImageRoot = this.profileImageRoot || this.sessionVerInfo.profileRoot; this.loginResSubscription = this.loginRes$.subscribe(loginRes => { this.loginRes = loginRes; }); this.roomInfoSubscription = this.roomInfo$.subscribe(roomInfo => { // this.eventList = undefined; // this.newEventList = undefined; // this.searchedList = undefined; // this.eventInfoStatus = undefined; // this.eventRemained = undefined; // this.userInfos = undefined; this.initEventMore(); this.roomInfo = roomInfo; }); this.eventListSubscription = this.eventList$.subscribe(eventList => { if ( !!eventList && eventList.length > 0 && !!this.roomInfo && !!this.roomInfo.lastReadEventSeq && this.baseEventSeq <= this.roomInfo.lastReadEventSeq ) { // 조회된 내용중에 read here 가 있을 경우. this.firstCheckReadHere = false; } this.eventList = eventList; this.changeDetectorRef.detectChanges(); if (this.searchingMode) { const baseseq = this.baseEventSeq; // setTimeout(() => { // this.doSearchTextInEvent(this.searchText, baseseq); // }, 800); this.baseEventSeq = eventList[0].seq; } else { if (!!eventList && eventList.length > 0) { this.baseEventSeq = eventList[0].seq; this.ready(); } } }); this.newEventListSubscription = this.newEventList$.subscribe( newEventList => { this.newEventList = newEventList; } ); this.searchedListSubscription = this.searchedList$.subscribe( searchedList => { this.searchedList = searchedList; } ); this.eventInfoStatusSubscription = this.eventInfoStatus$.subscribe( eventInfoStatus => { this.eventInfoStatus = eventInfoStatus; } ); this.eventRemainedSubscription = this.eventRemained$.subscribe( eventRemained => { this.eventRemained = eventRemained; } ); this.userInfosSubscription = this.userInfos$.subscribe(userInfos => { this.userInfos = userInfos; }); } ngOnDestroy(): void { if (!!this.loginResSubscription) { this.loginResSubscription.unsubscribe(); } if (!!this.roomInfoSubscription) { this.roomInfoSubscription.unsubscribe(); } if (!!this.eventListSubscription) { this.eventListSubscription.unsubscribe(); } if (!!this.newEventListSubscription) { this.newEventListSubscription.unsubscribe(); } if (!!this.searchedListSubscription) { this.searchedListSubscription.unsubscribe(); } if (!!this.eventInfoStatusSubscription) { this.eventInfoStatusSubscription.unsubscribe(); } if (!!this.eventRemainedSubscription) { this.eventRemainedSubscription.unsubscribe(); } if (!!this.userInfosSubscription) { this.userInfosSubscription.unsubscribe(); } } /** * UserInfo getter */ getUserName(seq: number): string { if (!this.userInfos) { return ''; } const userInfo: UserInfo[] = this.userInfos.filter( user => user.seq === seq ); if (!!userInfo && userInfo.length > 0) { return userInfo[0].name; } return '(알수없는 사용자)'; } getUserProfile(seq: number): string { if (!this.userInfos) { return ''; } const userInfo: UserInfo[] = this.userInfos.filter( user => user.seq === seq ); if (!!userInfo && userInfo.length > 0) { return userInfo[0].profileImageFile; } return ''; } isHighlightedEvent(seq: number): boolean { return ( !!this.searchedList && this.searchedList.filter(event => event.seq === seq).length > 0 ); } getUnreadCount(message: Info): string | number { // if (!this.userInfos || 0 === this.userInfos.length) { // return ''; // } const unreadCnt = this.userInfos.filter(user => { if (message.senderSeq === user.seq) { // 본인 글은 unreadCount 에 포함하지 않는다. return false; } return user.lastReadEventSeq < message.seq; }).length; return unreadCnt === 0 ? '' : unreadCnt; } /** * 정보성 Event 인지 판단. * @description 정보성 event 일 경우 프로필, 일시 를 표현하지 않는다. * Edit with reducers.ts / sync / updateRoomForNewEventMessage */ getIsInformation(info: Info) { if ( info.type === EventType.Join || info.type === EventType.Exit || info.type === EventType.ForcedExit || info.type === EventType.RenameRoom || info.type === EventType.NotificationForTimerRoom || info.type === EventType.GuideForRoomTimerChanged ) { return true; } return false; } /** Date Splitter show check */ getDateSplitter(message: Info): boolean { const curIndex = this.eventList.findIndex(v => v.seq === message.seq); if (curIndex === 0) { return true; } if (curIndex > 0) { if (!this.eventList[curIndex]) { return false; } return !moment(this.eventList[curIndex].sendDate).isSame( moment(this.eventList[curIndex - 1].sendDate), 'day' ); } return false; } getReadHere(message: Info): boolean { if ( !!this.roomInfo && !!this.roomInfo.lastReadEventSeq && this.initRoomLastEventSeq - this.roomInfo.lastReadEventSeq > this.minShowReadHere ) { if ( this.roomInfo.roomType === RoomType.Single || this.roomInfo.roomType === RoomType.Multi ) { if (!this.roomInfo.isTimeRoom) { const messageIndex = this.eventList.findIndex( v => v.seq === message.seq ); if ( this.eventList[messageIndex].seq === this.roomInfo.lastReadEventSeq + 1 ) { this.existReadHere = true; return true; } } } } return false; } getStringReadHereMore(): string { let rtnStr = ''; rtnStr = this.translateService.instant('chat.event.moreUnreadEventsWith', { countOfUnread: this.baseEventSeq - (this.roomInfo.lastReadEventSeq + 1) }); return rtnStr; } storeScrollPosition() { this.storedScrollItem = this.eventList[ this.virtualScroller.viewPortInfo.startIndex ]; } scrollToStoredItem() { if (!this.scrollUpInitalized) { this.chatMessagesContainer.nativeElement.classList.remove('hide'); } this.virtualScroller.scrollInto(this.storedScrollItem, true, 0, 0, () => { this.storedScrollItem = undefined; }); } ready(): void { if (!this.scrollUpInitalized) { this.chatMessagesContainer.nativeElement.classList.add('hide'); } setTimeout(() => { this.scrollToBottom(); }); } scrollToBottom(speed?: number): void { if (!!this.storedScrollItem) { // if (this.psChatContent) { // this.psChatContent.update(); const element = document.getElementById('message-box-readhere'); if (!!element && this.firstCheckReadHere) { setTimeout(() => { this.psChatContent.scrollToTop(element.offsetTop - 200, speed); }); this.firstCheckReadHere = false; } else { this.scrollToStoredItem(); } // } } else if (this.scrollUpInitalized) { if (!!this.newEventList && this.newEventList.length > 0) { this.existNewMessage.emit( this.newEventList[this.newEventList.length - 1] ); } } else { speed = speed || 0; if (this.psChatContent) { this.psChatContent.update(); const element = document.getElementById('message-box-readhere'); if (!!element && this.firstCheckReadHere) { setTimeout(() => { this.psChatContent.scrollToTop(element.offsetTop - 200, speed); }); this.firstCheckReadHere = false; } else { this.virtualScroller.scrollToIndex( this.eventList.length - 1, true, 0, 0, () => { this.chatMessagesContainer.nativeElement.classList.remove('hide'); } ); } } } } initEventMore() { // 방정보가 바뀌면 이전대화 보기 관련 값들을 초기화 한다. this.scrollUpInitalized = false; this.storedScrollItem = undefined; } clear() {} gotoPosition(eventSeq: number) { // if (this.psChatContent) { // this.psChatContent.update(); // const element = document.getElementById(eventSeq.toString()); // if (!!element) { // setTimeout(() => { // this.psChatContent.scrollToTop(element.offsetTop - 200); // }); // } // } if (!!this.virtualScroller) { const e = this.eventList.find(v => v.seq === eventSeq); this.fixScreen = true; this.virtualScroller.scrollInto(e, false, undefined, 0, () => { this.fixScreen = false; }); } } onClickOpenProfile(userSeq: number) { this.openProfile.emit(userSeq); } onClickMore(event: any) { event.preventDefault(); event.stopPropagation(); if (this.scrollUpInitalized && this.eventRemained) { // this.storedScrollItem = this.psChatContent.elementRef.nativeElement.scrollHeight; this.storeScrollPosition(); this.moreEvent.emit(this.eventList[0].seq); this.virtualScroller.invalidateCachedMeasurementAtIndex(0); } } /** [Event] MassTalk Detail View */ onMassDetail(value: number) { this.massDetail.emit(value); } onMassTranslationDetail(params: { message: Info; contentsType: string; }) { this.massTranslationDetail.emit(params); } /** [Event] Image Viewer */ onFileViewer(fileInfo: FileEventJson) { this.fileViewer.emit(fileInfo); } /** [Event] Attach File Save & Save As */ onSave(value: { fileInfo: FileEventJson; fileDownloadItem: FileDownloadItem; type: string; }) { this.save.emit(value); } /** [Event] Context Menu */ onContextMenu(event: { event: MouseEvent; message: Info; type?: string; }) { this.contextMenu.emit(event); } onScrollup(event: any) { if (!this.eventList || 0 === this.eventList.length) { return; } this.scrollUpInitalized = true; this.scrollUp.emit(event); } onYReachStart(event: any) { this.yReachStart.emit(event); } onYReachEnd(event: any) { this.yReachEnd.emit(event); } onVsChange(event: IPageInfo) { if ( -1 === event.startIndex || -1 === event.endIndex || (0 === event.startIndex && 0 === event.endIndex) ) { return; } this.logger.debug('onVsChange', event); } trackByEvent(index: number, info: Info): number { return info.seq; } }