From 41499618ea39c1c3f8e45711f37b268ddb1e562b Mon Sep 17 00:00:00 2001 From: Park Byung Eun Date: Fri, 21 Feb 2020 09:35:58 +0900 Subject: [PATCH] =?UTF-8?q?0220=20=EC=97=85=EB=AC=B4=20=EB=B2=88=EB=93=A4?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B7=B0=EC=96=B4=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- documents/업무/2월/3째주/0220.txt | 159 +- documents/업무/2월/3째주/0221.txt | 30 + .../file-viewer-prj/bundle-image.component.html | 23 + .../file-viewer-prj/bundle-image.component.ts | 123 + .../file-viewer-prj/bundle-image.event-json.ts | 35 + .../3째주/file-viewer-prj/common-api.service.ts | 409 ++++ .../2월/3째주/file-viewer-prj/file-info.ts | 43 + .../file-viewer.dialog.component.html | 17 + .../file-viewer.dialog.component.ts | 111 + .../2월/3째주/file-viewer-prj/file.component.ts | 73 + .../3째주/file-viewer-prj/file.event-json.ts | 52 + .../2월/3째주/file-viewer-prj/file.service.ts | 167 ++ .../file-viewer-prj/mass-text.event-json.ts | 33 + .../file-viewer-prj/mass-translation.event-json.ts | 37 + .../file-viewer-prj/message-box.component.ts | 205 ++ .../file-viewer-prj/messages.component copy.ts | 2158 +++++++++++++++++ .../3째주/file-viewer-prj/messages.component.ts | 699 ++++++ .../models copy/select-file-info.ts | 4 + .../file-viewer-prj/models/select-file-info.ts | 4 + .../3째주/file-viewer-prj/plan.event-json.ts | 34 + .../3째주/file-viewer-prj/public-api copy.ts | 49 + .../2월/3째주/file-viewer-prj/public-api.ts | 49 + .../3째주/file-viewer-prj/ucap-ui.module copy.ts | 185 ++ .../2월/3째주/file-viewer-prj/ucap-ui.module.ts | 185 ++ .../file-viewer-prj/video-conference.event-json.ts | 33 + 25 files changed, 4916 insertions(+), 1 deletion(-) create mode 100644 documents/업무/2월/3째주/0221.txt create mode 100644 documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.html create mode 100644 documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/bundle-image.event-json.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/common-api.service.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file-info.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.html create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file.component.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file.event-json.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/file.service.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/mass-text.event-json.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/mass-translation.event-json.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/message-box.component.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/messages.component copy.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/messages.component.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/models copy/select-file-info.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/models/select-file-info.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/plan.event-json.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/public-api copy.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/public-api.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module copy.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module.ts create mode 100644 documents/업무/2월/3째주/file-viewer-prj/video-conference.event-json.ts diff --git a/documents/업무/2월/3째주/0220.txt b/documents/업무/2월/3째주/0220.txt index bfe4789..377870f 100644 --- a/documents/업무/2월/3째주/0220.txt +++ b/documents/업무/2월/3째주/0220.txt @@ -3,9 +3,166 @@ 모바일로 테스트 하지 못할경우 JSON 데이터 만들어서 테스트 진행 state + reducer + action sync-service 테스트 후 수정 PhoneBookData 추가 - \ No newline at end of file + +파일 뷰어 + common-api-service.ts + file-viewer.dialog.component.html + file-viewer.dialog.component.ts + messages.component.ts ucap-webmessenger-app/layouts/messenger/component + file.servic.ts ucap-webmessenger-app/service + bundle-image.event-json.ts + file.event-json.ts + mass-text.event-json.ts + mass-translation.event-json.ts + plan.event-json.ts + video-confrence.event-json.ts + file-info.ts ucap-webmessenger-protocol-file/src/lib/models + public-api.ts ucap-webmessenger-ui/src + ucap-ui.module.ts ucap-webmessenger-ui/src/lib + file-viewer.compoent.html + file-viewer.compoent.ts + media-viewe.component.html + media-viewe.component.scss + media-viewe.component.spec.ts + media-viewe.component.ts + binary-viewe.component.html + binary-viewe.component.ts + document-viewer.component.ts + image-viewer.component.html + image-viewer.component.ts + sound-viewer.component.html + sound-viewer.component.ts + video-viewer.component.html + video-viewer.component.ts + select-file-info.ts + message-box.component.ts + messages.component.ts ucap-webmessenger-ui-chat/src /lib/component + bundle-image.component.html + bundle-image.component.ts + file.component.ts ucap-webmessenger-ui-chat/src/lib/component/message-box + +요구사항 + 이미지 뷰어 묶음 파일 출력 기능 + 기존 이미지 타입에 영향 주지 않아야 한다. + 이미지 뷰어 묶음 파일 다운로드 기능 + 이미지 뷰어 이전/다음 기능 + +메세지 출력 + 메세지 박스 출력 + 메세지 파일 출력 + +기존 파일 뷰어 분석 + app->messages.component 호출 + 파일 클릭 이벤트 정의() + room-info 서브젝트 생성 + 이벤트 리스트 서브젝트 생성 + fileviewer 클릭 이벤트 생성 + ui-chat->messages.component 호출 + room-info 서브젝트 전달 + 이벤트 서브젝트 전달 + fileviewer 클릭 이벤트 전달 + [for message.length] + ui-chat->message-box.component 호출 + message 정보 전달 + room-info 전달 + 파일저장 클릭 이벤트 전달 + fileviewer 클릭 이벤트 전달(FileEventJson) + ui-chat->message-box.component.html + EventType.File + ucap-chat-message-box-file 호출 + room-info 전달 + fileviewer 클릭 이벤트 전달 (FileEventJson) + EventType.BundleImage + ucap-chat-message-box-bundle-image 호출 + room-info 전달 + 파일 클릭 이벤트 실행(FileEventJson) + file-viewer.dialog.component.ts 호출 + fileInfo, downloadUrl, userSeq, deviceType, token 전달 + download 클릭 이벤트 정의 + file-viewer.component.ts 호출 + fileInfo 전달 + fileDownloadUrl 전달 + download 클릭 이벤트 전달() + 클로즈 이벤트 전달 + file-viewer.component.html + FileViewerType 구분 + ucap-image-viewer + fileInfo 전달 + fileDownloadUrl 전달 + closed 이벤트 클릭 전달 + download 이벤트 클릭 전달 + +수정 파일 뷰어 분석 +app->messages.component 호출 + 파일 정보 리스트 서브젝트 생성 + 선택 파일 정보 맵핑 + 이벤트 정의(선택 파일) + 이미지 뷰어 다이얼로그 호출 + ui-chat->messages.component 호출 + 파일 클릭 이벤트(상위호출) + + + +시작 +layout/messages.component.ts + ui-chat/messages.compoent + ui-chat/message-box.component + message-box/file.component.ts + image.component + + onFileViewer(fileInfo) + layout/messages.component.ts-> onFileViewer(fileInfo) 실행 + layout/common/dialog/file-view.dialog.component.ts + ui/file-viewer.component + ui/document-viewer.component.ts + ui/image-viewer.component.ts + ui/sound-viewer.component.ts + ui/video-viewer.compoent.ts + ui/binary-viewer.component.ts +끝 + +있는것 + 파일정보 리스트 + 방정보 + 사용자 정보 + +없는것 + +추가 기능 + 대화방 미디어 이미지 이전/다음 처리 + 메세지 박스 번들이미지 클릭 처리 + 미디어 이미지 뷰어 (이미지, 번들 이미지, 사운드 이미지, 비디오 이미지) 처리 + 번들이미지 다운로드 처리 + +정보 + 대화방 전체 파일정보 리스트 + 현재 사용자 선택 파일 정보 + +file-viewer.dialog.component.ts + 현재파일 정보 + +번들이미지 다운로드 처리 + +대화방 전체 파일정보 변경에 대한 구독 등록 + + + 미디어 뷰어 컴포넌트 생성 + ui/image-viewer.component.ts + ui/sound-viewer.component.ts + ui/video-viewer.compoent.ts + + +layout/messages.component.ts + + +TODO + 설정->쪽지 알림 메뉴 삭제 (주석) + 설정->원격 지원 메뉴 삭제 (주석) + left-menu->전화걸기 메뉴 삭제 (주석) \ No newline at end of file diff --git a/documents/업무/2월/3째주/0221.txt b/documents/업무/2월/3째주/0221.txt new file mode 100644 index 0000000..929e6b8 --- /dev/null +++ b/documents/업무/2월/3째주/0221.txt @@ -0,0 +1,30 @@ +추가 기능 + 대화방 미디어 이미지 이전/다음 처리 + 메세지 박스 번들이미지 클릭 처리 + 미디어 이미지 뷰어 (이미지, 번들 이미지, 사운드 이미지, 비디오 이미지) 처리 + 번들이미지 다운로드 처리 + + +TODO + 설정->쪽지 알림 메뉴 삭제 (주석) + 설정->원격 지원 메뉴 삭제 (주석) + left-menu->전화걸기 메뉴 삭제 (주석) + + +시작 +layout/messages.component.ts + ui-chat/messages.compoent + ui-chat/message-box.component + message-box/file.component.ts + image.component + + onFileViewer(fileInfo) + layout/messages.component.ts-> onFileViewer(fileInfo) 실행 + layout/common/dialog/file-view.dialog.component.ts + ui/file-viewer.component + ui/document-viewer.component.ts + ui/image-viewer.component.ts + ui/sound-viewer.component.ts + ui/video-viewer.compoent.ts + ui/binary-viewer.component.ts +끝 diff --git a/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.html b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.html new file mode 100644 index 0000000..32fc5fd --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.html @@ -0,0 +1,23 @@ +
+
+ {{ 'common.file.errors.expired' | translate }} +
+ + + + + +
diff --git a/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.ts b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.ts new file mode 100644 index 0000000..8678991 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.component.ts @@ -0,0 +1,123 @@ +import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core'; +import { BundleImageEventJson, Info } from '@ucap-webmessenger/protocol-event'; +import { FileDownloadItem } from '@ucap-webmessenger/api'; +import { NGXLogger } from 'ngx-logger'; +import { RoomInfo } from '@ucap-webmessenger/protocol-room'; +import { SelectFileInfo } from '@ucap-webmessenger/ui'; + +export interface Tile { + colspan?: number; + imgSrc?: string; +} +@Component({ + selector: 'ucap-chat-message-box-bundle-image', + templateUrl: './bundle-image.component.html', + styleUrls: ['./bundle-image.component.scss'] +}) +export class BundleImageComponent implements OnInit { + @Input() + set message(m: Info) { + this._message = m; + + this.makeTileGrid(); + } + get message() { + return this._message; + } + // tslint:disable-next-line: variable-name + _message: Info; + + @Input() + roomInfo: RoomInfo; + + @Output() + save = new EventEmitter<{ + fileInfo: BundleImageEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }>(); + @Output() + fileViewer = new EventEmitter(); + + showExpired = false; + tiles: Tile[] = []; + + constructor(private logger: NGXLogger) {} + + ngOnInit() { + this.makeTileGrid(); + } + + makeTileGrid() { + if ( + !this.message.sentMessageJson.thumbUrls || + 0 === this.message.sentMessageJson.thumbUrls.length || + 1 > this.message.sentMessageJson.fileCount + ) { + return; + } + const fileCount = Number(this.message.sentMessageJson.fileCount); + + this.tiles = []; + + const remainder = fileCount % 3; + const quotient = Math.floor(fileCount / 3); + + let checkCount: number; + + if (remainder === 1 && fileCount > 1) { + checkCount = quotient - 1; + } else if (remainder === 2 && fileCount > 1) { + checkCount = quotient; + } + + this.message.sentMessageJson.thumbUrls.forEach((v, idx) => { + const tile: Tile = {}; + + if (checkCount <= Math.floor(idx / 3)) { + tile.colspan = 3; + } else if (fileCount === 1) { + tile.colspan = 6; + } else { + tile.colspan = 2; + } + tile.imgSrc = this.message.sentMessageJson.baseUrl + v; + this.tiles.push(tile); + }); + } + + mouseEnter(event: MouseEvent): void { + if (!this.roomInfo || this.roomInfo.expiredFileStdSeq > this.message.seq) { + this.showExpired = true; + } + event.stopPropagation(); + event.preventDefault(); + } + mouseLeave(event: MouseEvent): void { + if (!this.roomInfo || this.roomInfo.expiredFileStdSeq > this.message.seq) { + this.showExpired = false; + } + event.stopPropagation(); + event.preventDefault(); + } + + getExpiredFile() { + if ( + !!this.roomInfo && + this.roomInfo.expiredFileStdSeq <= this.message.seq + ) { + return false; + } else { + return true; + } + } + + onClickFileViewer(index: number) { + if (!this.getExpiredFile()) { + this.fileViewer.emit({ + attachmentSeq: this._message.sentMessageJson.attachmentSeq, + index + }); + } + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/bundle-image.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.event-json.ts new file mode 100644 index 0000000..fe65e00 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/bundle-image.event-json.ts @@ -0,0 +1,35 @@ +import { StatusCode, JsonAnalization } from '@ucap-webmessenger/api'; +import { EventJsonDecoder } from './event-json'; + +export interface BundleImageEventJson { + statusCode?: StatusCode; + errorMessage?: string; + roomSeq?: number; + attachmentSeq?: number; + fileCount?: number; + baseUrl?: string; + thumbUrls?: string[]; +} + +export const decodeBundleImageEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + statusCode: json.StatusCode, + errorMessage: json.ErrorMessage, + roomSeq: Number(json.RoomID), + attachmentSeq: Number(json.AttSEQ), + fileCount: Number(json.FileCount), + baseUrl: json.BaseURL, + thumbUrls: json.ThumbURL + } as BundleImageEventJson; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e.toString() + } as BundleImageEventJson; + } +}; diff --git a/documents/업무/2월/3째주/file-viewer-prj/common-api.service.ts b/documents/업무/2월/3째주/file-viewer-prj/common-api.service.ts new file mode 100644 index 0000000..0e4b1f3 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/common-api.service.ts @@ -0,0 +1,409 @@ +import { Injectable, Inject } from '@angular/core'; +import { + HttpClient, + HttpEventType, + HttpResponse, + HttpRequest +} from '@angular/common/http'; + +import { Observable, Subject } from 'rxjs'; +import { map, filter } from 'rxjs/operators'; + +import { + FileProfileSaveRequest, + FileProfileSaveResponse, + encodeFileProfileSave, + decodeFileProfileSave +} from '../apis/file-profile-save'; +import { + FileTalkDownloadRequest, + encodeFileTalkDownload, + encodeFormDataFileTalkDownload +} from '../apis/file-talk-download'; +import { + FileTalkSaveRequest, + FileTalkSaveResponse, + encodeFileTalkSave, + decodeFileTalkSave +} from '../apis/file-talk-save'; +import { + FileTalkSaveMultiRequest, + FileTalkSaveMultiResponse, + encodeFileTalkSaveMulti, + decodeFileTalkSaveMulti +} from '../apis/file-talk-save-multi'; +import { + FileTalkShareRequest, + FileTalkShareResponse, + encodeFileTalkShare, + decodeFileTalkShare +} from '../apis/file-talk-share'; +import { + MassTalkDownloadRequest, + MassTalkDownloadResponse, + encodeMassTalkDownload, + decodeMassTalkDownload +} from '../apis/mass-talk-download'; +import { + MassTalkSaveRequest, + MassTalkSaveResponse, + encodeMassTalkSave, + decodeMassTalkSave +} from '../apis/mass-talk-save'; +import { + TransMassTalkDownloadRequest, + TransMassTalkDownloadResponse, + encodeTransMassTalkDownload, + decodeTransMassTalkDownload +} from '../apis/trans-mass-talk-download'; +import { + TransMassTalkSaveRequest, + TransMassTalkSaveResponse, + encodeTransMassTalkSave, + decodeTransMassTalkSave +} from '../apis/trans-mass-talk-save'; +import { + TranslationReqRequest, + TranslationReqResponse, + encodeTranslationReq, + decodeTranslationReq +} from '../apis/translation-req'; +import { + TranslationSaveRequest, + TranslationSaveResponse, + encodeTranslationSave, + decodeTranslationSave +} from '../apis/translation-save'; + +import { _MODULE_CONFIG } from '../config/token'; +import { ModuleConfig } from '../config/module-config'; +import { Urls } from '../config/urls'; +import { UrlConfig } from '@ucap-webmessenger/core'; +import { FileDownloadItem } from '@ucap-webmessenger/api'; + +@Injectable({ + providedIn: 'root' +}) +export class CommonApiService { + readonly urls: Urls; + + constructor( + @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, + private httpClient: HttpClient + ) { + this.urls = UrlConfig.getUrls( + this.moduleConfig.hostConfig, + this.moduleConfig.urls + ); + } + + public fileProfileSave( + req: FileProfileSaveRequest, + fileProfileSaveUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileProfileSaveUrl ? fileProfileSaveUrl : this.urls.fileProfileSave, + encodeFileProfileSave(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + const progress = req.fileUploadItem.uploadStart(); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.UploadProgress === event.type) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileProfileSave(event.body); + }) + ); + } + + public urlForFileTalkDownload( + req: FileTalkDownloadRequest, + fileTalkDownloadUrl?: string + ): string { + const httpReq = new HttpRequest( + 'GET', + !!fileTalkDownloadUrl ? fileTalkDownloadUrl : this.urls.fileTalkDownload, + {}, + { + params: encodeFileTalkDownload(req) + } + ); + + return httpReq.urlWithParams; + } + + public fileTalkDownload( + req: FileTalkDownloadRequest, + fileTalkDownloadUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkDownloadUrl ? fileTalkDownloadUrl : this.urls.fileTalkDownload, + encodeFormDataFileTalkDownload(req), + { reportProgress: true, responseType: 'blob' } + ); + + let progress: Subject; + if (!!req.fileDownloadItem) { + progress = req.fileDownloadItem.downloadStart(); + } + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.DownloadProgress === event.type) { + if (!!progress) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + } + return false; + }), + map((event: HttpResponse) => { + if (!!progress) { + req.fileDownloadItem.downloadComplete(); + } + return event.body; + }) + ); + } + + public fileTalkSave( + req: FileTalkSaveRequest, + fileTalkSaveUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkSaveUrl ? fileTalkSaveUrl : this.urls.fileTalkSave, + encodeFileTalkSave(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + const progress = req.fileUploadItem.uploadStart(); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.UploadProgress === event.type) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileTalkSave(event.body); + }) + ); + } + + public fileTalkSaveMulti( + req: FileTalkSaveMultiRequest, + fileTalkSaveMultiUrl?: string + ): Observable { + const httpReq = new HttpRequest( + 'POST', + !!fileTalkSaveMultiUrl + ? fileTalkSaveMultiUrl + : this.urls.fileTalkSaveMulti, + encodeFileTalkSaveMulti(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + const progress = req.fileUploadItem.uploadStart(); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.UploadProgress === event.type) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + return false; + }), + map((event: HttpResponse) => { + req.fileUploadItem.uploadComplete(); + return decodeFileTalkSaveMulti(event.body); + }) + ); + } + + public acceptableExtensionForFileTalk( + extensions: string[] + ): { accept: boolean; reject: string[] } { + let accept = true; + const reject: string[] = []; + for (const extension of extensions) { + if ( + -1 === + this.moduleConfig.acceptableFileExtensions.indexOf( + extension.toLowerCase() + ) + ) { + reject.push(extension); + accept = false; + } + } + return { + accept, + reject + }; + } + + public fileTalkShare( + req: FileTalkShareRequest + ): Observable { + return this.httpClient + .post( + this.urls.fileTalkShare, + {}, + { + params: encodeFileTalkShare(req) + } + ) + .pipe(map(res => decodeFileTalkShare(res))); + } + + public massTalkDownload( + req: MassTalkDownloadRequest + ): Observable { + return this.httpClient + .post( + this.urls.massTalkDownload, + {}, + { + params: encodeMassTalkDownload(req), + responseType: 'text' as 'json' + } + ) + .pipe(map(res => decodeMassTalkDownload(res))); + } + + public massTalkSave( + req: MassTalkSaveRequest + ): Observable { + const httpReq = new HttpRequest( + 'POST', + this.urls.massTalkSave, + encodeMassTalkSave(req), + { reportProgress: true, responseType: 'text' as 'json' } + ); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } + return false; + }), + map(res => decodeMassTalkSave((res as HttpResponse).body)) + ); + } + + public transMassTalkDownload( + req: TransMassTalkDownloadRequest + ): Observable { + return this.httpClient + .post( + this.urls.transMassTalkDownload, + {}, + { + params: encodeTransMassTalkDownload(req) + } + ) + .pipe(map(res => decodeTransMassTalkDownload(res))); + } + + public transMassTalkSave( + req: TransMassTalkSaveRequest + ): Observable { + return this.httpClient + .post( + this.urls.transMassTalkSave, + {}, + { + params: encodeTransMassTalkSave(req) + } + ) + .pipe(map(res => decodeTransMassTalkSave(res))); + } + + public translationReq( + req: TranslationReqRequest + ): Observable { + return this.httpClient + .post( + this.urls.translationReq, + {}, + { + params: encodeTranslationReq(req) + } + ) + .pipe(map(res => decodeTranslationReq(res))); + } + + public translationSave( + req: TranslationSaveRequest + ): Observable { + const httpReq = new HttpRequest( + 'POST', + this.urls.translationSave, + encodeTranslationSave(req), + { reportProgress: true } + ); + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } + return false; + }), + map(res => decodeTranslationSave((res as HttpResponse).body)) + ); + } + + public fileDownload( + fileDownloadUrl: string, + fileDownloadItem: FileDownloadItem + ): Observable { + const httpReq = new HttpRequest('GET', fileDownloadUrl, null, { + reportProgress: true, + responseType: 'blob' + }); + + let progress: Subject; + if (!!fileDownloadItem) { + progress = fileDownloadItem.downloadStart(); + } + + return this.httpClient.request(httpReq).pipe( + filter(event => { + if (event instanceof HttpResponse) { + return true; + } else if (HttpEventType.DownloadProgress === event.type) { + if (!!progress) { + progress.next(Math.round((100 * event.loaded) / event.total)); + } + } + return false; + }), + map((event: HttpResponse) => { + if (!!progress) { + fileDownloadItem.downloadComplete(); + } + return event.body; + }) + ); + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/file-info.ts b/documents/업무/2월/3째주/file-viewer-prj/file-info.ts new file mode 100644 index 0000000..ffea02f --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file-info.ts @@ -0,0 +1,43 @@ +import { FileType } from '../types/file.type'; +import { FileEventJson } from '@ucap-webmessenger/protocol-event'; + +export interface FileInfo { + // 대화방SEQ + roomSeq: string; + // 이벤트SEQ + eventSeq: number; + // 파일SEQ + seq: number; + // 송신자SEQ + senderSeq: number; + // 파일타입 + type: FileType; + // 파일이름 + name: string; + // 파일URL + url: string; + // 파일크기(byte) + size: number; + // 전송일시 + sendDate: string; + // 수신완료자수 + receivedUserCount: number; + // 수신자수 + receiverCount: number; + // 발신내용 + sentMessage: string; + // 발신내용 For Json + sentMessageJson?: FileEventJson; +} + +export function isMedia(fileInfo: FileInfo): boolean { + return ( + [FileType.Image, FileType.Sound, FileType.Video, FileType.Bundle].some( + v => v === fileInfo.type + ) || isSound(fileInfo) + ); +} + +export function isSound(fileInfo: FileInfo): boolean { + return -1 !== ['mp3'].indexOf(fileInfo.sentMessageJson.fileExt); +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.html b/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.html new file mode 100644 index 0000000..9185158 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.html @@ -0,0 +1,17 @@ + + + + + diff --git a/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.ts b/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.ts new file mode 100644 index 0000000..a3d9fc5 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file-viewer.dialog.component.ts @@ -0,0 +1,111 @@ +import { Component, OnInit, OnDestroy, Inject } from '@angular/core'; + +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { NGXLogger } from 'ngx-logger'; +import { DeviceType } from '@ucap-webmessenger/core'; +import { FileDownloadItem } from '@ucap-webmessenger/api'; +import { CommonApiService } from '@ucap-webmessenger/api-common'; +import { AppFileService } from '@app/services/file.service'; +import { FileInfo, isMedia } from '@ucap-webmessenger/protocol-file'; +import { SelectFileInfo } from '@ucap-webmessenger/ui'; + +export interface FileViewerDialogData { + fileInfos: FileInfo[]; + selectFileInfo: SelectFileInfo; + downloadUrl: string; + userSeq: number; + deviceType: DeviceType; + token: string; +} + +export interface FileViewerDialogResult {} + +@Component({ + selector: 'app-layout-common-file-viewer', + templateUrl: './file-viewer.dialog.component.html', + styleUrls: ['./file-viewer.dialog.component.scss'] +}) +export class FileViewerDialogComponent implements OnInit, OnDestroy { + isMediaType: boolean; + fileInfo: { + fileInfos: FileInfo[]; + selectFileInfo: SelectFileInfo; + }; + + currentFileInfo: FileInfo; + + constructor( + public dialogRef: MatDialogRef< + FileViewerDialogData, + FileViewerDialogResult + >, + @Inject(MAT_DIALOG_DATA) public data: FileViewerDialogData, + private commonApiService: CommonApiService, + private appFileService: AppFileService, + private logger: NGXLogger + ) { + this.currentFileInfo = this.data.fileInfos.find( + f => f.seq === this.data.selectFileInfo.attachmentSeq + ); + + if (!this.currentFileInfo) { + this.logger.warn( + 'file info is exist', + this.data.fileInfos, + this.data.selectFileInfo + ); + this.dialogRef.close(); + return; + } + + this.isMediaType = isMedia(this.currentFileInfo); + + this.fileInfo = { + fileInfos: this.data.fileInfos.filter(f => { + const i = isMedia(f); + return this.isMediaType ? i : !i; + }), + selectFileInfo: this.data.selectFileInfo + }; + } + + ngOnInit() {} + + ngOnDestroy(): void {} + + onDownload(info: { + attachmentSeq?: number; + downloadUrl?: string; + fileName: string; + fileDownloadItem: FileDownloadItem; + }): void { + this.appFileService.fileTalkDownlod({ + req: { + userSeq: this.data.userSeq, + deviceType: this.data.deviceType, + token: this.data.token, + attachmentsSeq: info.attachmentSeq, + fileDownloadItem: info.fileDownloadItem + }, + directDownloadUrl: info.downloadUrl, + fileName: info.fileName, + fileDownloadUrl: this.data.downloadUrl + }); + } + + onClosedViewer(): void { + this.dialogRef.close(); + } + + fileDownloadUrl = (attachmentSeq: number) => { + return this.commonApiService.urlForFileTalkDownload( + { + userSeq: this.data.userSeq, + deviceType: this.data.deviceType, + token: this.data.token, + attachmentsSeq: attachmentSeq + }, + this.data.downloadUrl + ); + }; +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/file.component.ts b/documents/업무/2월/3째주/file-viewer-prj/file.component.ts new file mode 100644 index 0000000..713b0f2 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core'; +import { Info, FileEventJson } from '@ucap-webmessenger/protocol-event'; +import { StatusCode, FileDownloadItem } from '@ucap-webmessenger/api'; +import { FileType } from '@ucap-webmessenger/protocol-file'; +import { NGXLogger } from 'ngx-logger'; +import { RoomInfo } from '@ucap-webmessenger/protocol-room'; +import { SelectFileInfo } from '@ucap-webmessenger/ui'; + +@Component({ + selector: 'ucap-chat-message-box-file', + templateUrl: './file.component.html', + styleUrls: ['./file.component.scss'] +}) +export class FileComponent implements OnInit { + @Input() + message: Info; + @Input() + roomInfo: RoomInfo; + + @Output() + save = new EventEmitter<{ + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }>(); + @Output() + fileViewer = new EventEmitter(); + + fileInfo?: FileEventJson; + fileDownloadItem: FileDownloadItem; + errorMessage?: string; + FileType = FileType; + + constructor(private logger: NGXLogger) {} + + ngOnInit() { + if (StatusCode.Success === this.message.sentMessageJson.statusCode) { + this.fileInfo = this.message.sentMessageJson; + } else { + this.errorMessage = + this.message.sentMessageJson.errorMessage || '[Error] System Error!!'; + } + + this.fileDownloadItem = new FileDownloadItem(); + } + + getExpiredFile() { + if ( + !!this.roomInfo && + this.roomInfo.expiredFileStdSeq <= this.message.seq + ) { + return false; + } else { + return true; + } + } + + onClickFileViewer(fileInfo: FileEventJson) { + if (!this.getExpiredFile()) { + this.fileViewer.emit({ attachmentSeq: this.fileInfo.attachmentSeq }); + } + } + + onSave(value: string) { + if (!this.getExpiredFile()) { + this.save.emit({ + fileInfo: this.fileInfo, + fileDownloadItem: this.fileDownloadItem, + type: value + }); + } + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/file.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/file.event-json.ts new file mode 100644 index 0000000..713a3a1 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file.event-json.ts @@ -0,0 +1,52 @@ +import { StatusCode, JsonAnalization } from '@ucap-webmessenger/api'; +import { FileType } from '@ucap-webmessenger/protocol-file'; +import { EventJsonDecoder } from './event-json'; + +export interface FileEventJson { + statusCode?: StatusCode; + errorMessage?: string; + roomSeq?: number; + fileName?: string; + fileExt?: string; + fileType?: FileType; + thumbUrl?: string; + attachmentSeq?: number; + attachmentSize?: number; + attachmentRegDate?: string; + imageWidth?: number; + imageHeight?: number; + companyCode?: string; + voiceTime?: string; + synappKey?: string; +} + +export const decodeFileEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + statusCode: json.StatusCode, + errorMessage: json.ErrorMessage, + roomSeq: Number(json.RoomID), + fileName: json.FileName, + fileExt: json.FileExt, + fileType: json.FileType, + thumbUrl: json.ThumbURL, + attachmentSeq: Number(json.AttSEQ), + attachmentSize: Number(json.AttSize), + attachmentRegDate: json.AttRegDate, + imageWidth: Number(json.ImageWidth), + imageHeight: Number(json.ImageHeight), + companyCode: json.CompanyCode, + voiceTime: json.VoiceTime, + synappKey: json.SynappKey + } as FileEventJson; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e.toString() + } as FileEventJson; + } +}; diff --git a/documents/업무/2월/3째주/file-viewer-prj/file.service.ts b/documents/업무/2월/3째주/file-viewer-prj/file.service.ts new file mode 100644 index 0000000..2e997a9 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/file.service.ts @@ -0,0 +1,167 @@ +import { Injectable, Inject, NgZone } from '@angular/core'; +import { + FileTalkDownloadRequest, + CommonApiService +} from '@ucap-webmessenger/api-common'; +import { map, take, finalize, catchError } from 'rxjs/operators'; +import { MimeUtil, FileUtil } from '@ucap-webmessenger/core'; +import { FileProtocolService } from '@ucap-webmessenger/protocol-file'; +import { UCAP_NATIVE_SERVICE, NativeService } from '@ucap-webmessenger/native'; +import { + SnackBarService, + AlertSnackbarComponent, + AlertSnackbarData +} from '@ucap-webmessenger/ui'; +import { TranslateService } from '@ngx-translate/core'; +import { NGXLogger } from 'ngx-logger'; +import { of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class AppFileService { + constructor( + private commonApiService: CommonApiService, + private fileProtocolService: FileProtocolService, + @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, + private snackBarService: SnackBarService, + private translateService: TranslateService, + private httpClient: HttpClient, + private ngZone: NgZone, + private logger: NGXLogger + ) {} + + fileTalkDownlod(param: { + req?: FileTalkDownloadRequest; + directDownloadUrl?: string; + fileName: string; + fileDownloadUrl?: string; + savePath?: string; + }) { + const req = param.req; + const directDownloadUrl = param.directDownloadUrl; + const fileName = param.fileName; + const fileDownloadItem = req.fileDownloadItem; + const fileDownloadUrl = param.fileDownloadUrl; + const savePath = param.savePath; + + if (!!req && !!req.attachmentsSeq) { + this.commonApiService + .fileTalkDownload(req, fileDownloadUrl) + .pipe( + take(1), + map(rawBlob => { + const mimeType = MimeUtil.getMimeFromExtension( + FileUtil.getExtension(fileName) + ); + const blob = rawBlob.slice(0, rawBlob.size, mimeType); + FileUtil.fromBlobToBuffer(blob) + .then(buffer => { + /** download check */ + this.fileProtocolService + .downCheck({ + seq: req.attachmentsSeq + }) + .pipe(take(1)) + .subscribe(); + + this.saveFile(buffer, fileName, mimeType, savePath); + }) + .catch(reason => { + this.fileTalkDownloadError(reason); + }); + }), + finalize(() => { + if (!!fileDownloadItem) { + setTimeout(() => { + fileDownloadItem.downloadingProgress$ = undefined; + }, 1000); + } + }), + catchError(error => of(error)) + ) + .subscribe(); + } else if (!!directDownloadUrl) { + this.commonApiService + .fileDownload(directDownloadUrl, fileDownloadItem) + .pipe( + take(1), + map(rawBlob => { + const mimeType = MimeUtil.getMimeFromExtension( + FileUtil.getExtension(fileName) + ); + const blob = rawBlob.slice(0, rawBlob.size, mimeType); + FileUtil.fromBlobToBuffer(blob) + .then(buffer => { + this.saveFile(buffer, fileName, mimeType, savePath); + }) + .catch(reason => { + this.fileTalkDownloadError(reason); + }); + }), + finalize(() => { + if (!!fileDownloadItem) { + setTimeout(() => { + fileDownloadItem.downloadingProgress$ = undefined; + }, 1000); + } + }), + catchError(error => of(error)) + ) + .subscribe(); + } + } + + private saveFile( + buffer: Buffer, + fileName: string, + mimeType: string, + savePath: string + ): void { + this.nativeService + .saveFile(buffer, fileName, mimeType, savePath) + .then(filePath => { + if (!!filePath) { + const snackBarRef = this.snackBarService.open( + this.translateService.instant('common.file.results.savedToPath', { + path: filePath + }), + this.translateService.instant('common.file.open'), + { + duration: 3000, + verticalPosition: 'bottom', + horizontalPosition: 'center' + } + ); + + snackBarRef.onAction().subscribe(() => { + snackBarRef.dismiss(); + this.ngZone.runOutsideAngular(() => { + this.nativeService.openTargetItem(filePath).catch(reason => { + this.logger.warn(reason); + }); + }); + }); + } else { + this.fileTalkDownloadError('fail'); + } + }) + .catch(reason => { + this.fileTalkDownloadError(reason); + }); + } + + private fileTalkDownloadError(reason: any): void { + this.logger.warn(reason); + this.snackBarService.openFromComponent< + AlertSnackbarComponent, + AlertSnackbarData + >(AlertSnackbarComponent, { + data: { + html: this.translateService.instant('common.file.errors.failToSave'), + buttonText: this.translateService.instant('common.file.errors.label') + } + }); + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/mass-text.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/mass-text.event-json.ts new file mode 100644 index 0000000..ffb9904 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/mass-text.event-json.ts @@ -0,0 +1,33 @@ +import { StatusCode, JsonAnalization } from '@ucap-webmessenger/api'; +import { EventJsonDecoder } from './event-json'; + +export interface MassTextEventJson { + statusCode?: StatusCode; + errorMessage?: string; + roomSeq?: number; + massSeq?: number; + regDate?: string; + content?: string; +} + +export const decodeMassTextEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + statusCode: json.StatusCode, + errorMessage: json.ErrorMessage, + roomSeq: Number(json.RoomID), + massSeq: Number(json.EventMassSeq), + regDate: json.RegDate, + content: json.Content + } as MassTextEventJson; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e.toString() + } as MassTextEventJson; + } +}; diff --git a/documents/업무/2월/3째주/file-viewer-prj/mass-translation.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/mass-translation.event-json.ts new file mode 100644 index 0000000..1997c30 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/mass-translation.event-json.ts @@ -0,0 +1,37 @@ +import { StatusCode, JsonAnalization } from '@ucap-webmessenger/api'; +import { EventJsonDecoder } from './event-json'; + +export interface MassTranslationEventJson { + statusCode?: StatusCode; + errorMessage?: string; + translationSeq?: number; + destLocale?: string; + roomSeq?: number; + regDate?: string; + original?: string; + translation?: string; +} + +export const decodeMassTranslationEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + statusCode: json.StatusCode, + errorMessage: json.ErrorMessage, + translationSeq: Number(json.EventTransSeq), + destLocale: json.DestLocale, + roomSeq: Number(json.RoomID), + regDate: json.RegDate, + original: json.Original, + translation: json.Translation + } as MassTranslationEventJson; + } catch (e) { + return { + statusCode: StatusCode.Fail, + errorMessage: e.toString() + } as MassTranslationEventJson; + } +}; diff --git a/documents/업무/2월/3째주/file-viewer-prj/message-box.component.ts b/documents/업무/2월/3째주/file-viewer-prj/message-box.component.ts new file mode 100644 index 0000000..5a0d6e3 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/message-box.component.ts @@ -0,0 +1,205 @@ +import { + Component, + OnInit, + Input, + EventEmitter, + Output, + AfterViewInit, + ElementRef, + ViewChild, + ChangeDetectorRef +} from '@angular/core'; + +import { shakeAnimation } from 'angular-animations'; + +import { + Info, + EventType, + InfoResponse, + EventJson, + FileEventJson, + MassTranslationEventJson +} from '@ucap-webmessenger/protocol-event'; +import { NGXLogger } from 'ngx-logger'; +import moment from 'moment'; +import { FileDownloadItem } from '@ucap-webmessenger/api'; +import { RoomInfo } from '@ucap-webmessenger/protocol-room'; +import { SelectFileInfo } from '@ucap-webmessenger/ui'; + +@Component({ + selector: 'ucap-chat-message-box', + templateUrl: './message-box.component.html', + styleUrls: ['./message-box.component.scss'], + animations: [shakeAnimation()] +}) +export class MessageBoxComponent implements OnInit, AfterViewInit { + @Input() + message: Info; + + @Input() + mine = false; + + @Input() + highlight = false; + + @Input() + existReadToHere = false; + + @Input() + dateChanged = false; + + @Input() + senderName: string; + + @Input() + profileImageRoot: string; + + @Input() + profileImage: string; + + @Input() + roomInfo: RoomInfo; + + @Input() + translationSimpleview = false; + + @Input() + unreadCount: number; + + @Output() + openProfile = 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; + }>(); + + @ViewChild('mbContainer', { static: true }) + mbContainer: ElementRef; + + @ViewChild('mbChatRow', { static: true }) + mbChatRow: ElementRef; + + EventType = EventType; + + moment = moment; + + firstEventSeq = 0; + existReadHere = false; + shakeIt = false; + + get offsetTop() { + return this.mbChatRow.nativeElement.offsetTop; + } + + constructor( + private elementRef: ElementRef, + private changeDetectorRef: ChangeDetectorRef, + private logger: NGXLogger + ) {} + + ngOnInit() { + this.mbContainer.nativeElement.classList.add('hide'); + } + + ngAfterViewInit(): void { + this.elementRef.nativeElement.style.height = `${this.mbContainer.nativeElement.offsetHeight}px`; + this.elementRef.nativeElement.style.maxHeight = `${this.mbContainer.nativeElement.offsetHeight}px`; + this.mbContainer.nativeElement.classList.remove('hide'); + } + + /** + * 정보성 Event 인지 판단. + * @description 정보성 event 일 경우 프로필, 일시 를 표현하지 않는다. + * Edit with reducers.ts / sync / updateRoomForNewEventMessage + */ + isInformation(info: Info) { + if ( + info.type === EventType.Join || + info.type === EventType.Exit || + info.type === EventType.ForcedExit || + info.type === EventType.RenameRoom || + info.type === EventType.NotificationForiOSCapture || + info.type === EventType.NotificationForTimerRoom || + info.type === EventType.GuideForRoomTimerChanged + ) { + return true; + } + return false; + } + + onClickOpenProfile(event: MouseEvent, userSeq: number) { + event.preventDefault(); + event.stopPropagation(); + + this.openProfile.emit(userSeq); + } + + /** [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: 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: any, message: Info) { + if ( + message.type === EventType.Translation || + message.type === EventType.MassTranslation + ) { + this.contextMenu.emit({ event: event.event, message, type: event.type }); + } else { + this.contextMenu.emit({ event, message }); + } + } + + shake() { + this.shakeIt = false; + this.changeDetectorRef.detectChanges(); + setTimeout(() => { + this.shakeIt = true; + this.changeDetectorRef.detectChanges(); + }, 1); + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/messages.component copy.ts b/documents/업무/2월/3째주/file-viewer-prj/messages.component copy.ts new file mode 100644 index 0000000..bec29e5 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/messages.component copy.ts @@ -0,0 +1,2158 @@ +import { + Component, + OnInit, + OnDestroy, + ViewChild, + AfterViewInit, + Output, + EventEmitter, + Inject, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { + ucapAnimations, + SnackBarService, + ClipboardService, + DialogService, + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult, + AlertDialogComponent, + AlertDialogData, + AlertDialogResult, + FileUploadQueueComponent, + StringUtil, + SelectFileInfo +} from '@ucap-webmessenger/ui'; +import { Store, select } from '@ngrx/store'; +import { NGXLogger } from 'ngx-logger'; +import { + Observable, + Subscription, + forkJoin, + of, + BehaviorSubject, + combineLatest +} from 'rxjs'; + +import { + Info, + EventType, + isRecalled, + isCopyable, + isRecallable, + isForwardable, + InfoResponse, + EventJson, + FileEventJson, + StickerEventJson, + MassTextEventJson, + TranslationEventJson, + MassTranslationEventJson +} from '@ucap-webmessenger/protocol-event'; + +import * as AppStore from '@app/store'; +import * as EventStore from '@app/store/messenger/event'; +import * as ChatStore from '@app/store/messenger/chat'; +import * as RoomStore from '@app/store/messenger/room'; +import * as SyncStore from '@app/store/messenger/sync'; +import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; +import { + SessionStorageService, + LocalStorageService +} from '@ucap-webmessenger/web-storage'; +import { + EnvironmentsInfo, + KEY_ENVIRONMENTS_INFO, + UserSelectDialogType, + RightDrawer, + KEY_STICKER_HISTORY +} from '@app/types'; +import { + RoomInfo, + UserInfo, + RoomType, + UserInfoShort +} from '@ucap-webmessenger/protocol-room'; +import { take, map, catchError, tap } from 'rxjs/operators'; +import { + FormComponent as UCapUiChatFormComponent, + MessagesComponent as UCapUiChatMessagesComponent +} from '@ucap-webmessenger/ui-chat'; +import { KEY_VER_INFO } from '@app/types'; +import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; +import { + MatMenuTrigger, + MatSnackBarRef, + SimpleSnackBar, + MatCheckbox +} from '@angular/material'; +import { FileUploadItem, FileDownloadItem } from '@ucap-webmessenger/api'; +import { + CommonApiService, + FileTalkSaveRequest, + FileTalkSaveResponse, + FileTalkSaveMultiRequest +} from '@ucap-webmessenger/api-common'; +import { + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult +} from '../dialogs/chat/create-chat.dialog.component'; +import { + FileViewerDialogComponent, + FileViewerDialogData, + FileViewerDialogResult +} from '@app/layouts/common/dialogs/file-viewer.dialog.component'; +import { FileUtil, StickerFilesInfo } from '@ucap-webmessenger/core'; + +import { StatusCode } from '@ucap-webmessenger/api'; +import { + EditChatRoomDialogComponent, + EditChatRoomDialogResult, + EditChatRoomDialogData +} from '../dialogs/chat/edit-chat-room.dialog.component'; +import { + SelectGroupDialogComponent, + SelectGroupDialogResult, + SelectGroupDialogData +} from '../dialogs/group/select-group.dialog.component'; +import { GroupDetailData } from '@ucap-webmessenger/protocol-sync'; +import { environment } from '../../../../environments/environment'; +import { + MassDetailComponent, + MassDetailDialogData +} from '../dialogs/chat/mass-detail.component'; +import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native'; +import { TranslateService } from '@ngx-translate/core'; +import { TranslatePipe } from 'projects/ucap-webmessenger-ui/src/lib/pipes/translate.pipe'; +import { TranslateService as UiTranslateService } from '@ucap-webmessenger/ui'; + +import { + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult +} from '../dialogs/chat/clipboard.dialog.component'; +import { AppFileService } from '@app/services/file.service'; +import { FileInfo } from '@ucap-webmessenger/protocol-file'; + +@Component({ + selector: 'app-layout-messenger-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.scss'], + animations: ucapAnimations, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { + @Output() + openProfile = new EventEmitter<{ + userSeq: number; + }>(); + @Output() + closeRightDrawer = new EventEmitter(); + + @ViewChild('chatForm', { static: false }) + private chatForm: UCapUiChatFormComponent; + + @ViewChild('messageContextMenuTrigger', { static: true }) + messageContextMenuTrigger: MatMenuTrigger; + messageContextMenuPosition = { x: '0px', y: '0px' }; + + @ViewChild('fileUploadQueue', { static: true }) + fileUploadQueue: FileUploadQueueComponent; + + @ViewChild('chatMessages', { static: true }) + chatMessages: UCapUiChatMessagesComponent; + + @ViewChild('choiceBundleImage', { static: false }) + choiceBundleImage: MatCheckbox; + + environmentsInfo: EnvironmentsInfo; + + loginResSubscription: Subscription; + loginResSubject = new BehaviorSubject(undefined); + + roomInfoSubscription: Subscription; + roomInfoSubject = new BehaviorSubject(undefined); + + userInfoListSubscription: Subscription; + userInfoListSubject = new BehaviorSubject(undefined); + + eventListSubscription: Subscription; + eventListSubject = new BehaviorSubject[]>(undefined); + eventListNewSubject = new BehaviorSubject[]>(undefined); + + eventInfoStatusSubscription: Subscription; + eventInfoStatusSubject = new BehaviorSubject(undefined); + + eventRemainedSubscription: Subscription; + eventRemainedSubject = new BehaviorSubject(false); // 이전대화가 남아 있는지 여부 + + fileInfoListSubscription: Subscription; + fileInfoListSubject = new BehaviorSubject([]); + + lockSubject = new BehaviorSubject(false); + + sessionVerInfo: VersionInfo2Response; + + baseEventSeq = 0; + eventListProcessing$: Observable; + searchEventListProcessing: boolean; + searchEventListProcessingSubscription: Subscription; + + isRecalledMessage = isRecalled; + isCopyableMessage = isCopyable; + isRecallableMessage = isRecallable; + isForwardableMessage = isForwardable; + + /** Timer 대화방의 대화 삭제를 위한 interval */ + interval: any; + + /** About Searching */ + isShowSearchArea = false; + moreSearchProcessing = false; + searchText = ''; + searchedListSubject = new BehaviorSubject[]>(undefined); + searchedFocusEvent: Info; + searchTotalCount = 0; + searchCurrentIndex = 0; + + /** About Sticker */ + isShowStickerSelector = false; + selectedSticker: StickerFilesInfo; + + /** 번역기능 비활성화 2020-02-07 + * About Translation + **/ + // isTranslationProcess = false; + // isShowTranslation = false; + // translationSimpleview = false; + // translationPreview = false; + // destLocale = 'en'; // default English :: en + // translationPreviewInfo: { + // previewInfo: TranslationSaveResponse | null; + // translationType: EventType.Translation | EventType.MassTranslation; + // }; + + /** About ReadHere */ + firstCheckReadHere = true; + clearReadHere = false; + initRoomLastEventSeq: number; + isImageOnlyForFileUpload = false; + bundleFileUploadItem: FileUploadItem; + bundleFileUploadItems: FileUploadItem[]; + + snackBarPreviewEvent: MatSnackBarRef; + + RoomType = RoomType; + environment = environment; + + constructor( + private store: Store, + private sessionStorageService: SessionStorageService, + private localStorageService: LocalStorageService, + private commonApiService: CommonApiService, + private clipboardService: ClipboardService, + private uiTranslateService: UiTranslateService, + private translateService: TranslateService, + private changeDetectorRef: ChangeDetectorRef, + private dialogService: DialogService, + private snackBarService: SnackBarService, + @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, + private appFileService: AppFileService, + private logger: NGXLogger + ) { + this.sessionVerInfo = this.sessionStorageService.get( + KEY_VER_INFO + ); + + this.environmentsInfo = this.sessionStorageService.get( + KEY_ENVIRONMENTS_INFO + ); + } + + ngOnInit() { + this.loginResSubscription = this.store + .pipe(select(AppStore.AccountSelector.AuthenticationSelector.loginRes)) + .subscribe(loginRes => { + this.loginResSubject.next(loginRes); + }); + + this.roomInfoSubscription = this.store + .pipe(select(AppStore.MessengerSelector.RoomSelector.roomInfo)) + .subscribe(roomInfo => { + if ( + !this.roomInfoSubject.value || + (!!this.roomInfoSubject.value && + !!roomInfo && + this.roomInfoSubject.value.roomSeq !== roomInfo.roomSeq) + ) { + this.clearView(); + + if (!!this.roomInfoSubject.value && !!this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + + if ( + !!this.roomInfoSubject.value && + !!this.roomInfoSubject.value.isTimeRoom + ) { + this.interval = setInterval(() => { + this.store.dispatch(EventStore.infoIntervalClear({})); + }, 1000); + } + + this.readyToReply(); + } + + this.roomInfoSubject.next(roomInfo); + }); + + this.userInfoListSubscription = this.store + .pipe(select(AppStore.MessengerSelector.RoomSelector.selectUserinfolist)) + .subscribe(userInfoList => { + this.userInfoListSubject.next(userInfoList); + }); + + this.eventListProcessing$ = this.store.pipe( + select(AppStore.MessengerSelector.EventSelector.infoListProcessing) + ); + + this.searchEventListProcessingSubscription = this.store + .pipe( + select( + AppStore.MessengerSelector.EventSelector.infoSearchListProcessing + ) + ) + .subscribe(process => { + this.lockSubject.next(process); + this.searchEventListProcessing = process; + if (!process && this.isShowSearchArea) { + this.doSearchTextInEvent(this.searchText); + this.snackBarService.open( + this.translateService.instant('chat.searchEventByTextEnd'), + this.translateService.instant('common.messages.confirm'), + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + + this.eventRemainedSubscription = this.store + .pipe(select(AppStore.MessengerSelector.EventSelector.remainInfo)) + .subscribe(remained => { + this.eventRemainedSubject.next(remained); + }); + + // [Daesang] + this.eventListSubscription = this.store + .pipe(select(AppStore.MessengerSelector.EventSelector.selectAllInfoList)) + .subscribe(infoList => { + if ( + !!this.eventListSubject.value && + this.eventListSubject.value.length > 0 + ) { + this.eventListNewSubject.next( + infoList.filter(info => { + if ( + info.seq <= + this.eventListSubject.value[ + this.eventListSubject.value.length - 1 + ].seq + ) { + return false; + } + return true; + }) + ); + + if ( + !!infoList && + infoList.length > 0 && + !!this.roomInfoSubject.value && + !!this.roomInfoSubject.value.lastReadEventSeq && + this.baseEventSeq <= this.roomInfoSubject.value.lastReadEventSeq + ) { + // 조회된 내용중에 read here 가 있을 경우. + this.firstCheckReadHere = false; + } + } + + this.eventListSubject.next(infoList); + + if (this.moreSearchProcessing) { + const baseseq = this.baseEventSeq; + // setTimeout(() => { + // this.doSearchTextInEvent(this.searchText, baseseq); + // }, 800); + this.baseEventSeq = infoList[0].seq; + } else { + if (!!infoList && infoList.length > 0) { + this.baseEventSeq = infoList[0].seq; + } + } + }); + + // // [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)) + .subscribe(res => { + this.eventInfoStatusSubject.next(res); + + if (!!res) { + const elist = this.eventListSubject.value; + if (res.baseSeq === 0 && elist.length > 0) { + this.initRoomLastEventSeq = elist[elist.length - 1].seq; + } + } + }); + + this.fileInfoListSubscription = combineLatest([ + this.store.pipe(select(AppStore.MessengerSelector.RoomSelector.roomInfo)), + this.store.pipe( + select(AppStore.MessengerSelector.EventSelector.selectAllFileInfoList) + ) + ]) + .pipe( + tap(() => {}), + tap(([roomInfo, fileInfoList]) => { + // tslint:disable-next-line: variable-name + const _fileInfoList = fileInfoList.filter(fileInfo => { + if (!!roomInfo && fileInfo.roomSeq === roomInfo.roomSeq) { + return true; + } else { + return false; + } + }); + + this.fileInfoListSubject.next(_fileInfoList); + }) + ) + .subscribe(); + } + + ngOnDestroy(): void { + if (!!this.loginResSubscription) { + this.loginResSubscription.unsubscribe(); + } + if (!!this.roomInfoSubscription) { + this.roomInfoSubscription.unsubscribe(); + } + if (!!this.userInfoListSubscription) { + this.userInfoListSubscription.unsubscribe(); + } + if (!!this.eventListSubscription) { + this.eventListSubscription.unsubscribe(); + } + if (!!this.eventInfoStatusSubscription) { + this.eventInfoStatusSubscription.unsubscribe(); + } + if (!!this.eventRemainedSubscription) { + this.eventRemainedSubscription.unsubscribe(); + } + if (!!this.searchEventListProcessingSubscription) { + this.searchEventListProcessingSubscription.unsubscribe(); + } + if (!!this.fileInfoListSubscription) { + this.fileInfoListSubscription.unsubscribe(); + } + + if (!!this.interval) { + clearInterval(this.interval); + } + } + + ngAfterViewInit(): void { + // this.readyToReply(); + } + + /** + * 채팅방의 여러 팝업들을 닫아준다. + */ + clearView() { + // Right Drawer closed.. + this.closeRightDrawer.emit(); + + // Sticker Selector Clear.. + this.isShowStickerSelector = false; + this.selectedSticker = undefined; + + // Chat Search Clear.. + this.onCloseSearchArea(); + + // 번역기능 비활성화 2020-02-07 + // Translate Clear.. + // this.isTranslationProcess = false; + // this.isShowTranslation = false; + // this.translationSimpleview = false; + // this.translationPreview = false; + // this.destLocale = 'en'; // default English :: en + // this.translationPreviewInfo = null; + + // Read here Clear.. + this.firstCheckReadHere = true; + + // Chat Formfield Clear.. + if (!!this.chatForm) { + this.chatForm.replyInput.nativeElement.value = ''; + } + } + + get _roomUserInfos() { + if (this.roomInfoSubject.value.roomType === RoomType.Single) { + return this.userInfoListSubject.value.filter(roomUserInfo => { + return this.loginResSubject.value.userSeq !== roomUserInfo.seq; + }); + } else { + return this.userInfoListSubject.value + .filter(roomUserInfo => { + return ( + this.loginResSubject.value.userSeq !== roomUserInfo.seq && + roomUserInfo.isJoinRoom + ); + }) + .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + } + } + + getRoomNameByRoomUser(roomUserInfos: (UserInfo | UserInfoShort)[]) { + let roomName = new TranslatePipe( + this.uiTranslateService, + this.changeDetectorRef + ).transform(roomUserInfos, 'name', ','); + + if (!roomName || roomName.trim().length === 0) { + roomName = this.translateService.instant('chat.noRoomUser'); + } + return roomName; + } + + /** 대화전송 가능한 방인지 판단 */ + getEnableSend() { + if (!this.roomInfoSubject.value) { + return false; + } + + if ( + [ + RoomType.Bot, + RoomType.Allim, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + + return true; + } + + getConvertTimer(timerInterval: number, unit: number = 1) { + if (timerInterval >= 0 && timerInterval < 60 * unit) { + return Math.floor((timerInterval / 1) * unit) + ' 초'; + } else if (timerInterval >= 60 * unit && timerInterval < 3600 * unit) { + return Math.floor(((timerInterval / 1) * unit) / 60) + ' 분'; + } else if (timerInterval >= 3600 * unit && timerInterval <= 86400 * unit) { + return Math.floor(((timerInterval / 1) * unit) / 60 / 60) + ' 시간'; + } else { + return ''; + } + } + + getShowContextMenu(menuType: string) { + if ( + ['OPEN_ROOM_USER', 'ADD_MEMBER', 'ADD_GROUP', 'EDIT_ROOM'].some( + v => v === menuType + ) + ) { + if ( + !this.roomInfoSubject.value || + !this.roomInfoSubject.value.roomType || + [ + RoomType.Mytalk, + RoomType.Allim, + RoomType.Bot, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + } + + return true; + } + + getShowUnreadCount(): boolean { + if ( + !this.roomInfoSubject.value || + this.roomInfoSubject.value === undefined + ) { + return true; + } + if ( + [ + RoomType.Mytalk, + RoomType.Allim, + RoomType.Bot, + RoomType.Link, + RoomType.Allim_Elephant, + RoomType.Allim_TMS + ].some(v => v === this.roomInfoSubject.value.roomType) + ) { + return false; + } + + return true; + } + + readyToReply(): void { + setTimeout(() => { + this.focusReplyInput(); + }); + } + + focusReplyInput(): void { + setTimeout(() => { + if (!!this.chatForm) { + this.chatForm.focus(); + } + }); + } + + onScrollupMessages(event: any) {} + onYReachStartMessages(event: any) { + // 자동 스크롤이 아닌 버튼 방식으로 변경. + // this.onMoreEvent(this.baseEventSeq); + } + onYReachEndMessages(event: any) { + this.chatMessages.initEventMore(); + if (!!this.snackBarPreviewEvent) { + this.snackBarPreviewEvent.dismiss(); + } + + // // clear readHere object.. 정책상 클리어 하지 않도록 함. + // if (!this.firstCheckReadHere) { + // this.clearReadHere = true; + // } + } + + /** More Event */ + onMoreEvent(seq: number) { + this.store.dispatch( + EventStore.info({ + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount + }) + ); + } + + /** Send Event */ + async onSendMessage(message: string) { + if (!!this.bundleFileUploadItems) { + if (this.choiceBundleImage.checked) { + await this.uploadBundleFile(); + } else { + this.bundleFileUploadItem = undefined; + await this.uploadFiles(this.bundleFileUploadItems).finally(() => { + this.bundleFileUploadItems = undefined; + }); + } + } + + this.chatMessages.initEventMore(); + + if (!this.selectedSticker) { + if (!message || message.trim().length === 0) { + // const result = await this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // width: '360px', + // data: { + // title: this.translateService.instant('chat.errors.label'), + // message: this.translateService.instant( + // 'chat.errors.inputChatMessage' + // ) + // } + // }); + return; + } + } + + // 번역기능 비활성화 2020-02-07 + // if (!!this.isShowTranslation && this.destLocale.trim().length > 0) { + // /** CASE : Translation */ + // // 번역할 대화 없이 스티커만 전송할 경우. + // if (!message || message.trim().length === 0) { + // this.sendMessageOfSticker(message); + // } else { + // this.sendMessageOfTranslate(message); + // } + // } else + + if (!!this.selectedSticker) { + /** CASE : Sticker */ + this.sendMessageOfSticker(message); + } else if ( + message.trim().length > + environment.productConfig.CommonSetting.masstextLength + ) { + /** CASE : MASS TEXT */ + this.sendMessageOfMassText(message); + } else { + /** CASE : Normal Text */ + this.sendMessageOfNormal(message); + } + } + + /** Send Normal message */ + sendMessageOfNormal(message: string) { + this.store.dispatch( + EventStore.send({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.Character, + sentMessage: StringUtil.escapeHtml(message) + } + }) + ); + } + /** Send Sticker message */ + async sendMessageOfSticker(message: string, isCheck: boolean = true) { + if ( + !!isCheck && + !!message && + message.trim().length > + environment.productConfig.CommonSetting.masstextLength + ) { + const result = await this.dialogService.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + width: '360px', + data: { + title: this.translateService.instant('chat.errors.label'), + message: this.translateService.instant( + 'chat.errors.maxLengthOfMassText', + { + maxLength: environment.productConfig.CommonSetting.masstextLength + } + ) + } + }); + return; + } + + const stickerJson: StickerEventJson = { + name: '스티커', + file: this.selectedSticker.index, + chat: !!message ? StringUtil.escapeHtml(message.trim()) : '' + }; + this.store.dispatch( + EventStore.send({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.Sticker, + sentMessage: JSON.stringify(stickerJson) + } + }) + ); + this.isShowStickerSelector = false; + this.setStickerHistory(this.selectedSticker); + this.selectedSticker = null; + } + /** Send Masstext message */ + sendMessageOfMassText(message: string) { + this.store.dispatch( + EventStore.sendMass({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + eventType: EventType.MassText, + // sentMessage: message.replace(/\n/gi, '\r\n') + sentMessage: StringUtil.escapeHtml(message) + } + }) + ); + } + /** + * 번역기능 비활성화 2020-02-07 + * Send Translation message + **/ + // sendMessageOfTranslate(message: string) { + // const destLocale = this.destLocale; + // const original = StringUtil.escapeHtml(message); + // const roomSeq = this.roomInfoSubject.value.roomSeq; + + // if (!!this.isTranslationProcess) { + // return; + // } + + // this.isTranslationProcess = true; + // this.commonApiService + // .translationSave({ + // userSeq: this.loginResSubject.value.userSeq, + // deviceType: this.environmentsInfo.deviceType, + // token: this.loginResSubject.value.tokenString, + // roomSeq, + // original, + // srcLocale: '', + // destLocale + // } as TranslationSaveRequest) + // .pipe( + // take(1), + // map(res => { + // if (res.statusCode === StatusCode.Success) { + // let sentMessage = ''; + // let eventType = EventType.Translation; + // let previewObject: TranslationEventJson | MassTranslationEventJson; + + // if (res.translationSeq > 0) { + // // Mass Text Translation + // previewObject = res; + // sentMessage = res.returnJson; + // eventType = EventType.MassTranslation; + // } else { + // // Normal Text Translation + // previewObject = { + // locale: destLocale, + // original, + // translation: res.translation, + // stickername: '', + // stickerfile: !!this.selectedSticker + // ? this.selectedSticker.index + // : '' + // }; + + // sentMessage = JSON.stringify(previewObject); + // eventType = EventType.Translation; + // } + + // if (!!this.translationPreview) { + // // preview + // this.translationPreviewInfo = { + // previewInfo: res, + // translationType: eventType + // }; + // } else { + // // direct send + // this.store.dispatch( + // EventStore.send({ + // senderSeq: this.loginResSubject.value.userSeq, + // req: { + // roomSeq, + // eventType, + // sentMessage + // } + // }) + // ); + + // if (!!this.translationPreviewInfo) { + // this.translationPreviewInfo = null; + // } + // } + + // if (!!this.selectedSticker) { + // this.isShowStickerSelector = false; + // this.setStickerHistory(this.selectedSticker); + // this.selectedSticker = null; + // } + // } else { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // width: '360px', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // this.logger.error('res', res); + // } + // }), + // catchError(error => { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // width: '360px', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // return of(this.logger.error('error', error)); + // }) + // ) + // .subscribe(() => { + // this.isTranslationProcess = false; + // }); + // } + + onClickReceiveAlarm() { + this.store.dispatch( + RoomStore.updateOnlyAlarm({ roomInfo: this.roomInfoSubject.value }) + ); + } + + /** MassText Detail View */ + onMassDetail(value: number) { + this.store.dispatch( + ChatStore.selectedMassDetail({ + massEventSeq: value + }) + ); + } + + onMassTranslationDetail(params: { + message: Info; + contentsType: string; + }) { + this.commonApiService + .transMassTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventTransSeq: params.message.sentMessageJson.translationSeq.toString() + }) + .pipe(take(1)) + .subscribe(res => { + let contents = ''; + if (res.statusCode === StatusCode.Success) { + contents = + params.contentsType === 'T' ? res.translation : res.original; + } else { + contents = + params.contentsType === 'T' + ? params.message.sentMessageJson.translation + : params.message.sentMessageJson.original; + } + + this.dialogService.open( + MassDetailComponent, + { + disableClose: false, + width: '550px', + data: { + title: this.translateService.instant('chat.detailView'), + contents + } + } + ); + }); + } + + async onFileViewer(selectFileInfo: SelectFileInfo) { + const result = await this.dialogService.open< + FileViewerDialogComponent, + FileViewerDialogData, + FileViewerDialogResult + >(FileViewerDialogComponent, { + position: { + top: '50px' + }, + maxWidth: '100vw', + maxHeight: '100vh', + height: 'calc(100% - 50px)', + width: '100%', + hasBackdrop: false, + panelClass: 'app-dialog-full', + data: { + fileInfos: this.fileInfoListSubject.value, + selectFileInfo, + downloadUrl: this.sessionVerInfo.downloadUrl, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + userSeq: this.loginResSubject.value.userSeq + } + }); + } + + /** File Save, Save As */ + onSave(value: { + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }) { + this.logger.debug('fileSave', value); + if (value.type === 'saveAs') { + this.nativeService + .selectSaveFilePath(value.fileInfo.fileName) + .then(result => { + if (!result) { + return; + } + + if (result.canceled) { + // this.snackBarService.open( + // this.translateService.instant('common.file.results.canceled'), + // this.translateService.instant('common.file.errors.label'), + // { + // duration: 1000 + // } + // ); + } else { + this.saveFile(value, result.filePath); + } + }) + .catch(reason => { + this.snackBarService.open( + this.translateService.instant( + 'common.file.errors.failToSpecifyPath' + ), + this.translateService.instant('common.file.errors.label') + ); + }); + } else { + this.saveFile(value); + } + } + + onFileDragEnter(items: DataTransferItemList) { + this.clearView(); + this.logger.debug('onFileDragEnter', items); + } + + onFileDragOver() { + this.logger.debug('onFileDragOver'); + } + + onFileDragLeave() { + this.logger.debug('onFileDragLeave'); + } + + onExistNewMessage(info: Info) { + let message = ''; + + const contents = StringUtil.convertFinalEventMessage( + info.type, + info.sentMessageJson || info.sentMessage + ); + + if (!!contents) { + const senderUser = this.userInfoListSubject.value.filter( + user => user.seq === info.senderSeq + ); + if (!!senderUser && senderUser.length > 0) { + message += `${senderUser[0].name} : `; + } + message += contents; + + this.snackBarPreviewEvent = this.snackBarService.open( + message, + this.translateService.instant('common.messages.confirm'), + { + // duration: 3000, + verticalPosition: 'bottom', + horizontalPosition: 'center', + panelClass: ['chat-snackbar-class'] + } + ); + this.snackBarPreviewEvent.onAction().subscribe(() => { + this.chatMessages.initEventMore(); + this.chatMessages.scrollToBottom(); + this.snackBarPreviewEvent.dismiss(); + }); + } + } + + saveFile( + value: { + fileInfo: FileEventJson; + fileDownloadItem: FileDownloadItem; + type: string; + }, + savePath?: string + ) { + this.appFileService.fileTalkDownlod({ + req: { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + attachmentsSeq: value.fileInfo.attachmentSeq, + fileDownloadItem: value.fileDownloadItem + }, + fileName: value.fileInfo.fileName, + savePath + }); + } + + async onFileSelected(fileUploadItems: FileUploadItem[]) { + this.logger.debug('onFileSelected', fileUploadItems); + this.clearView(); + + const fileAllowSize = + !!this.sessionVerInfo && this.sessionVerInfo.fileAllowSize + ? this.sessionVerInfo.fileAllowSize + : environment.productConfig.CommonSetting.defaultFileAllowSize; + + if (fileAllowSize > 0) { + if ( + fileUploadItems.filter( + fui => fui.file.size > fileAllowSize * 1024 * 1024 + ).length + ) { + this.snackBarService.open( + this.translateService.instant('common.file.errors.oversize', { + maxSize: fileAllowSize + }), + '', + { + duration: 1000, + verticalPosition: 'bottom', + horizontalPosition: 'center' + } + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + return; + } + } + + const checkExt = this.commonApiService.acceptableExtensionForFileTalk( + fileUploadItems.map(fui => FileUtil.getExtension(fui.file.name)) + ); + if (!checkExt.accept) { + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + + this.snackBarService.open( + this.translateService.instant('common.file.errors.notSupporedType', { + supporedType: + checkExt.reject.length > 0 ? checkExt.reject.join(',') : '' + }), + '', + { + duration: 1000, + verticalPosition: 'bottom', + horizontalPosition: 'center' + } + ); + + return; + } + + this.isImageOnlyForFileUpload = this.isImageOnly(fileUploadItems); + + if (this.isImageOnlyForFileUpload) { + this.bundleFileUploadItem = FileUploadItem.from(); + this.bundleFileUploadItems = fileUploadItems; + return; + } + this.bundleFileUploadItem = undefined; + this.bundleFileUploadItems = undefined; + + await this.uploadFiles(fileUploadItems); + } + + private uploadFiles(fileUploadItems: FileUploadItem[]): Promise { + // 일반 업로드 + return new Promise(async (resolve, reject) => { + const allObservables: Observable[] = []; + + for (const fileUploadItem of fileUploadItems) { + let thumbnail: File; + if ( + -1 !== + [ + '3gp', + 'avi', + 'm4v', + 'mkv', + 'mov', + 'mp4', + 'mpeg', + 'mpg', + 'rv', + 'ts', + 'webm', + 'wmv' + ].indexOf(FileUtil.getExtension(fileUploadItem.file.name)) + ) { + thumbnail = await FileUtil.thumbnail(fileUploadItem.file); + this.logger.debug('thumbnail', thumbnail); + } + + const req: FileTalkSaveRequest = { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + roomSeq: this.roomInfoSubject.value.roomSeq, + file: fileUploadItem.file, + fileName: fileUploadItem.file.name, + thumb: thumbnail, + fileUploadItem + }; + + allObservables.push( + this.commonApiService.fileTalkSave(req, null).pipe( + map(res => { + if (!res) { + return; + } + if (StatusCode.Success === res.statusCode) { + return res; + } else { + throw res; + } + }) + ) + ); + } + + const info = { + senderSeq: this.loginResSubject.value.userSeq, + roomSeq: this.roomInfoSubject.value.roomSeq + }; + + forkJoin(allObservables) + .pipe(take(1)) + .subscribe( + resList => { + for (const res of resList) { + this.store.dispatch( + EventStore.send({ + senderSeq: info.senderSeq, + req: { + roomSeq: info.roomSeq, + eventType: EventType.File, + sentMessage: res.returnJson + } + }) + ); + } + resolve(); + }, + error => { + this.logger.debug('onFileSelected error', error); + this.snackBarService.open( + this.translateService.instant('common.file.errors.failToUpload'), + this.translateService.instant('common.file.errors.label') + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + reject(error); + }, + () => { + this.fileUploadQueue.onUploadComplete(); + } + ); + }); + } + + private uploadBundleFile(): Promise { + return new Promise((resolve, reject) => { + const fileList: File[] = []; + const info = { + senderSeq: this.loginResSubject.value.userSeq, + roomSeq: this.roomInfoSubject.value.roomSeq + }; + for (const fileUploadItem of this.bundleFileUploadItems) { + fileList.push(fileUploadItem.file); + } + const req: FileTalkSaveMultiRequest = { + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + roomSeq: this.roomInfoSubject.value.roomSeq, + fileUploadItem: this.bundleFileUploadItem, + files: fileList + }; + + this.commonApiService + .fileTalkSaveMulti(req, null) + .pipe( + map(res => { + if (!res) { + return; + } + if (StatusCode.Success === res.statusCode) { + return res; + } else { + throw res; + } + }) + ) + .subscribe( + res => { + this.logger.debug('fileTalkSaveMulti', res); + + this.store.dispatch( + EventStore.send({ + senderSeq: info.senderSeq, + req: { + roomSeq: info.roomSeq, + eventType: EventType.BundleImage, + sentMessage: res.returnJson + } + }) + ); + resolve(); + }, + error => { + this.logger.debug('onFileSelected error', error); + this.snackBarService.open( + this.translateService.instant('common.file.errors.failToUpload'), + this.translateService.instant('common.file.errors.label') + ); + + if (!!this.fileUploadQueue) { + this.fileUploadQueue.onUploadComplete(); + } + reject(error); + }, + () => { + this.bundleFileUploadItem = undefined; + this.bundleFileUploadItems = undefined; + + this.fileUploadQueue.onUploadComplete(); + } + ); + }); + } + + private isImageOnly(fileUploadItems: FileUploadItem[]): boolean { + if (!fileUploadItems || 0 === fileUploadItems.length) { + return false; + } + + for (const fileUploadItem of fileUploadItems) { + if (-1 === fileUploadItem.file.type.indexOf('image/')) { + return false; + } + } + return true; + } + + onContextMenuMessage(params: { + event: MouseEvent; + message: Info; + type?: string; + }) { + params.event.preventDefault(); + params.event.stopPropagation(); + + this.messageContextMenuPosition.x = params.event.clientX + 'px'; + this.messageContextMenuPosition.y = params.event.clientY + 'px'; + this.messageContextMenuTrigger.menu.focusFirstItem('mouse'); + this.messageContextMenuTrigger.menuData = { + message: params.message, + loginRes: this.loginResSubject.value, + clicktype: params.type + }; + this.messageContextMenuTrigger.openMenu(); + } + + async onClickMessageContextMenu( + menuType: string, + message: Info, + clicktype?: string + ) { + switch (menuType) { + case 'COPY': + { + switch (message.type) { + case EventType.Character: + { + if ( + this.clipboardService.copyFromContent( + (message as Info).sentMessage + ) + ) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + } + break; + case EventType.MassText: + { + this.commonApiService + .massTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventMassSeq: message.seq + }) + .pipe(take(1)) + .subscribe(res => { + if (this.clipboardService.copyFromContent(res.content)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + } + break; + case EventType.Translation: + { + let trgtStr = ''; + if (clicktype === 'translation') { + // translation + trgtStr = (message.sentMessageJson as TranslationEventJson) + .translation; + } else { + // original + trgtStr = (message.sentMessageJson as TranslationEventJson) + .original; + } + + if (this.clipboardService.copyFromContent(trgtStr)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + } + break; + case EventType.MassTranslation: + { + const sentMessageJson: MassTranslationEventJson = message.sentMessageJson as MassTranslationEventJson; + this.commonApiService + .transMassTalkDownload({ + userSeq: this.loginResSubject.value.userSeq, + deviceType: this.environmentsInfo.deviceType, + token: this.loginResSubject.value.tokenString, + eventTransSeq: sentMessageJson.translationSeq.toString() + }) + .pipe(take(1)) + .subscribe(res => { + let contents = ''; + if (res.statusCode === StatusCode.Success) { + contents = + clicktype === 'translation' + ? res.translation + : res.original; + } else { + contents = + clicktype === 'translation' + ? sentMessageJson.translation + : sentMessageJson.original; + } + + if (this.clipboardService.copyFromContent(contents)) { + this.snackBarService.open( + this.translateService.instant( + 'common.clipboard.results.copied' + ), + '', + { + duration: 3000, + verticalPosition: 'top', + horizontalPosition: 'center' + } + ); + } + }); + } + break; + default: + break; + } + } + break; + case 'FORWARD': + { + const result = await this.dialogService.open< + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult + >(CreateChatDialogComponent, { + width: '600px', + data: { + type: UserSelectDialogType.MessageForward, + title: this.translateService.instant('chat.forwardEventTo'), + ignoreRoom: [this.roomInfoSubject.value] + } + }); + + if (!!result && !!result.choice && result.choice) { + const userSeqs: number[] = []; + let roomSeq = ''; + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 + ) { + result.selectedUserList.map(user => userSeqs.push(user.seq)); + } + + if (!!result.selectedRoom) { + roomSeq = result.selectedRoom.roomSeq; + } + + if (userSeqs.length > 0 || roomSeq.trim().length > 0) { + this.store.dispatch( + EventStore.forward({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: '-999', + eventType: message.type, + sentMessage: message.sentMessage + }, + trgtUserSeqs: userSeqs, + trgtRoomSeq: roomSeq + }) + ); + } + } + } + break; + case 'FORWARD_TO_ME': + { + if (this.loginResSubject.value.talkWithMeBotSeq > -1) { + this.store.dispatch( + EventStore.forward({ + senderSeq: this.loginResSubject.value.userSeq, + req: { + roomSeq: '-999', + eventType: message.type, + sentMessage: message.sentMessage + }, + trgtUserSeqs: [this.loginResSubject.value.talkWithMeBotSeq] + }) + ); + } + } + break; + case 'DELETE': + { + const result = await this.dialogService.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + width: '400px', + data: { + title: this.translateService.instant('chat.removeEvent'), + html: this.translateService.instant('chat.confirmRemoveEvent') + } + }); + + if (!!result && !!result.choice && result.choice) { + this.store.dispatch( + EventStore.del({ + roomSeq: this.roomInfoSubject.value.roomSeq, + eventSeq: message.seq + }) + ); + } + } + break; + case 'RECALL': + { + const result = await this.dialogService.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + width: '400px', + data: { + title: this.translateService.instant('chat.recallEvent'), + html: this.translateService.instant('chat.confirmRecallEvent') + } + }); + + if (!!result && !!result.choice && result.choice) { + this.store.dispatch( + EventStore.cancel({ + roomSeq: this.roomInfoSubject.value.roomSeq, + eventSeq: message.seq, + deviceType: this.environmentsInfo.deviceType + }) + ); + } + } + break; + default: + break; + } + } + + async onClickContextMenu(menuType: string) { + switch (menuType) { + case 'OPEN_ALBUM_LIST': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.AlbumBox + }) + ); + } + break; + case 'OPEN_FILE_LIST': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.FileBox + }) + ); + } + break; + case 'CHAT_SEARCH': + { + this.onShowToggleSearchArea(); + } + break; + case 'OPEN_ROOM_USER': + { + this.store.dispatch( + ChatStore.selectedRightDrawer({ + req: RightDrawer.RoomUser + }) + ); + } + break; + case 'ADD_MEMBER': + { + const curRoomUser = this.userInfoListSubject.value.filter( + user => + user.seq !== this.loginResSubject.value.userSeq && user.isJoinRoom + ); + + const result = await this.dialogService.open< + CreateChatDialogComponent, + CreateChatDialogData, + CreateChatDialogResult + >(CreateChatDialogComponent, { + width: '600px', + data: { + type: UserSelectDialogType.EditChatMember, + title: this.translateService.instant('chat.modifyRoomMember'), + curRoomUser + } + }); + + if (!!result && !!result.choice && result.choice) { + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 && + curRoomUser + .map(user => user.seq) + .sort() + .join('|') === + result.selectedUserList + .map(user => user.seq) + .sort() + .join('|') + ) { + // 변경된 것이 없다면 중지. + return; + } + + // include me here.. + const userSeqs: number[] = this.userInfoListSubject.value + .filter(userInfo => userInfo.isJoinRoom) + .map(userInfo => userInfo.seq); + + if ( + !!result.selectedUserList && + result.selectedUserList.length > 0 + ) { + result.selectedUserList.forEach(user => { + if (userSeqs.indexOf(user.seq) < 0) { + userSeqs.push(user.seq); + } + }); + } + + if (userSeqs.length > 0) { + // include me + const myUserSeq = this.loginResSubject.value.userSeq; + if (!!myUserSeq && userSeqs.indexOf(myUserSeq) < 0) { + userSeqs.push(myUserSeq); + } + + this.store.dispatch( + RoomStore.inviteOrOpen({ + req: { + divCd: 'Invite', + userSeqs + } + }) + ); + } + } + } + break; + case 'ADD_GROUP': + { + const result = await this.dialogService.open< + SelectGroupDialogComponent, + SelectGroupDialogData, + SelectGroupDialogResult + >(SelectGroupDialogComponent, { + width: '600px', + data: { + title: this.translateService.instant('chat.addMemberToGroup') + } + }); + + if (!!result && !!result.choice && result.choice) { + if (!!result.group) { + const oldGroup: GroupDetailData = result.group; + const trgtUserSeq: number[] = []; + result.group.userSeqs.map(seq => trgtUserSeq.push(seq)); + this.userInfoListSubject.value + .filter(v => v.isJoinRoom) + .filter(v => result.group.userSeqs.indexOf(v.seq) < 0) + .forEach(user => { + trgtUserSeq.push(user.seq); + }); + + this.store.dispatch( + SyncStore.updateGroupMember({ + oldGroup, + trgtUserSeq + }) + ); + } + } + } + break; + case 'EDIT_ROOM': + { + const result = await this.dialogService.open< + EditChatRoomDialogComponent, + EditChatRoomDialogData, + EditChatRoomDialogResult + >(EditChatRoomDialogComponent, { + width: '600px', + data: { + title: this.translateService.instant('chat.settingsOfRoom'), + roomInfo: this.roomInfoSubject.value + } + }); + + if (!!result && !!result.choice && result.choice) { + const roomName: string = result.roomName; + const roomNameChangeTarget: string = result.roomNameChangeTarget; + const timeRoomInterval: number = result.timeRoomInterval; + const roomInfo: RoomInfo = result.roomInfo; + + // 방제목 업데이트. + this.store.dispatch( + RoomStore.update({ + req: { + roomSeq: roomInfo.roomSeq, + roomName, + receiveAlarm: roomInfo.receiveAlarm, + syncAll: + roomNameChangeTarget.toUpperCase() === 'ALL' ? true : false + } + }) + ); + + if ( + roomInfo.isTimeRoom && + timeRoomInterval > 0 && + roomInfo.timeRoomInterval !== timeRoomInterval + ) { + this.store.dispatch( + RoomStore.updateTimeRoomInterval({ + roomSeq: roomInfo.roomSeq, + timerInterval: timeRoomInterval + }) + ); + } + } + } + break; + case 'CLOSE_ROOM': + { + this.store.dispatch(ChatStore.clearSelectedRoom()); + } + break; + default: + break; + } + } + + onClickOpenProfile(userSeq: number) { + const roomType = this.roomInfoSubject.value.roomType; + if ( + roomType !== RoomType.Allim && + roomType !== RoomType.Bot && + roomType !== RoomType.Link && + roomType !== RoomType.Allim_Elephant && + roomType !== RoomType.Allim_TMS + ) { + this.openProfile.emit({ userSeq }); + } + } + + /** About Sticker */ + onShowToggleStickerSelector() { + this.isShowStickerSelector = !this.isShowStickerSelector; + if (!this.isShowStickerSelector) { + this.selectedSticker = null; + } + } + onSelectedSticker(stickerInfo: StickerFilesInfo) { + this.selectedSticker = stickerInfo; + } + setStickerHistory(sticker: StickerFilesInfo) { + const history = this.localStorageService.get(KEY_STICKER_HISTORY); + + if (!!history && history.length > 0) { + const stickers: string[] = []; + [sticker.index, ...history.filter(hist => hist !== sticker.index)].map( + (s, i) => { + if (i < 10) { + stickers.push(s); + } + } + ); + + this.localStorageService.set(KEY_STICKER_HISTORY, stickers); + } else { + this.localStorageService.set(KEY_STICKER_HISTORY, [ + sticker.index + ]); + } + } + getStickerHistory(): string[] { + return this.localStorageService.get(KEY_STICKER_HISTORY); + } + + /** About Chat Search */ + onShowToggleSearchArea() { + this.isShowSearchArea = !this.isShowSearchArea; + + if (!this.isShowSearchArea) { + this.searchedListSubject.next([]); + this.searchedFocusEvent = null; + this.searchText = ''; + } + } + onCloseSearchArea() { + this.isShowSearchArea = false; + this.searchedListSubject.next([]); + this.searchedFocusEvent = null; + this.searchText = ''; + + this.moreSearchProcessing = false; + this.searchTotalCount = 0; + this.searchCurrentIndex = 0; + + this.store.dispatch(EventStore.infoForSearchEnd({})); + } + onSearchChat(searchText: string, baseSeq?: number) { + this.searchText = searchText; + + // CASE :: searching text after retrieve All event Infos. + this.store.dispatch( + EventStore.infoAll({ + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: this.eventListSubject.value[0].seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount * 2 + }, + infoList: undefined + }) + ); + + // this.doSearchTextInEvent(searchText); + } + doSearchTextInEvent(searchText: string, baseSeq?: number): void { + this.searchedListSubject.next( + this.eventListSubject.value.filter(event => { + let contents = ''; + if (event.type === EventType.Character) { + contents = event.sentMessage; + } else if ( + event.type === EventType.Sticker && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as StickerEventJson).chat; + } else if (event.type === EventType.File && !!event.sentMessageJson) { + contents = (event.sentMessageJson as FileEventJson).fileName; + } else if ( + event.type === EventType.MassText && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as MassTextEventJson).content; + } else if ( + (event.type === EventType.Translation || + event.type === EventType.MassTranslation) && + !!event.sentMessageJson + ) { + contents = (event.sentMessageJson as TranslationEventJson).original; + contents += (event.sentMessageJson as TranslationEventJson) + .translation; + } + + return contents.indexOf(searchText) > -1; + }) + ); + + if ( + !!this.searchedListSubject.value && + this.searchedListSubject.value.length > 0 + ) { + this.searchTotalCount = this.searchedListSubject.value.length; + + if (!!baseSeq && baseSeq > 0) { + this.searchedListSubject.value.forEach((searched, index) => { + if (searched.seq <= baseSeq) { + this.searchCurrentIndex = index + 1; + this.searchedFocusEvent = searched; + } + }); + } else { + this.searchCurrentIndex = this.searchedListSubject.value.length; + this.searchedFocusEvent = this.searchedListSubject.value[ + this.searchedListSubject.value.length - 1 + ]; + } + + this.store.dispatch(EventStore.infoForSearchEnd({})); + + // this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + this.chatMessages.initEventMore(this.searchedFocusEvent.seq); + } else { + this.searchTotalCount = 0; + this.searchCurrentIndex = 0; + + this.searchedFocusEvent = null; + } + } + onPrevSearch() { + this.searchedListSubject.value.forEach((event, index) => { + if (event.seq === this.searchedFocusEvent.seq && index > 0) { + this.searchCurrentIndex = this.searchCurrentIndex - 1; + this.searchedFocusEvent = this.searchedListSubject.value[index - 1]; + this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + } + }); + } + onNextSearch() { + // let exist = false; + // this.searchedList.forEach((event, index) => { + // if ( + // event.seq === this.searchedFocusEvent.seq && + // index < this.searchedList.length - 2 && + // !exist + // ) { + // exist = true; + // this.searchCurrentIndex = this.searchCurrentIndex + 1; + // this.searchedFocusEvent = this.searchedList[index + 1]; + // this.goSearchPosition(this.searchedFocusEvent.seq); + // } + // }); + + if (this.searchCurrentIndex < this.searchedListSubject.value.length) { + this.searchedFocusEvent = this.searchedListSubject.value[ + this.searchCurrentIndex + ]; + this.searchCurrentIndex = this.searchCurrentIndex + 1; + this.chatMessages.gotoPosition(this.searchedFocusEvent.seq); + } + } + onSearchAndPrev() { + if ( + !!this.searchText && + this.searchText.trim().length > 0 && + this.eventRemainedSubject.value + ) { + this.moreSearchProcessing = true; + this.chatMessages.storeScrollPosition(); + + // Case :: retrieve event infos step by step until include searchtext in event.. + this.store.dispatch( + EventStore.infoForSearch({ + req: { + roomSeq: this.roomInfoSubject.value.roomSeq, + baseSeq: this.eventListSubject.value[0].seq, + requestCount: + environment.productConfig.CommonSetting.eventRequestDefaultCount + }, + searchText: this.searchText + }) + ); + } + } + + /** + * 번역기능 비활성화 2020-02-07 + * About Translation */ + // onShowToggleTranslation() { + // this.isShowTranslation = !this.isShowTranslation; + // if (!this.isShowTranslation) { + // } + // } + // onChangeTranslationSimpleView(value: boolean) { + // this.translationSimpleview = value; + // } + // onChangeTranslationPreView(value: boolean) { + // this.translationPreview = value; + // } + // onChangeDestLocale(destLocale: string) { + // this.destLocale = destLocale; + // } + // onCancelTranslation() { + // this.isTranslationProcess = false; + // this.translationPreviewInfo = null; + // } + + // onSendTranslationMessage(params: { + // previewInfo: TranslationSaveResponse | null; + // translationType: EventType.Translation | EventType.MassTranslation; + // }) { + // let sentMessage = ''; + // if (params.translationType === EventType.MassTranslation) { + // // Mass Text Translation + // sentMessage = params.previewInfo.returnJson; + // } else { + // // Normal Text Translation + // sentMessage = JSON.stringify({ + // locale: params.previewInfo.destLocale, + // original: params.previewInfo.original, + // translation: params.previewInfo.translation, + // stickername: '', + // stickerfile: !!this.selectedSticker ? this.selectedSticker.index : '' + // }); + // } + + // this.store.dispatch( + // EventStore.send({ + // senderSeq: this.loginResSubject.value.userSeq, + // req: { + // roomSeq: this.roomInfoSubject.value.roomSeq, + // eventType: params.translationType, + // sentMessage + // } + // }) + // ); + + // this.isTranslationProcess = false; + // this.translationPreviewInfo = null; + // } + + onClipboardPaste(event: ClipboardEvent) { + this.nativeService.readFromClipboard().then(async data => { + if (!!data.image && !!data.text) { + const result = await this.dialogService.open< + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult + >(ClipboardDialogComponent, { + width: '800px', + maxWidth: '800px', + height: '800px', + minHeight: '800px', + disableClose: false, + data: { + content: data + } + }); + + if (result.selected.text) { + this.onSendMessage(data.text.replace(/\t/g, ' ')); + } + + if (result.selected.image) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } + } else if (!!data.image && !data.text) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } else { + const v = this.chatForm.replyInput.nativeElement.value; + const selectionStart = this.chatForm.replyInput.nativeElement + .selectionStart; + const selectionEnd = this.chatForm.replyInput.nativeElement + .selectionEnd; + + const start = v.substr(0, selectionStart); + const end = v.substr(selectionEnd); + + this.chatForm.replyInput.nativeElement.value = `${start}${data.text}${end}`; + + event.preventDefault(); + } + }); + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/messages.component.ts b/documents/업무/2월/3째주/file-viewer-prj/messages.component.ts new file mode 100644 index 0000000..c3964c2 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/messages.component.ts @@ -0,0 +1,699 @@ +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; + @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() + lock$: 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('chatMessagesBufferContainer', { static: false }) + chatMessagesBufferContainer: ElementRef; + + @ViewChild(VirtualScrollerComponent, { static: false }) + private virtualScroller: VirtualScrollerComponent; + + @ViewChildren('chatMessageBox') + chatMessageBoxList: QueryList; + + storedScrollItem: Info; // 이전대화를 불러올 경우 현재 스크롤 포지션 유지를 위한 값. 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[]; + 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; + 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): 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) { + 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 + ); + } + } + } 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, + 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; + 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; + 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; + } + + compareItemsFunc = ( + item1: Info, + item2: Info + // tslint:disable-next-line: semicolon + ): boolean => !!item1 && !!item2 && item1.seq === item2.seq; +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/models copy/select-file-info.ts b/documents/업무/2월/3째주/file-viewer-prj/models copy/select-file-info.ts new file mode 100644 index 0000000..eea7598 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/models copy/select-file-info.ts @@ -0,0 +1,4 @@ +export interface SelectFileInfo { + attachmentSeq: number; + index?: number; +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/models/select-file-info.ts b/documents/업무/2월/3째주/file-viewer-prj/models/select-file-info.ts new file mode 100644 index 0000000..eea7598 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/models/select-file-info.ts @@ -0,0 +1,4 @@ +export interface SelectFileInfo { + attachmentSeq: number; + index?: number; +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/plan.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/plan.event-json.ts new file mode 100644 index 0000000..bba0309 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/plan.event-json.ts @@ -0,0 +1,34 @@ +import { EventJsonDecoder } from './event-json'; +import { JsonAnalization } from '@ucap-webmessenger/api'; + +export interface PlanEventJson { + planSeq?: number; + title?: string; + contents?: PlanContentType | string; + date?: string; + endDate?: string; + active?: boolean; +} +export enum PlanContentType { + New = 'PLAN_CONTENTS_NEW', + Update = 'PLAN_CONTENTS_UPDATE', + Delete = 'PLAN_CONTENTS_DELETE' +} + +export const decodePlanEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + return { + planSeq: Number(json.planSeq), + title: json.title, + contents: json.contents, + date: json.date, + endDate: json.endDate, + active: !!json.activeYn && 'Y' === json.activeYn ? true : false + } as PlanEventJson; + } catch (e) { + return {} as PlanEventJson; + } +}; diff --git a/documents/업무/2월/3째주/file-viewer-prj/public-api copy.ts b/documents/업무/2월/3째주/file-viewer-prj/public-api copy.ts new file mode 100644 index 0000000..c3c8982 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/public-api copy.ts @@ -0,0 +1,49 @@ +/* + * Public API Surface of ucap-webmessenger-ui + */ + +export * from './lib/animations/index'; + +export * from './lib/components/file-viewer/document-viewer.component'; +export * from './lib/components/file-viewer/image-viewer.component'; +export * from './lib/components/file-viewer/sound-viewer.component'; +export * from './lib/components/file-viewer/video-viewer.component'; + +export * from './lib/components/expansion-panel.component'; +export * from './lib/components/file-upload-queue.component'; +export * from './lib/components/file-viewer.component'; +export * from './lib/components/float-action-button.component'; +export * from './lib/components/pick-date.component'; +export * from './lib/components/pick-time.component'; +export * from './lib/components/step-input.component'; +export * from './lib/components/split-button.component'; +export * from './lib/components/sticker-selector.component'; +export * from './lib/components/inline-edit-input.component'; + +export * from './lib/data-source/virtual-scroll-tree-flat.data-source'; + +export * from './lib/dialogs/alert.dialog.component'; +export * from './lib/dialogs/confirm.dialog.component'; + +export * from './lib/directives/click-outside.directive'; +export * from './lib/directives/file-upload-for.directive'; +export * from './lib/directives/image.directive'; + +export * from './lib/services/bottom-sheet.service'; +export * from './lib/services/clipboard.service'; +export * from './lib/services/dialog.service'; +export * from './lib/services/snack-bar.service'; +export * from './lib/services/splash-screen.service'; +export * from './lib/services/translate.service'; +export * from './lib/services/date.service'; +export * from './lib/services/paginator-intl.service'; + +export * from './lib/snackbars/alert.snackbar.component'; + +export * from './lib/types/file-viewer.type'; + +export * from './lib/models/select-file-info'; + +export * from './lib/utils/string.util'; + +export * from './lib/ucap-ui.module'; diff --git a/documents/업무/2월/3째주/file-viewer-prj/public-api.ts b/documents/업무/2월/3째주/file-viewer-prj/public-api.ts new file mode 100644 index 0000000..c3c8982 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/public-api.ts @@ -0,0 +1,49 @@ +/* + * Public API Surface of ucap-webmessenger-ui + */ + +export * from './lib/animations/index'; + +export * from './lib/components/file-viewer/document-viewer.component'; +export * from './lib/components/file-viewer/image-viewer.component'; +export * from './lib/components/file-viewer/sound-viewer.component'; +export * from './lib/components/file-viewer/video-viewer.component'; + +export * from './lib/components/expansion-panel.component'; +export * from './lib/components/file-upload-queue.component'; +export * from './lib/components/file-viewer.component'; +export * from './lib/components/float-action-button.component'; +export * from './lib/components/pick-date.component'; +export * from './lib/components/pick-time.component'; +export * from './lib/components/step-input.component'; +export * from './lib/components/split-button.component'; +export * from './lib/components/sticker-selector.component'; +export * from './lib/components/inline-edit-input.component'; + +export * from './lib/data-source/virtual-scroll-tree-flat.data-source'; + +export * from './lib/dialogs/alert.dialog.component'; +export * from './lib/dialogs/confirm.dialog.component'; + +export * from './lib/directives/click-outside.directive'; +export * from './lib/directives/file-upload-for.directive'; +export * from './lib/directives/image.directive'; + +export * from './lib/services/bottom-sheet.service'; +export * from './lib/services/clipboard.service'; +export * from './lib/services/dialog.service'; +export * from './lib/services/snack-bar.service'; +export * from './lib/services/splash-screen.service'; +export * from './lib/services/translate.service'; +export * from './lib/services/date.service'; +export * from './lib/services/paginator-intl.service'; + +export * from './lib/snackbars/alert.snackbar.component'; + +export * from './lib/types/file-viewer.type'; + +export * from './lib/models/select-file-info'; + +export * from './lib/utils/string.util'; + +export * from './lib/ucap-ui.module'; diff --git a/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module copy.ts b/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module copy.ts new file mode 100644 index 0000000..dff7dd4 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module copy.ts @@ -0,0 +1,185 @@ +import { MatTooltipModule } from '@angular/material/tooltip'; +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { FlexLayoutModule } from '@angular/flex-layout'; + +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { + MatTabsModule, + MatSelectModule, + MatSlideToggleModule, + MatTableModule, + MatPaginatorModule +} from '@angular/material'; + +import { DragDropModule } from '@angular/cdk/drag-drop'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { FileUploadQueueComponent } from './components/file-upload-queue.component'; +import { FloatActionButtonComponent } from './components/float-action-button.component'; +import { FileViewerComponent } from './components/file-viewer.component'; +import { MediaViewerComponent } from './components/media-viewer.component'; +import { ExpansionPanelComponent } from './components/expansion-panel.component'; +import { SplitButtonComponent } from './components/split-button.component'; +import { PickDateComponent } from './components/pick-date.component'; +import { PickTimeComponent } from './components/pick-time.component'; +import { StepInputComponent } from './components/step-input.component'; +import { StickerSelectorComponent } from './components/sticker-selector.component'; +import { InlineEditInputComponent } from './components/inline-edit-input.component'; + +import { BinaryViewerComponent } from './components/file-viewer/binary-viewer.component'; +import { DocumentViewerComponent } from './components/file-viewer/document-viewer.component'; +import { ImageViewerComponent } from './components/file-viewer/image-viewer.component'; +import { SoundViewerComponent } from './components/file-viewer/sound-viewer.component'; +import { VideoViewerComponent } from './components/file-viewer/video-viewer.component'; + +import { BottomSheetService } from './services/bottom-sheet.service'; +import { ClipboardService } from './services/clipboard.service'; +import { DialogService } from './services/dialog.service'; +import { SnackBarService } from './services/snack-bar.service'; +import { SplashScreenService } from './services/splash-screen.service'; +import { TranslateService } from './services/translate.service'; +import { DateService } from './services/date.service'; +import { PaginatorIntlService } from './services/paginator-intl.service'; + +import { ClickOutsideDirective } from './directives/click-outside.directive'; +import { FileUploadForDirective } from './directives/file-upload-for.directive'; +import { ImageDirective } from './directives/image.directive'; +import { CdkVirtualScrollViewportPatchDirective } from './directives/cdk-virtual-scroll-viewport-patch.directive'; + +import { AlertDialogComponent } from './dialogs/alert.dialog.component'; +import { ConfirmDialogComponent } from './dialogs/confirm.dialog.component'; + +import { BytesPipe } from './pipes/bytes.pipe'; +import { LinefeedToHtmlPipe, HtmlToLinefeedPipe } from './pipes/linefeed.pipe'; +import { SecondsToMinutesPipe } from './pipes/seconds-to-minutes.pipe'; +import { LinkyPipe } from './pipes/linky.pipe'; +import { TranslatePipe } from './pipes/translate.pipe'; +import { DatePipe } from './pipes/date.pipe'; +import { SafeHtmlPipe } from './pipes/safe-html.pipe'; + +import { + StringEmptyCheckPipe, + StringFormatterPhonePipe +} from './pipes/string.pipe'; +import { ClickDebounceDirective } from './directives/click-debounce.directive'; +import { TranslationSectionComponent } from './components/translation-section.component'; +import { AlertSnackbarComponent } from './snackbars/alert.snackbar.component'; +import { IntegratedSearchFormComponent } from './components/integrated-search-form.component'; +import { IntegratedSearchComponent } from './components/integrated-search.component'; +import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; + +const COMPONENTS = [ + FileUploadQueueComponent, + FloatActionButtonComponent, + FileViewerComponent, + ExpansionPanelComponent, + StickerSelectorComponent, + SplitButtonComponent, + PickDateComponent, + PickTimeComponent, + StepInputComponent, + TranslationSectionComponent, + InlineEditInputComponent, + IntegratedSearchComponent, + IntegratedSearchFormComponent, + + BinaryViewerComponent, + DocumentViewerComponent, + ImageViewerComponent, + SoundViewerComponent, + VideoViewerComponent, + MediaViewerComponent +]; +const DIALOGS = [ + AlertDialogComponent, + ConfirmDialogComponent, + AlertSnackbarComponent +]; +const DIRECTIVES = [ + ClickOutsideDirective, + FileUploadForDirective, + ImageDirective, + CdkVirtualScrollViewportPatchDirective, + ClickDebounceDirective +]; +const PIPES = [ + BytesPipe, + LinefeedToHtmlPipe, + HtmlToLinefeedPipe, + SecondsToMinutesPipe, + LinkyPipe, + TranslatePipe, + DatePipe, + StringEmptyCheckPipe, + StringFormatterPhonePipe, + SafeHtmlPipe +]; +const SERVICES = [ + BottomSheetService, + ClipboardService, + DialogService, + SnackBarService, + SplashScreenService, + TranslateService, + DateService, + PaginatorIntlService +]; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + FlexLayoutModule, + MatButtonModule, + MatCardModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatSliderModule, + MatSnackBarModule, + MatToolbarModule, + MatTooltipModule, + MatTabsModule, + MatFormFieldModule, + MatButtonToggleModule, + MatMenuModule, + MatDatepickerModule, + MatSelectModule, + MatSlideToggleModule, + MatTableModule, + MatPaginatorModule, + + PerfectScrollbarModule, + DragDropModule, + TranslateModule + ], + exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES], + declarations: [...COMPONENTS, ...DIALOGS, ...DIRECTIVES, ...PIPES], + entryComponents: [...DIALOGS] +}) +export class UCapUiModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: UCapUiModule, + providers: [...SERVICES] + }; + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module.ts b/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module.ts new file mode 100644 index 0000000..dff7dd4 --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/ucap-ui.module.ts @@ -0,0 +1,185 @@ +import { MatTooltipModule } from '@angular/material/tooltip'; +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { FlexLayoutModule } from '@angular/flex-layout'; + +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { + MatTabsModule, + MatSelectModule, + MatSlideToggleModule, + MatTableModule, + MatPaginatorModule +} from '@angular/material'; + +import { DragDropModule } from '@angular/cdk/drag-drop'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { FileUploadQueueComponent } from './components/file-upload-queue.component'; +import { FloatActionButtonComponent } from './components/float-action-button.component'; +import { FileViewerComponent } from './components/file-viewer.component'; +import { MediaViewerComponent } from './components/media-viewer.component'; +import { ExpansionPanelComponent } from './components/expansion-panel.component'; +import { SplitButtonComponent } from './components/split-button.component'; +import { PickDateComponent } from './components/pick-date.component'; +import { PickTimeComponent } from './components/pick-time.component'; +import { StepInputComponent } from './components/step-input.component'; +import { StickerSelectorComponent } from './components/sticker-selector.component'; +import { InlineEditInputComponent } from './components/inline-edit-input.component'; + +import { BinaryViewerComponent } from './components/file-viewer/binary-viewer.component'; +import { DocumentViewerComponent } from './components/file-viewer/document-viewer.component'; +import { ImageViewerComponent } from './components/file-viewer/image-viewer.component'; +import { SoundViewerComponent } from './components/file-viewer/sound-viewer.component'; +import { VideoViewerComponent } from './components/file-viewer/video-viewer.component'; + +import { BottomSheetService } from './services/bottom-sheet.service'; +import { ClipboardService } from './services/clipboard.service'; +import { DialogService } from './services/dialog.service'; +import { SnackBarService } from './services/snack-bar.service'; +import { SplashScreenService } from './services/splash-screen.service'; +import { TranslateService } from './services/translate.service'; +import { DateService } from './services/date.service'; +import { PaginatorIntlService } from './services/paginator-intl.service'; + +import { ClickOutsideDirective } from './directives/click-outside.directive'; +import { FileUploadForDirective } from './directives/file-upload-for.directive'; +import { ImageDirective } from './directives/image.directive'; +import { CdkVirtualScrollViewportPatchDirective } from './directives/cdk-virtual-scroll-viewport-patch.directive'; + +import { AlertDialogComponent } from './dialogs/alert.dialog.component'; +import { ConfirmDialogComponent } from './dialogs/confirm.dialog.component'; + +import { BytesPipe } from './pipes/bytes.pipe'; +import { LinefeedToHtmlPipe, HtmlToLinefeedPipe } from './pipes/linefeed.pipe'; +import { SecondsToMinutesPipe } from './pipes/seconds-to-minutes.pipe'; +import { LinkyPipe } from './pipes/linky.pipe'; +import { TranslatePipe } from './pipes/translate.pipe'; +import { DatePipe } from './pipes/date.pipe'; +import { SafeHtmlPipe } from './pipes/safe-html.pipe'; + +import { + StringEmptyCheckPipe, + StringFormatterPhonePipe +} from './pipes/string.pipe'; +import { ClickDebounceDirective } from './directives/click-debounce.directive'; +import { TranslationSectionComponent } from './components/translation-section.component'; +import { AlertSnackbarComponent } from './snackbars/alert.snackbar.component'; +import { IntegratedSearchFormComponent } from './components/integrated-search-form.component'; +import { IntegratedSearchComponent } from './components/integrated-search.component'; +import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; + +const COMPONENTS = [ + FileUploadQueueComponent, + FloatActionButtonComponent, + FileViewerComponent, + ExpansionPanelComponent, + StickerSelectorComponent, + SplitButtonComponent, + PickDateComponent, + PickTimeComponent, + StepInputComponent, + TranslationSectionComponent, + InlineEditInputComponent, + IntegratedSearchComponent, + IntegratedSearchFormComponent, + + BinaryViewerComponent, + DocumentViewerComponent, + ImageViewerComponent, + SoundViewerComponent, + VideoViewerComponent, + MediaViewerComponent +]; +const DIALOGS = [ + AlertDialogComponent, + ConfirmDialogComponent, + AlertSnackbarComponent +]; +const DIRECTIVES = [ + ClickOutsideDirective, + FileUploadForDirective, + ImageDirective, + CdkVirtualScrollViewportPatchDirective, + ClickDebounceDirective +]; +const PIPES = [ + BytesPipe, + LinefeedToHtmlPipe, + HtmlToLinefeedPipe, + SecondsToMinutesPipe, + LinkyPipe, + TranslatePipe, + DatePipe, + StringEmptyCheckPipe, + StringFormatterPhonePipe, + SafeHtmlPipe +]; +const SERVICES = [ + BottomSheetService, + ClipboardService, + DialogService, + SnackBarService, + SplashScreenService, + TranslateService, + DateService, + PaginatorIntlService +]; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + FlexLayoutModule, + MatButtonModule, + MatCardModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatSliderModule, + MatSnackBarModule, + MatToolbarModule, + MatTooltipModule, + MatTabsModule, + MatFormFieldModule, + MatButtonToggleModule, + MatMenuModule, + MatDatepickerModule, + MatSelectModule, + MatSlideToggleModule, + MatTableModule, + MatPaginatorModule, + + PerfectScrollbarModule, + DragDropModule, + TranslateModule + ], + exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES], + declarations: [...COMPONENTS, ...DIALOGS, ...DIRECTIVES, ...PIPES], + entryComponents: [...DIALOGS] +}) +export class UCapUiModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: UCapUiModule, + providers: [...SERVICES] + }; + } +} diff --git a/documents/업무/2월/3째주/file-viewer-prj/video-conference.event-json.ts b/documents/업무/2월/3째주/file-viewer-prj/video-conference.event-json.ts new file mode 100644 index 0000000..5aa209e --- /dev/null +++ b/documents/업무/2월/3째주/file-viewer-prj/video-conference.event-json.ts @@ -0,0 +1,33 @@ +import { EventJsonDecoder } from './event-json'; +import { JsonAnalization } from '@ucap-webmessenger/api'; +import { VideoConferenceContentsType } from '@ucap-webmessenger/protocol-event'; + +export interface VideoConferenceEventJson { + conferenceSeq?: number; + title?: string; + contents?: VideoConferenceContentsType; + startDate?: string; + endDate?: string; + register?: string; + attendee?: string; +} + +export const decodeVideoConferenceEventJson: EventJsonDecoder = ( + message: string +) => { + try { + const json = JsonAnalization.receiveAnalization(message); + + return { + conferenceSeq: Number(json.confSeq), + title: json.title, + contents: json.contents, + startDate: json.startDate, + endDate: json.endDate, + register: json.register, + attendee: json.attendee + } as VideoConferenceEventJson; + } catch (e) { + return {} as VideoConferenceEventJson; + } +};