import { of } from 'rxjs'; import { tap, switchMap, map, catchError, exhaustMap, concatMap, withLatestFrom, debounceTime, mergeMap } from 'rxjs/operators'; import { Injectable, Inject } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Dictionary } from '@ngrx/entity'; import { EventProtocolService } from '@ucap/ng-protocol-event'; import { RoomType, OpenResponse as CreateResponse, Open3Response as CreateTimerResponse, ExitResponse as DeleteResponse, ExitAllResponse as DeleteMultiResponse } from '@ucap/protocol-room'; import { RoomProtocolService } from '@ucap/ng-protocol-room'; import { FileProtocolService } from '@ucap/ng-protocol-file'; import * as RoomActions from '../room/actions'; import { events, eventsFailure, eventsSuccess, read, readFailure, readSuccess, fileInfos, fileInfosFailure, fileInfosSuccess, send, sendSuccess, sendFailure, addEvent, addEventSuccess, del, delNotification, delFailure, delEventList, moreEvents, forward, forwardFailure, forwardAfterRoomOpen, roomOpenAfterForward, cancel, cancelFailure, cancelNotification, updateEventList } from './actions'; import { InfoRequest, ReadResponse, FileType, EventType, DelResponse, SendResponse, CancelResponse } from '@ucap/protocol-event'; import { ModuleConfig } from '../../config/module-config'; import { _MODULE_CONFIG } from '../../config/token'; import { Chatting } from './state'; import { ChattingSelector, RoomSelector } from '../state'; import { Router } from '@angular/router'; import { LocaleCode } from '@ucap/core'; import { LoginSelector } from '@ucap/ng-store-authentication'; import { I18nService } from '@ucap/ng-i18n'; @Injectable() export class Effects { events$ = createEffect( () => { return this.actions$.pipe( ofType(events), map((action) => action.req), switchMap((req) => { return this.eventProtocolService.info(req).pipe( map((res) => { if (!!res && !!res.res) { const infoList = res.infoList; this.store.dispatch( eventsSuccess({ eventInfoList: infoList, res: res.res, remainEvent: infoList.length === req.requestCount ? true : false }) ); if (req.baseSeq === 0) { // 최초 이벤트 목록 조회. // SSVC_TYPE_EVENT_READ_REQ 수행. const maxSeq = Math.max.apply( Math, infoList.map((v) => v.seq) ); this.store.dispatch( read({ roomId: req.roomId, lastReadSeq: Number(maxSeq) }) ); // File 정보 수집. this.store.dispatch( fileInfos({ req: { roomId: res.res.roomId, // { 파일타입 } cf) I : 이미지 V: 동영상 F: 파일 "" 빈값이면 모든 타입을 내려줌 type: FileType.All } }) ); } else { } } }), catchError((error) => of(eventsFailure({ roomId: req.roomId, error })) ) ); }) ); }, { dispatch: false } ); moreEvents$ = createEffect( () => { return this.actions$.pipe( ofType(moreEvents), mergeMap( (action) => of(action).pipe( withLatestFrom( this.store.pipe( select(ChattingSelector.eventList, action.roomId) ) ) ), (action, latestStoreData) => latestStoreData ), tap(([req, eventList]) => { if (!!eventList && eventList.length > 0) { this.store.dispatch( events({ req: { roomId: req.roomId, baseSeq: eventList.sort((a, b) => a.seq - b.seq)[0].seq, requestCount: this.moduleConfig?.eventRequestDefaultCount || 50 } as InfoRequest }) ); } }) ); }, { dispatch: false } ); read$ = createEffect( () => { return this.actions$.pipe( ofType(read), exhaustMap((req) => this.eventProtocolService.read(req).pipe( map((res: ReadResponse) => { // room user lastReadEventSeq reset. this.store.dispatch(readSuccess(res)); // room noReadCount reset. this.store.dispatch( RoomActions.updateUnreadCount({ roomId: res.roomId }) ); }), catchError((error) => of(readFailure({ error }))) ) ) ); }, { dispatch: false } ); fileInfos$ = createEffect( () => { return this.actions$.pipe( ofType(fileInfos), switchMap((action) => { return this.fileProtocolService.info(action.req).pipe( map((res) => { this.store.dispatch( fileInfosSuccess({ fileInfoList: res.fileInfos, fileInfoCheckList: res.fileInfoChecks, res: res.res }) ); }), catchError((error) => of(fileInfosFailure({ roomId: action.req.roomId, error })) ) ); }) ); }, { dispatch: false } ); addEvent$ = createEffect( () => { return this.actions$.pipe( ofType(addEvent), withLatestFrom( this.store.pipe( select( (state: any) => state.chat.chatting.chattings.entities as Dictionary ) ), this.store.pipe( select((state: any) => state.chat.chatting.activeRoomId) ) ), tap(([action, chattings, activeRoomId]) => { // Room in chattings state >> event add if (!!chattings && !!chattings[action.roomId]) { if (action.info.type === EventType.File) { // Retrieve event of FileType in rooms this.store.dispatch( fileInfos({ req: { roomId: action.roomId, type: FileType.All } }) ); } } // room > rooms :: finalEvent-Infos refresh this.store.dispatch( addEventSuccess({ roomId: action.roomId, info: action.info }) ); }) ); }, { dispatch: false } ); send$ = createEffect(() => this.actions$.pipe( ofType(send), concatMap((action) => this.eventProtocolService.send(action.req).pipe( map((res) => { return sendSuccess({ senderSeq: action.senderSeq, res }); }), catchError((error) => of(sendFailure({ error }))) ) ) ) ); sendSuccess$ = createEffect( () => { return this.actions$.pipe( ofType(sendSuccess), tap((action) => { const res = action.res; this.store.dispatch( addEvent({ roomId: res.roomId, info: res.info, SVC_TYPE: res.SVC_TYPE, SSVC_TYPE: res.SSVC_TYPE }) ); }) ); }, { dispatch: false } ); /******************************************************************* * [Room Action watching.] *******************************************************************/ selectedRoomSuccess$ = createEffect( () => { return this.actions$.pipe( ofType(RoomActions.selectedRoomSuccess), debounceTime(300), tap((action) => { const requestCount = this.moduleConfig?.eventRequestInitCount || 50; const req: InfoRequest = { roomId: action.roomId, baseSeq: 0, requestCount }; this.store.dispatch(events({ req })); }) ); }, { dispatch: false } ); del$ = createEffect(() => this.actions$.pipe( ofType(del), exhaustMap((req) => this.eventProtocolService.del(req).pipe( map((res: DelResponse) => { return delNotification({ noti: res }); }), catchError((error) => of(delFailure({ error }))) ) ) ) ); delNotification$ = createEffect( () => { return this.actions$.pipe( ofType(delNotification), map((action) => action.noti), withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))), tap(([noti, activeRoomId]) => { // 현재 방이 오픈되어 있으면 방내용 갱신 const roomId = noti.roomId; if (!!activeRoomId && activeRoomId === roomId) { this.store.dispatch( delEventList({ roomId, eventSeqs: [noti.eventSeq] }) ); } // 대화 > 리스트의 항목 갱신 this.store.dispatch( RoomActions.room({ req: { roomId: noti.roomId, isDetail: false, localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode } }) ); }) ); }, { dispatch: false } ); forward$ = createEffect( () => { return this.actions$.pipe( ofType(forward), tap((action) => { if (!!action.trgtRoomSeq) { // 대화전달 후 방오픈. Exist roomSeq. if (action.req.eventType === EventType.File) { // file share request action } else { this.store.dispatch(roomOpenAfterForward(action)); } } else if (!!action.trgtUserSeqs && action.trgtUserSeqs.length > 0) { // 방오픈 후 대화전달. this.store.dispatch(forwardAfterRoomOpen(action)); } }) ); }, { dispatch: false } ); forwardAfterRoomOpen$ = createEffect( () => { let createRes: CreateResponse; return this.actions$.pipe( ofType(forwardAfterRoomOpen), exhaustMap((actionReq) => { return this.roomProtocolService .open({ divCd: 'forwardOpen', userSeqs: actionReq.trgtUserSeqs }) .pipe( map((res: CreateResponse) => { createRes = res; this.store.dispatch(RoomActions.createSuccess({ res })); }), map(() => { if (actionReq.req.eventType === EventType.File) { // file share request action } else { this.store.dispatch( send({ senderSeq: actionReq.senderSeq, req: { roomId: createRes.roomId, eventType: actionReq.req.eventType, sentMessage: actionReq.req.sentMessage } }) ); } }), catchError((error) => of(RoomActions.createFailure({ error }))) ); }) ); }, { dispatch: false } ); roomOpenAfterForward$ = createEffect( () => { return this.actions$.pipe( ofType(roomOpenAfterForward), exhaustMap((action) => { return this.eventProtocolService .send({ roomId: action.trgtRoomSeq, eventType: action.req.eventType, sentMessage: action.req.sentMessage }) .pipe( map((res: SendResponse) => { this.store.dispatch( addEvent({ roomId: res.roomId, info: res.info, SVC_TYPE: res.SVC_TYPE, SSVC_TYPE: res.SSVC_TYPE }) ); this.store.dispatch( RoomActions.selectedRoom({ roomId: res.roomId, localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode }) ); }), catchError((error) => of(RoomActions.createFailure({ error }))) ); }) ); }, { dispatch: false } ); cancel$ = createEffect(() => this.actions$.pipe( ofType(cancel), exhaustMap((req) => this.eventProtocolService.cancel(req).pipe( map((res: CancelResponse) => { return delNotification({ noti: res }); }), catchError((error) => of(delFailure({ error }))) ) ) ) ); cancelNotification$ = createEffect( () => { return this.actions$.pipe( ofType(cancelNotification), withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))), tap(([action, activeRoomId]) => { // 현재 방이 오픈되어 있으면 방내용 갱신 if ( !!activeRoomId && activeRoomId.localeCompare(action.noti.roomId) === 0 ) { this.store.dispatch( updateEventList({ roomId: action.noti.roomId, eventSeq: action.noti.eventSeq, sentMessage: this.i18nService.t('event.recalled') }) ); } // 대화 > 리스트의 항목 갱신 this.store.dispatch( RoomActions.room({ req: { roomId: action.noti.roomId, isDetail: false, localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode } }) ); }) ); }, { dispatch: false } ); constructor( private actions$: Actions, private store: Store, private router: Router, @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, private roomProtocolService: RoomProtocolService, private eventProtocolService: EventProtocolService, private fileProtocolService: FileProtocolService, private i18nService: I18nService ) { this.i18nService.setDefaultNamespace('chat'); } }