여기까지 읽음 기능 추가.

This commit is contained in:
leejinho 2019-12-19 14:23:11 +09:00
parent 7501275465
commit 7a448f3f1a
16 changed files with 303 additions and 55 deletions

View File

@ -179,7 +179,6 @@
<div
fxFlex="1 1 auto"
class="chat-content"
#messageBoxContainer
ucapFileUploadFor
[fileUploadQueue]="fileUploadQueue"
(fileSelected)="onFileSelected($event)"
@ -187,6 +186,7 @@
(fileDragOver)="onFileDragOver()"
(fileDragLeave)="onFileDragLeave()"
>
<!-- CHAT MESSAGES -->
<perfect-scrollbar
fxFlex="1 1 auto"
@ -196,13 +196,16 @@
(psYReachEnd)="onScrollReachEnd($event)"
>
<ucap-chat-messages
[messages]="eventList"
[eventList]="eventList"
[roomInfo]="roomInfo"
[eventInfoStatus]="eventInfoStatus"
[eventRemain]="eventRemain$ | async"
[userInfos]="userInfoList"
[loginRes]="loginRes"
[sessionVerInfo]="sessionVerInfo"
[isShowUnreadCount]="getShowUnreadCount()"
[clearReadHere]="clearReadHere"
[minShowReadHere]="CONST.READ_HERE_MIN_EVENT_INFO_READ_COUNT"
(moreEvent)="onMoreEvent($event)"
(massDetail)="onMassDetail($event)"
(save)="onSave($event)"

View File

