ucap-doc/documents/업무/2월/3째주/file-viewer-prj/messages.component.ts
2020-02-21 09:35:58 +09:00

700 lines
19 KiB
TypeScript

import {
Component,
OnInit,
Input,
EventEmitter,
Output,
ViewChild,
OnDestroy,
ChangeDetectionStrategy,
ElementRef,
ChangeDetectorRef,
ViewChildren,
QueryList
} 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 moment from 'moment';
import { FileDownloadItem } from '@ucap-webmessenger/api';
import { TranslateService } from '@ngx-translate/core';
import { Observable, Subscription, timer } from 'rxjs';
import { VirtualScrollerComponent, IPageInfo } from 'ngx-virtual-scroller';
import { MessageBoxComponent } from './message-box.component';
import { debounce } from 'rxjs/operators';
import { SelectFileInfo } from '@ucap-webmessenger/ui';
@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<LoginResponse>;
@Input()
roomInfo$: Observable<RoomInfo>;
@Input()
eventList$: Observable<Info<EventJson>[]>;
@Input()
newEventList$: Observable<Info<EventJson>[]>;
@Input()
searchedList$: Observable<Info<EventJson>[]>;
@Input()
eventInfoStatus$: Observable<InfoResponse>;
@Input()
eventRemained$: Observable<boolean>;
@Input()
sessionVerInfo: VersionInfo2Response;
@Input()
userInfos$: Observable<UserInfo[]>;
@Input()
lock$: Observable<boolean>;
@Input()
isShowUnreadCount = true;
@Input()
clearReadHere: boolean;
@Input()
minShowReadHere = 10;
@Input()
translationSimpleview = false;
@Input()
searchingMode = false;
@Output()
openProfile = new EventEmitter<number>();
@Output()
moreEvent = new EventEmitter<number>();
@Output()
massDetail = new EventEmitter<number>();
@Output()
massTranslationDetail = new EventEmitter<{
message: Info<MassTranslationEventJson>;
contentsType: string;
}>();
@Output()
fileViewer = new EventEmitter<SelectFileInfo>();
@Output()
save = new EventEmitter<{
fileInfo: FileEventJson;
fileDownloadItem: FileDownloadItem;
type: string;
}>();
@Output()
contextMenu = new EventEmitter<{
event: MouseEvent;
message: Info<EventJson>;
type?: string;
}>();
@Output()
scrollUp = new EventEmitter<any>();
@Output()
yReachEnd = new EventEmitter<any>();
@Output()
yReachStart = new EventEmitter<any>();
@Output()
existNewMessage = new EventEmitter<Info<EventJson>>();
@ViewChild('chatMessagesContainer', { static: false })
chatMessagesContainer: ElementRef<HTMLElement>;
@ViewChild('chatMessagesBuffer', { static: false })
chatMessagesBuffer: ElementRef<HTMLElement>;
@ViewChild('chatMessagesBufferContainer', { static: false })
chatMessagesBufferContainer: ElementRef<HTMLElement>;
@ViewChild(VirtualScrollerComponent, { static: false })
private virtualScroller: VirtualScrollerComponent;
@ViewChildren('chatMessageBox')
chatMessageBoxList: QueryList<MessageBoxComponent>;
storedScrollItem: Info<EventJson>; // 이전대화를 불러올 경우 현재 스크롤 포지션 유지를 위한 값. 0 이면 초기로딩.
storedScrollItemOffsetTop: number | undefined;
scrollUpInitalized = false; // ps 에서 초기 로딩시 scroll reach start 이벤트 발생 버그를 우회하기 위한 init 값으로 scrollUp 에 의해 true 로 된다.
firstCheckReadHere = true;
initRoomLastEventSeq: number;
baseEventSeq = 0;
gotoEventSeq: number;
loginRes: LoginResponse;
loginResSubscription: Subscription;
roomInfo: RoomInfo;
roomInfoSubscription: Subscription;
eventList: Info<EventJson>[];
eventListSubscription: Subscription;
newEventList: Info<EventJson>[];
newEventListSubscription: Subscription;
searchedList: Info<EventJson>[];
searchedListSubscription: Subscription;
eventInfoStatus: InfoResponse;
eventInfoStatusSubscription: Subscription;
eventRemained: boolean;
eventRemainedSubscription: Subscription;
userInfos: UserInfo[];
userInfosSubscription: Subscription;
EventType = EventType;
profileImageRoot: string;
moment = moment;
readToHereEvent: Info<EventJson>;
existReadToHereEvent = true;
hidden = false;
swapped = false;
initalized = false;
constructor(
private logger: NGXLogger,
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.roomInfo = roomInfo;
this.initalized = false;
/** [S] initializing by changed room */
// reset :: roomLastEventSeq
if (!!roomInfo && !!roomInfo.finalEventSeq) {
this.initRoomLastEventSeq = roomInfo.finalEventSeq;
}
// clear :: readToHearEvent object
this.readToHereEvent = undefined;
this.existReadToHereEvent = true;
/** [E] initializing by changed room */
if (!this.roomInfo || this.roomInfo.roomSeq !== roomInfo.roomSeq) {
this.initEventMore();
}
});
this.eventListSubscription = this.eventList$
.pipe(debounce(() => timer(100)))
.subscribe(eventList => {
this.eventList = eventList;
if (!!eventList && eventList.length > 0) {
if (!this.readToHereEvent && this.existReadToHereEvent) {
this.readToHereEvent = this.getReadHere();
}
if (
this.baseEventSeq > 0 &&
!!this.roomInfo &&
!!this.roomInfo.lastReadEventSeq &&
this.baseEventSeq <= this.roomInfo.lastReadEventSeq
) {
// 기존 대화 내용이 있는 상태에서 추가로 조회된 내용중에 read here 가 있을 경우.
this.firstCheckReadHere = false;
}
} else {
this.readToHereEvent = undefined;
}
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<EventJson>): string | number {
// if (!this.userInfos || 0 === this.userInfos.length) {
// return '';
// }
const unreadCnt = this.userInfos
.filter(user => user.isJoinRoom && user.seq !== message.senderSeq)
.filter(user => user.lastReadEventSeq < message.seq).length;
return unreadCnt === 0 ? '' : unreadCnt;
}
/**
* 정보성 Event 인지 판단.
* @description 정보성 event 일 경우 프로필, 일시 를 표현하지 않는다.
* Edit with reducers.ts / sync / updateRoomForNewEventMessage
*/
getIsInformation(info: Info<EventJson>) {
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<EventJson>): 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<EventJson> | 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
);
}
}
} else {
this.existReadToHereEvent = false;
}
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
];
const chatMessageBox = this.chatMessageBoxList.find(
el =>
el.message.seq ===
this.eventList[this.virtualScroller.viewPortInfo.startIndex].seq
);
if (!!chatMessageBox) {
this.storedScrollItemOffsetTop =
chatMessageBox.offsetTop -
this.virtualScroller.viewPortInfo.scrollStartPosition;
} else {
this.storedScrollItemOffsetTop = 0;
}
}
swapScrollTo(
to: Info<EventJson>,
preCallback: () => void,
postCallback: () => void,
useHide: boolean,
useSwap: boolean,
addtionalOffset?: number
) {
this.preSwapScroll(useHide, useSwap);
if (!!preCallback) {
preCallback();
}
this.virtualScroller.scrollInto(
to,
true,
undefined !== this.storedScrollItemOffsetTop
? -this.storedScrollItemOffsetTop
: undefined !== addtionalOffset
? -addtionalOffset
: 0,
0,
() => {
setTimeout(() => {
if (!!postCallback) {
postCallback();
}
this.postSwapScroll(useHide, useSwap);
});
}
);
}
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.chatMessagesBufferContainer.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.chatMessagesBufferContainer.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(): void {
if (!!this.storedScrollItem) {
if (!!this.readToHereEvent && this.firstCheckReadHere) {
this.swapScrollTo(
this.readToHereEvent,
() => {},
() => {
this.firstCheckReadHere = false;
},
true,
true
);
} else {
this.swapScrollTo(
this.storedScrollItem,
() => {},
() => {
this.storedScrollItem = undefined;
this.storedScrollItemOffsetTop = undefined;
},
true,
true
);
}
} else if (this.scrollUpInitalized) {
if (!!this.newEventList && this.newEventList.length > 0) {
this.existNewMessage.emit(
this.newEventList[this.newEventList.length - 1]
);
}
} else {
if (!!this.readToHereEvent && this.firstCheckReadHere) {
this.swapScrollTo(
this.readToHereEvent,
() => {},
() => {
this.firstCheckReadHere = false;
},
false,
false
);
} else {
if (!this.eventList || 0 === this.eventList.length) {
return;
}
if (!!this.gotoEventSeq) {
this.gotoPosition(this.gotoEventSeq);
this.gotoEventSeq = undefined;
return;
}
const lastEvent =
!!this.eventList && 0 < this.eventList.length
? this.eventList[this.eventList.length - 1]
: undefined;
if (undefined === lastEvent) {
return;
}
const isInViewPortItems = this.isInViewPortItems(lastEvent.seq);
this.swapScrollTo(
this.eventList[this.eventList.length - 1],
() => {},
() => {
this.initalized = true;
},
-1 === this.virtualScroller.viewPortInfo.endIndex ||
!isInViewPortItems,
!isInViewPortItems
);
}
}
}
initEventMore(gotoEventSeq?: number) {
// 방정보가 바뀌면 이전대화 보기 관련 값들을 초기화 한다.
this.scrollUpInitalized = false;
this.storedScrollItem = undefined;
this.storedScrollItemOffsetTop = undefined;
this.gotoEventSeq = gotoEventSeq;
}
clear() {}
gotoPosition(eventSeq: number) {
const isInViewPortItems = this.isInViewPortItems(eventSeq);
if (!!this.virtualScroller) {
const e = this.eventList.find(v => v.seq === eventSeq);
this.swapScrollTo(
e,
() => {},
() => {
const chatMessageBox = this.chatMessageBoxList.find(
el => el.message.seq === eventSeq
);
if (!!chatMessageBox) {
chatMessageBox.shake();
}
},
!isInViewPortItems,
!isInViewPortItems,
50
);
}
}
isInViewPortItems(eventSeq: number): boolean {
if (undefined === eventSeq) {
return false;
}
const viewPortItemIndex = this.virtualScroller.viewPortItems.findIndex(
v => v.seq === eventSeq
);
const newEvent =
!!this.newEventList &&
-1 !== this.newEventList.findIndex(e => e.seq === eventSeq);
return -1 !== viewPortItemIndex || newEvent;
}
onClickOpenProfile(userSeq: number) {
this.openProfile.emit(userSeq);
}
onClickMore(event: any) {
event.preventDefault();
event.stopPropagation();
if (this.scrollUpInitalized && this.eventRemained) {
this.virtualScroller.scrollToPosition(0);
this.virtualScroller.invalidateCachedMeasurementAtIndex(0);
this.storeScrollPosition();
this.preSwapScroll(true, true);
this.moreEvent.emit(this.eventList[0].seq);
}
}
/** [Event] MassTalk Detail View */
onMassDetail(value: number) {
this.massDetail.emit(value);
}
onMassTranslationDetail(params: {
message: Info<MassTranslationEventJson>;
contentsType: string;
}) {
this.massTranslationDetail.emit(params);
}
/** [Event] Image Viewer */
onFileViewer(fileInfo: SelectFileInfo) {
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<EventJson>;
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<EventJson>): number {
return info.seq;
}
compareItemsFunc = (
item1: Info<EventJson>,
item2: Info<EventJson>
// tslint:disable-next-line: semicolon
): boolean => !!item1 && !!item2 && item1.seq === item2.seq;
}