virtual scroll of chat room is implemented

This commit is contained in:
Richard Park 2020-01-20 16:40:17 +09:00
parent 86a9cd3d84
commit f41c5ae5f0
5 changed files with 130 additions and 72 deletions

View File

@ -7,7 +7,8 @@ import {
Output, Output,
EventEmitter, EventEmitter,
Inject, Inject,
ChangeDetectorRef ChangeDetectorRef,
ChangeDetectionStrategy
} from '@angular/core'; } from '@angular/core';
import { import {
ucapAnimations, ucapAnimations,
@ -133,7 +134,8 @@ import { FileProtocolService } from '@ucap-webmessenger/protocol-file';
selector: 'app-layout-messenger-messages', selector: 'app-layout-messenger-messages',
templateUrl: './messages.component.html', templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'], styleUrls: ['./messages.component.scss'],
animations: ucapAnimations animations: ucapAnimations,
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() @Output()
@ -391,14 +393,14 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
} }
}); });
this.interval = setInterval(() => { if (
if ( !!this.roomInfoSubject.value &&
!!this.roomInfoSubject.value && !!this.roomInfoSubject.value.isTimeRoom
!!this.roomInfoSubject.value.isTimeRoom ) {
) { this.interval = setInterval(() => {
this.store.dispatch(EventStore.infoIntervalClear({})); this.store.dispatch(EventStore.infoIntervalClear({}));
} }, 1000);
}, 1000); }
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -424,7 +426,9 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.searchEventListProcessingSubscription.unsubscribe(); this.searchEventListProcessingSubscription.unsubscribe();
} }
clearInterval(this.interval); if (!!this.interval) {
clearInterval(this.interval);
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -1847,7 +1851,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.eventRemainedSubject.value this.eventRemainedSubject.value
) { ) {
this.moreSearchProcessing = true; this.moreSearchProcessing = true;
this.chatMessages.storeScrollHeight(); this.chatMessages.storeScrollPosition();
// Case :: retrieve event infos step by step until include searchtext in event.. // Case :: retrieve event infos step by step until include searchtext in event..
this.store.dispatch( this.store.dispatch(

View File

@ -111,14 +111,14 @@ export class MessageBoxComponent implements OnInit, AfterViewInit {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.logger.debug( // this.logger.debug(
'offsetHeight' + this.message.seq, // 'offsetHeight' + this.message.seq,
this.mbContainer.nativeElement.offsetHeight // this.mbContainer.nativeElement.offsetHeight
); // );
this.elementRef.nativeElement.style.height = `${this.mbContainer // this.elementRef.nativeElement.style.height = `${this.mbContainer
.nativeElement.offsetHeight + 20}px`; // .nativeElement.offsetHeight + 20}px`;
this.elementRef.nativeElement.style.maxHeight = `${this.mbContainer // this.elementRef.nativeElement.style.maxHeight = `${this.mbContainer
.nativeElement.offsetHeight + 20}px`; // .nativeElement.offsetHeight + 20}px`;
this.mbContainer.nativeElement.classList.remove('hide'); this.mbContainer.nativeElement.classList.remove('hide');
} }

View File

@ -9,8 +9,14 @@
(psYReachStart)="onYReachStart($event)" (psYReachStart)="onYReachStart($event)"
(psYReachEnd)="onYReachEnd($event)" (psYReachEnd)="onYReachEnd($event)"
[enableUnequalChildrenSizes]="true" [enableUnequalChildrenSizes]="true"
[modifyOverflowStyleOfParentScroll]="false"
(vsChange)="onVsChange($event)"
> >
<div class="chat-messages" [style.opacity]="fixScreen ? 0 : 1"> <div
#chatMessagesContainer
class="chat-messages hide"
[style.opacity]="fixScreen ? 0 : 1"
>
<div <div
*ngIf="eventRemained && eventList.length > 0" *ngIf="eventRemained && eventList.length > 0"
class="message-row view-previous" class="message-row view-previous"
@ -51,17 +57,15 @@
<!-- MESSAGE --> <!-- MESSAGE -->
<div #container> <div #container>
<ucap-chat-message-box <ucap-chat-message-box
*ngFor=" *ngFor="let message of scroll.viewPortItems; trackBy: trackByEvent"
let message of scroll.viewPortItems;
trackBy: trackByEvent;
let i = index
"
[id]="message.seq" [id]="message.seq"
[message]="message" [message]="message"
[mine]="message.senderSeq === loginRes.userSeq" [mine]="message.senderSeq === loginRes.userSeq"
[highlight]="isHighlightedEvent(message.seq)" [highlight]="isHighlightedEvent(message.seq)"
[existReadToHere]="getReadHere(i) && existReadHere && !clearReadHere" [existReadToHere]="
[dateChanged]="getDateSplitter(i)" getReadHere(message) && existReadHere && !clearReadHere
"
[dateChanged]="getDateSplitter(message)"
[senderName]="getUserName(message.senderSeq)" [senderName]="getUserName(message.senderSeq)"
[profileImageRoot]="profileImageRoot" [profileImageRoot]="profileImageRoot"
[profileImage]="getUserProfile(message.senderSeq)" [profileImage]="getUserProfile(message.senderSeq)"

View File

@ -185,3 +185,7 @@ $meBox-bg: #ffffff;
} }
} }
} }
.hide {
opacity: 0 !important;
}