@ -3,7 +3,6 @@ import {
OnInit,
OnDestroy,
ViewChild,
ElementRef,
AfterViewInit,
Output,
EventEmitter
@ -24,7 +23,7 @@ import {
} from '@ucap-webmessenger/ui';
import { Store, select } from '@ngrx/store';
import { NGXLogger } from 'ngx-logger';
import { Observable, Subscription, forkJoin, of } from 'rxjs';
import { Observable, Subscription, forkJoin, of, combineLatest } from 'rxjs';
import {
Info,
EventType,
@ -109,9 +108,6 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
@Output()
openProfile = new EventEmitter<UserInfo>();
@ViewChild('messageBoxContainer', { static: true })
private messageBoxContainer: ElementRef;
@ViewChild('chatForm', { static: false })
private chatForm: UCapUiChatFormComponent;
@ -159,9 +155,14 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
isShowStickerSelector = false;
selectedSticker: StickerFilesInfo;
/** About ReadHere */
firstcheckReadHere = true;
clearReadHere = false;
snackBarPreviewEvent: MatSnackBarRef<SimpleSnackBar>;
RoomType = RoomType;
CONST = CONST;
constructor(
private store: Store<any>,
@ -229,10 +230,15 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
})
);
this.eventListSubscription = this.store
// [Daesang]
this.eventListSubscription = combineLatest([
this.store.pipe(
select(AppStore.MessengerSelector.EventSelector.selectAllInfoList)
),
this.store.pipe(select(AppStore.MessengerSelector.RoomSelector.roomInfo))
])
.pipe(
select(AppStore.MessengerSelector.EventSelector.selectAllInfoList),
tap(infoList => {
tap(([infoList, roomInfo]) => {
if (!!this.eventList && this.eventList.length > 0) {
this.eventListNew = infoList.filter(info => {
if (info.seq <= this.eventList[this.eventList.length - 1].seq) {
@ -240,7 +246,20 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
}
return true;
});
if (
!!infoList &&
infoList.length > 0 &&
!!roomInfo &&
!!roomInfo.lastReadEventSeq &&
roomInfo.lastReadEventSeq > 0 &&
this.baseEventSeq <= roomInfo.lastReadEventSeq
) {
// 조회된 내용중에 read here 가 있을 경우.
this.firstcheckReadHere = false;
}
}
this.eventList = infoList;
if (!!infoList && infoList.length > 0) {
@ -251,6 +270,29 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
)
.subscribe();
// // [GROUP]
// this.eventListSubscription = this.store
// .pipe(
// select(AppStore.MessengerSelector.EventSelector.selectAllInfoList),
// tap(infoList => {
// if (!!this.eventList && this.eventList.length > 0) {
// this.eventListNew = infoList.filter(info => {
// if (info.seq <= this.eventList[this.eventList.length - 1].seq) {
// return false;
// }
// return true;
// });
// }
// this.eventList = infoList;
// if (!!infoList && infoList.length > 0) {
// this.baseEventSeq = infoList[0].seq;
// this.readyToReply();
// }
// })
// )
// .subscribe();
this.eventInfoStatusSubscription = this.store
.pipe(
select(AppStore.MessengerSelector.EventSelector.infoStatus),
@ -296,6 +338,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
// Sticker Selector Clear..
this.isShowStickerSelector = false;
this.selectedSticker = undefined;
this.firstcheckReadHere = true;
}
get _roomUserInfos() {
@ -395,6 +439,17 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.psChatContent.directiveRef) {
this.psChatContent.directiveRef.update();
const element = document.getElementById('message-box-readhere');
if (!!element && this.firstcheckReadHere) {
setTimeout(() => {
this.psChatContent.directiveRef.scrollToTop(
element.offsetTop - 200,
speed
);
});
this.firstcheckReadHere = false;
} else {
setTimeout(() => {
this.psChatContent.directiveRef.scrollToTop(
this.psChatContent.directiveRef.elementRef.nativeElement
@ -405,6 +460,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.eventMorePosition = 0;
});
}
}
} else if (this.scrollUpinit) {
if (!!this.eventListNew && this.eventListNew.length > 0) {
let message = '';
@ -437,23 +493,41 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.psChatContent.directiveRef) {
this.psChatContent.directiveRef.update();
const element = document.getElementById('message-box-readhere');
if (!!element && this.firstcheckReadHere) {
setTimeout(() => {
this.psChatContent.directiveRef.scrollToTop(
element.offsetTop - 200,
speed
);
});
this.firstcheckReadHere = false;
} else {
setTimeout(() => {
this.psChatContent.directiveRef.scrollToBottom(0, speed);
});
}
}
}
}
onScrollup(event: any) {
this.scrollUpinit = true;
}
onScrollReachStart(event: any) {
this.onMoreEvent(this.baseEventSeq);
// 자동 스크롤이 아닌 버튼 방식으로 변경.
// this.onMoreEvent(this.baseEventSeq);
}
onScrollReachEnd(event: any) {
this.setEventMoreInit();
if (!!this.snackBarPreviewEvent) {
this.snackBarPreviewEvent.dismiss();
}
// clear readHere object..
if (!this.firstcheckReadHere) {
this.clearReadHere = true;
}
}
/** More Event */

View File

@ -32,6 +32,7 @@ export const infoSuccess = createAction(
props<{
infoList: Info<EventJson>[];
res: InfoResponse;
remainInfo: boolean;
}>()
);
export const infoFailure = createAction(
@ -61,6 +62,7 @@ export const infoMoreSuccess = createAction(
props<{
infoList: Info<EventJson>[];
res: InfoResponse;
remainInfo: boolean;
}>()
);

View File

@ -106,14 +106,47 @@ import { RoomUserData } from '@ucap-webmessenger/protocol-sync';
@Injectable()
export class Effects {
// selectedRoomForInfo$ = createEffect(() =>
// this.actions$.pipe(
// ofType(ChatStore.selectedRoom),
// map(action => {
// return info({
// roomSeq: action.roomSeq,
// baseSeq: 0,
// requestCount: CONST.EVENT_INFO_READ_COUNT
// });
// })
// )
// );
selectedRoomForInfo$ = createEffect(() =>
this.actions$.pipe(
ofType(ChatStore.selectedRoom),
ofType(RoomStore.infoSuccess),
map(action => {
const roomInfo = action.roomInfo;
let requestCount = 0;
// 여기까지 읽음 처리..
if (!!roomInfo.finalEventSeq && !!roomInfo.lastReadEventSeq) {
requestCount = roomInfo.finalEventSeq - roomInfo.lastReadEventSeq;
// 기존 요청개수보다 요청할 갯수가 적을 경우 기본값.
if (CONST.EVENT_INFO_READ_COUNT >= requestCount) {
requestCount = CONST.EVENT_INFO_READ_COUNT;
} else {
// 여기까지 읽음 처리를 위한 최대 요청 개수 제한.
if (CONST.READ_HERE_MAX_EVENT_INFO_READ_COUNT < requestCount) {
requestCount = CONST.READ_HERE_MAX_EVENT_INFO_READ_REQUEST_COUNT;
} else {
requestCount = requestCount + 5;
}
}
}
return info({
roomSeq: action.roomSeq,
roomSeq: roomInfo.roomSeq,
baseSeq: 0,
requestCount: CONST.EVENT_INFO_READ_COUNT
requestCount
});
})
)
@ -141,14 +174,18 @@ export class Effects {
this.store.dispatch(
infoSuccess({
infoList,
res: res as InfoResponse
res: res as InfoResponse,
remainInfo:
infoList.length === req.requestCount ? true : false
})
);
} else {
this.store.dispatch(
infoMoreSuccess({
infoList,
res: res as InfoResponse
res: res as InfoResponse,
remainInfo:
infoList.length === req.requestCount ? true : false
})
);
}

View File

@ -37,11 +37,7 @@ export const reducer = createReducer(
}),
infoStatus: action.res,
infoListProcessing: false,
remainInfo:
!!action.infoList &&
action.infoList.length === CONST.EVENT_INFO_READ_COUNT
? true
: false
remainInfo: action.remainInfo
};
}),
on(infoMoreSuccess, (state, action) => {
@ -52,11 +48,7 @@ export const reducer = createReducer(
}),
infoStatus: action.res,
infoListProcessing: false,
remainInfo:
!!action.infoList &&
action.infoList.length === CONST.EVENT_INFO_READ_COUNT
? true
: false
remainInfo: action.remainInfo
};
}),
on(infoFailure, (state, action) => {

View File

@ -1,10 +1,17 @@
export enum CONST {
/** 대용량 텍스트로 보내는 문자열의 길이 기준 */
MASSTEXT_LEN = 800,
/** 대화방의 이벤트를 조회하는 수 */
/** 대화방의 이벤트를 조회하는 수 */
EVENT_INFO_READ_COUNT = 50,
/** Timer Room 최초 오픈시 timer interval */
DEFAULT_TIMER_ROOM_INTERVAL = 24 * 60 * 60,
/** 한번에 채팅을 할 수 있는 인원수 제한 */
CHATROOM_USER = 300
CHATROOM_USER = 300,
/** 여기까지 읽음을 표시할때 조회할 최소 이벤트 갯수 */
READ_HERE_MIN_EVENT_INFO_READ_COUNT = 10,
/** 여기까지 읽음을 표시할때 조회할 최대 이벤트 갯수 */
READ_HERE_MAX_EVENT_INFO_READ_COUNT = 100,
/** 여기까지 읽음을 표시할때 조회할 최대 이벤트 갯수를 초과 했을 경우 요청할 최초 이벤트 갯수 */
READ_HERE_MAX_EVENT_INFO_READ_REQUEST_COUNT = 10
}

View File

@ -28,4 +28,12 @@ export interface RoomInfo {
isTimeRoom: boolean;
/** 12. 타이머시간(n) */
timeRoomInterval: number;
// [Daesang]
/** PIN 정렬정보 */
pinOrderInfo?: string;
/** 마지막 읽은 이벤트SEQ */
lastReadEventSeq?: number;
/** 최종대화SEQ */
finalEventSeq?: number;
}

View File

@ -94,7 +94,11 @@ export const decodeInfoData: ProtocolDecoder<InfoData> = (
isJoinRoom: info[9] === 'Y' ? true : false,
expiredFileStdSeq: Number(info[10]),
isTimeRoom: info[11] === 'Y' ? true : false,
timeRoomInterval: info[11] !== 'Y' ? 0 : Number(info[12]) || 0
timeRoomInterval: info[11] !== 'Y' ? 0 : Number(info[12]) || 0,
pinOrderInfo: info[13],
lastReadEventSeq: Number(info[14]),
finalEventSeq: Number(info[15])
};
}
}

View File

@ -88,7 +88,11 @@ export const decodeRoomData: ProtocolDecoder<RoomData> = (
isJoinRoom: info[9] === 'Y' ? true : false,
expiredFileStdSeq: Number(info[10]),
isTimeRoom: info[11] === 'Y' ? true : false,
timeRoomInterval: info[11] !== 'Y' ? 0 : Number(info[12]) || 0
timeRoomInterval: info[11] !== 'Y' ? 0 : Number(info[12]) || 0,
pinOrderInfo: info[13],
lastReadEventSeq: Number(info[14]),
finalEventSeq: Number(info[15])
});
}
}

