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 { 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('chatMessagesBuffer', { static: false }) chatMessagesBuffer: ElementRef; @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; readToHereEvent: Info; swapped = false; hidden = 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.baseEventSeq === 0) { this.initRoomLastEventSeq = eventList[eventList.length - 1].seq; } if ( !!eventList && eventList.length > 0 && this.baseEventSeq > 0 && !!this.roomInfo && !!this.roomInfo.lastReadEventSeq && this.baseEventSeq <= this.roomInfo.lastReadEventSeq ) { // 기존 대화 내용이 있는 상태에서 추가로 조회된 내용중에 read here 가 있을 경우. this.firstCheckReadHere = false; } this.eventList = eventList; this.readToHereEvent = this.getReadHere(); 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(): Info | undefined { 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) { return this.eventList.find( v => v.seq === this.roomInfo.lastReadEventSeq + 1 ); } } } return undefined; } 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 ]; } swapScroll( to: Info, preCallback: () => void, postCallback: () => void, useHide: boolean, useSwap: boolean ) { this.preSwapScroll(useHide, useSwap); if (!!preCallback) { preCallback(); } this.virtualScroller.scrollInto(to, true, 0, 0, () => { setTimeout(() => { if (!!postCallback) { postCallback(); } this.postSwapScroll(useHide, useSwap); }, 100); }); } preSwapScroll(useHide: boolean, useSwap: boolean) { if (useSwap && !this.swapped) { this.chatMessagesBuffer.nativeElement.innerHTML = this.chatMessagesContainer.nativeElement.innerHTML; this.chatMessagesBuffer.nativeElement.scrollTop = this.chatMessagesContainer.nativeElement.scrollTop; this.chatMessagesBuffer.nativeElement.classList.remove('disappear'); this.swapped = true; } if (useHide && !this.hidden) { this.chatMessagesContainer.nativeElement.classList.add('hide'); this.hidden = true; } } postSwapScroll(useHide: boolean, useSwap: boolean) { if (useSwap && this.swapped) { this.chatMessagesBuffer.nativeElement.innerHTML = ''; this.chatMessagesBuffer.nativeElement.scrollTop = 0; this.chatMessagesBuffer.nativeElement.classList.add('disappear'); this.swapped = false; } if (useHide && this.hidden) { this.chatMessagesContainer.nativeElement.classList.remove('hide'); this.hidden = false; } } ready(): void { this.scrollToBottom(); } scrollToBottom(speed?: number): void { if (!!this.storedScrollItem) { if (!!this.readToHereEvent && this.firstCheckReadHere) { this.swapScroll( this.readToHereEvent, () => {}, () => { this.firstCheckReadHere = false; }, true, true ); } else { this.swapScroll( this.storedScrollItem, () => {}, () => { this.storedScrollItem = undefined; }, true, true ); } } 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.readToHereEvent && this.firstCheckReadHere) { this.swapScroll( this.readToHereEvent, () => {}, () => { this.firstCheckReadHere = false; }, true, true ); } else { if ( this.virtualScroller.viewPortInfo.endIndex === this.eventList.length - 2 ) { this.swapScroll( this.eventList[this.eventList.length - 1], () => {}, () => {}, false, false ); } else { this.swapScroll( this.eventList[this.eventList.length - 1], () => {}, () => {}, true, false ); } } } } initEventMore() { // 방정보가 바뀌면 이전대화 보기 관련 값들을 초기화 한다. this.scrollUpInitalized = false; this.storedScrollItem = undefined; } clear() {} gotoPosition(eventSeq: number) { if (!!this.virtualScroller) { const e = this.eventList.find(v => v.seq === eventSeq); this.virtualScroller.scrollInto(e, true, 0, 0, () => {}); } } onClickOpenProfile(userSeq: number) { this.openProfile.emit(userSeq); } onClickMore(event: any) { event.preventDefault(); event.stopPropagation(); if (this.scrollUpInitalized && this.eventRemained) { this.storeScrollPosition(); this.preSwapScroll(true, true); 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; } }