View File

@ -6,8 +6,8 @@ import {
Output, Output,
ViewChild, ViewChild,
OnDestroy, OnDestroy,
ElementRef, ChangeDetectionStrategy,
Self ElementRef
} from '@angular/core'; } from '@angular/core';
import { import {
@ -28,12 +28,13 @@ import { FileDownloadItem } from '@ucap-webmessenger/api';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller'; import { VirtualScrollerComponent, IPageInfo } from 'ngx-virtual-scroller';
@Component({ @Component({
selector: 'ucap-chat-messages', selector: 'ucap-chat-messages',
templateUrl: './messages.component.html', templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'] styleUrls: ['./messages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MessagesComponent implements OnInit, OnDestroy { export class MessagesComponent implements OnInit, OnDestroy {
@Input() @Input()
@ -103,13 +104,16 @@ export class MessagesComponent implements OnInit, OnDestroy {
@Output() @Output()
existNewMessage = new EventEmitter<Info<EventJson>>(); existNewMessage = new EventEmitter<Info<EventJson>>();
@ViewChild('chatMessagesContainer', { static: false })
chatMessagesContainer: ElementRef<HTMLElement>;
@ViewChild(PerfectScrollbarDirective, { static: false }) @ViewChild(PerfectScrollbarDirective, { static: false })
psChatContent: PerfectScrollbarDirective; psChatContent: PerfectScrollbarDirective;
@ViewChild(VirtualScrollerComponent, { static: false }) @ViewChild(VirtualScrollerComponent, { static: false })
private virtualScroller: VirtualScrollerComponent; private virtualScroller: VirtualScrollerComponent;
storedScrollHeight = 0; // 이전대화를 불러올 경우 현재 스크롤 포지션 유지를 위한 값. 0 이면 초기로딩. storedScrollItem: Info<EventJson>; // 이전대화를 불러올 경우 현재 스크롤 포지션 유지를 위한 값. 0 이면 초기로딩.
scrollUpInitalized = false; // ps 에서 초기 로딩시 scroll reach start 이벤트 발생 버그를 우회하기 위한 init 값으로 scrollUp 에 의해 true 로 된다. scrollUpInitalized = false; // ps 에서 초기 로딩시 scroll reach start 이벤트 발생 버그를 우회하기 위한 init 값으로 scrollUp 에 의해 true 로 된다.
firstCheckReadHere = true; firstCheckReadHere = true;
initRoomLastEventSeq: number; initRoomLastEventSeq: number;
@ -175,6 +179,8 @@ export class MessagesComponent implements OnInit, OnDestroy {
this.firstCheckReadHere = false; this.firstCheckReadHere = false;
} }
this.eventList = eventList;
if (this.searchingMode) { if (this.searchingMode) {
const baseseq = this.baseEventSeq; const baseseq = this.baseEventSeq;
// setTimeout(() => { // setTimeout(() => {
@ -187,8 +193,6 @@ export class MessagesComponent implements OnInit, OnDestroy {
this.ready(); this.ready();
} }
} }
this.eventList = eventList;
}); });
this.newEventListSubscription = this.newEventList$.subscribe( this.newEventListSubscription = this.newEventList$.subscribe(
newEventList => { newEventList => {
@ -310,7 +314,9 @@ export class MessagesComponent implements OnInit, OnDestroy {
} }
/** Date Splitter show check */ /** Date Splitter show check */
getDateSplitter(curIndex: number): boolean { getDateSplitter(message: Info<EventJson>): boolean {
const curIndex = this.eventList.findIndex(v => v.seq === message.seq);
if (curIndex === 0) { if (curIndex === 0) {
return true; return true;
} }
@ -318,21 +324,15 @@ export class MessagesComponent implements OnInit, OnDestroy {
if (!this.eventList[curIndex]) { if (!this.eventList[curIndex]) {
return false; return false;
} }
return ( return !moment(this.eventList[curIndex].sendDate).isSame(
this.datePipe.transform( moment(this.eventList[curIndex - 1].sendDate),
moment(this.eventList[curIndex].sendDate).toDate(), 'day'
'yyyyMMdd'
) !==
this.datePipe.transform(
moment(this.eventList[curIndex - 1].sendDate).toDate(),
'yyyyMMdd'
)
); );
} }
return false; return false;
} }
getReadHere(messageIndex: number): boolean { getReadHere(message: Info<EventJson>): boolean {
if ( if (
!!this.roomInfo && !!this.roomInfo &&
!!this.roomInfo.lastReadEventSeq && !!this.roomInfo.lastReadEventSeq &&
@ -344,6 +344,10 @@ export class MessagesComponent implements OnInit, OnDestroy {
this.roomInfo.roomType === RoomType.Multi this.roomInfo.roomType === RoomType.Multi
) { ) {
if (!this.roomInfo.isTimeRoom) { if (!this.roomInfo.isTimeRoom) {
const messageIndex = this.eventList.findIndex(
v => v.seq === message.seq
);
if ( if (
this.eventList[messageIndex].seq === this.eventList[messageIndex].seq ===
this.roomInfo.lastReadEventSeq + 1 this.roomInfo.lastReadEventSeq + 1
@ -365,40 +369,51 @@ export class MessagesComponent implements OnInit, OnDestroy {
return rtnStr; return rtnStr;
} }
storeScrollHeight() { storeScrollPosition() {
this.storedScrollHeight = this.psChatContent.elementRef.nativeElement.scrollHeight; 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 { ready(): void {
if (!this.scrollUpInitalized) {
this.chatMessagesContainer.nativeElement.classList.add('hide');
}
setTimeout(() => { setTimeout(() => {
this.scrollToBottom(); this.scrollToBottom();
// setTimeout(() => {
// this.chatMessagesContainer.nativeElement.classList.remove('hide');
// }, 100);
}); });
} }
scrollToBottom(speed?: number): void { scrollToBottom(speed?: number): void {
if (this.storedScrollHeight > 0) { if (!!this.storedScrollItem) {
if (this.psChatContent) { // if (this.psChatContent) {
this.psChatContent.update(); // this.psChatContent.update();
const element = document.getElementById('message-box-readhere'); const element = document.getElementById('message-box-readhere');
if (!!element && this.firstCheckReadHere) { if (!!element && this.firstCheckReadHere) {
setTimeout(() => { setTimeout(() => {
this.psChatContent.scrollToTop(element.offsetTop - 200, speed); this.psChatContent.scrollToTop(element.offsetTop - 200, speed);
}); });
this.firstCheckReadHere = false; this.firstCheckReadHere = false;
} else { } else {
setTimeout(() => { this.scrollToStoredItem();
this.psChatContent.scrollToTop(
this.psChatContent.elementRef.nativeElement.scrollHeight -
this.storedScrollHeight,
speed
);
this.storedScrollHeight = 0;
});
}
} }
// }
} else if (this.scrollUpInitalized) { } else if (this.scrollUpInitalized) {
if (!!this.newEventList && this.newEventList.length > 0) { if (!!this.newEventList && this.newEventList.length > 0) {
this.existNewMessage.emit( this.existNewMessage.emit(
@ -418,9 +433,23 @@ export class MessagesComponent implements OnInit, OnDestroy {
this.firstCheckReadHere = false; this.firstCheckReadHere = false;
} else { } else {
setTimeout(() => { this.virtualScroller.scrollInto(
this.psChatContent.scrollToBottom(0, speed); this.eventList[this.eventList.length - 1],
}); true,
0,
0,
() => {
if (!this.scrollUpInitalized) {
this.chatMessagesContainer.nativeElement.classList.remove(
'hide'
);
}
}
);
// setTimeout(() => {
// this.psChatContent.scrollToBottom(0, speed);
// });
} }
} }
} }
@ -429,7 +458,7 @@ export class MessagesComponent implements OnInit, OnDestroy {
initEventMore() { initEventMore() {
// 방정보가 바뀌면 이전대화 보기 관련 값들을 초기화 한다. // 방정보가 바뀌면 이전대화 보기 관련 값들을 초기화 한다.
this.scrollUpInitalized = false; this.scrollUpInitalized = false;
this.storedScrollHeight = 0; this.storedScrollItem = undefined;
} }
clear() {} clear() {}
@ -464,9 +493,12 @@ export class MessagesComponent implements OnInit, OnDestroy {
event.stopPropagation(); event.stopPropagation();
if (this.scrollUpInitalized && this.eventRemained) { if (this.scrollUpInitalized && this.eventRemained) {
this.storedScrollHeight = this.psChatContent.elementRef.nativeElement.scrollHeight; // this.storedScrollItem = this.psChatContent.elementRef.nativeElement.scrollHeight;
this.storeScrollPosition();
this.moreEvent.emit(this.eventList[0].seq); this.moreEvent.emit(this.eventList[0].seq);
this.virtualScroller.invalidateCachedMeasurementAtIndex(0);
} }
} }
@ -506,6 +538,9 @@ export class MessagesComponent implements OnInit, OnDestroy {
} }
onScrollup(event: any) { onScrollup(event: any) {
if (!this.eventList || 0 === this.eventList.length) {
return;
}
this.scrollUpInitalized = true; this.scrollUpInitalized = true;
this.scrollUp.emit(event); this.scrollUp.emit(event);
} }
@ -516,6 +551,17 @@ export class MessagesComponent implements OnInit, OnDestroy {
this.yReachEnd.emit(event); 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 { trackByEvent(index: number, info: Info<EventJson>): number {
return info.seq; return info.seq;
} }