View File

@ -0,0 +1,5 @@
<div class="read-here">
<span class="line"></span>
<span>여기까지 읽었습니다.</span>
<span class="line"></span>
</div>

View File

@ -0,0 +1,18 @@
.read-here {
display: flex;
align-content: center;
flex-flow: row;
.line {
height: 1px;
background-color: #cccccc;
width: 40%;
flex: 1 1 auto;
margin-bottom: 10px;
}
.date {
width: 160px;
font-size: 13px;
text-align: center;
font-weight: 600;
}
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ReadHereComponent } from './read-here.component';
describe('Chat::MessageBox::ReadHereComponent', () => {
let component: ReadHereComponent;
let fixture: ComponentFixture<ReadHereComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ReadHereComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ReadHereComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'ucap-chat-message-box-read-here',
templateUrl: './read-here.component.html',
styleUrls: ['./read-here.component.scss']
})
export class ReadHereComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@ -1,7 +1,20 @@
<div class="chat-messages" #scrollMe>
<!-- <div class="message-row" *ngIf="eventRemain">
<button mat-button (click)="onClickMore($event)">이전 대화 보기</button>
</div> -->
<div class="message-row" *ngIf="eventRemain">
<button mat-button (click)="onClickMore($event)">
이전 대화 보기
<span
*ngIf="
!!roomInfo &&
!!firstEventSeq &&
roomInfo.lastReadEventSeq < firstEventSeq
"
>
안읽은 메시지가 더 있습니다.({{
firstEventSeq - (roomInfo.lastReadEventSeq + 1)
}})개
</span>
</button>
</div>
<!-- MESSAGE -->
<div
*ngFor="let message of messages; let i = index"
@ -11,6 +24,13 @@
contact: message.senderSeq !== loginRes.userSeq
}"
>
<ucap-chat-message-box-read-here
id="message-box-readhere"
*ngIf="getReadHere(i) && existReadHere && !clearReadHere"
class="date-splitter"
>
</ucap-chat-message-box-read-here>
<ucap-chat-message-box-date-splitter
*ngIf="getDateSplitter(i)"
class="date-splitter"

View File

@ -1,13 +1,5 @@
import { CONST } from '@ucap-webmessenger/core';
import {
Component,
OnInit,
Input,
EventEmitter,
Output,
ViewEncapsulation,
HostListener
} from '@angular/core';
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import {
Info,
@ -17,7 +9,7 @@ import {
FileEventJson
} from '@ucap-webmessenger/protocol-event';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { UserInfo } from '@ucap-webmessenger/protocol-room';
import { UserInfo, RoomInfo, RoomType } from '@ucap-webmessenger/protocol-room';
import { NGXLogger } from 'ngx-logger';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { FileInfo } from '../models/file-info.json';
@ -33,7 +25,16 @@ export class MessagesComponent implements OnInit {
@Input()
loginRes: LoginResponse;
@Input()
messages: Info<EventJson>[];
roomInfo: RoomInfo;
@Input()
set eventList(elist: Info<EventJson>[]) {
if (!!elist && elist.length > 0) {
this.firstEventSeq = elist[0].seq;
this.lastEventSeq = elist[elist.length - 1].seq;
}
this.messages = elist;
}
@Input()
eventInfoStatus?: InfoResponse;
@Input()
@ -44,6 +45,10 @@ export class MessagesComponent implements OnInit {
sessionVerInfo: VersionInfo2Response;
@Input()
isShowUnreadCount = true;
@Input()
clearReadHere: boolean;
@Input()
minShowReadHere = 10;
@Output()
openProfile = new EventEmitter<UserInfo>();
@ -61,11 +66,17 @@ export class MessagesComponent implements OnInit {
message: Info<EventJson>;
}>();
messages: Info<EventJson>[];
EventType = EventType;
CONST = CONST;
profileImageRoot: string;
moment = moment;
firstEventSeq = 0;
lastEventSeq = 0;
existReadHere = false;
constructor(private logger: NGXLogger, private datePipe: DatePipe) {}
ngOnInit() {
@ -165,6 +176,31 @@ export class MessagesComponent implements OnInit {
return false;
}
getReadHere(messageIndex: number): boolean {
if (
!!this.roomInfo &&
!!this.roomInfo.lastReadEventSeq &&
this.roomInfo.lastReadEventSeq > 0 &&
this.lastEventSeq - this.roomInfo.lastReadEventSeq > 5
) {
if (
this.roomInfo.roomType === RoomType.Single ||
this.roomInfo.roomType === RoomType.Multi
) {
if (!this.roomInfo.isTimeRoom) {
if (
this.messages[messageIndex].seq ===
this.roomInfo.lastReadEventSeq + 1
) {
this.existReadHere = true;
return true;
}
}
}
}
return false;
}
onClickOpenProfile(event: MouseEvent, userInfo: UserInfo) {
event.preventDefault();
event.stopPropagation();

View File

@ -23,6 +23,7 @@ import { InformationComponent as MBInformationComponent } from './components/mes
import { MassTranslationComponent as MBMassTranslationComponent } from './components/message-box/mass-translation.component';
import { MassComponent as MBMassComponent } from './components/message-box/mass.component';
import { NoticeComponent as MBNoticeComponent } from './components/message-box/notice.component';
import { ReadHereComponent as MBReadHereComponent } from './components/message-box/read-here.component';
import { RecallComponent as MBRecallComponent } from './components/message-box/recall.component';
import { ScheduleComponent as MBScheduleComponent } from './components/message-box/schedule.component';
import { StickerComponent as MBStickerComponent } from './components/message-box/sticker.component';
@ -43,6 +44,7 @@ const COMPONENTS = [
MBMassTranslationComponent,
MBMassComponent,
MBNoticeComponent,
MBReadHereComponent,
MBRecallComponent,
MBScheduleComponent,
MBStickerComponent,