대화내용 검색 기능 추가.

This commit is contained in:
leejinho 2019-12-23 15:23:27 +09:00
parent ccc88e194e
commit b4a6081edd
14 changed files with 496 additions and 30 deletions

View File

@ -127,6 +127,13 @@
>
파일함
</button>
<button
mat-menu-item
*ngIf="getShowContextMenu('CHAT_SEARCH')"
(click)="onClickContextMenu('CHAT_SEARCH')"
>
대화내용 검색
</button>
<button
mat-menu-item
*ngIf="getShowContextMenu('OPEN_ROOM_USER')"
@ -174,6 +181,17 @@
>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div *ngIf="isShowSearchArea" class="char-search">
<ucap-chat-search
[totalCount]="searchTotalCount"
[curIndex]="searchCurrentIndex"
(searchText)="onSearchChat($event)"
(prevSearch)="onPrevSearch()"
(nextSearch)="onNextSearch()"
(searchAndPrev)="onSearchAndPrev()"
(closeSearchArea)="onCloseSearchArea()"
></ucap-chat-search>
</div>
</div>
<!-- CHAT CONTENT -->
<div
@ -196,6 +214,7 @@
>
<ucap-chat-messages
[eventList]="eventList"
[searchedList]="searchedList"
[roomInfo]="roomInfo"
[eventInfoStatus]="eventInfoStatus"
[eventRemain]="eventRemain$ | async"
@ -269,7 +288,7 @@
[hasBackdrop]="false"
(ucapClickOutside)="messageContextMenuTrigger.closeMenu()"
>
<ng-template matMenuContent let-message="message" let-loginRes="loginRes">
<ng-template matMenuContent let-message="message">
<ng-container *ngIf="!isRecalledMessage(message.type)">
<button
mat-menu-item

View File

@ -34,7 +34,8 @@ import {
InfoResponse,
EventJson,
FileEventJson,
StickerEventJson
StickerEventJson,
MassTextEventJson
} from '@ucap-webmessenger/protocol-event';
import * as AppStore from '@app/store';
@ -152,6 +153,15 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
/** Timer 대화방의 대화 삭제를 위한 interval */
interval: any;
/** About Searching */
isShowSearchArea = true;
moreSearchProcessing = false;
searchText = '';
searchedList: Info<EventJson>[];
searchedFocusEvent: Info<EventJson>;
searchTotalCount = 0;
searchCurrentIndex = 0;
/** About Sticker */
isShowStickerSelector = false;
selectedSticker: StickerFilesInfo;
@ -253,7 +263,6 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
infoList.length > 0 &&
!!roomInfo &&
!!roomInfo.lastReadEventSeq &&
roomInfo.lastReadEventSeq > 0 &&
this.baseEventSeq <= roomInfo.lastReadEventSeq
) {
// 조회된 내용중에 read here 가 있을 경우.
@ -263,9 +272,17 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.eventList = infoList;
if (!!infoList && infoList.length > 0) {
if (this.moreSearchProcessing) {
const baseseq = this.baseEventSeq;
setTimeout(() => {
this.onSearchChat(this.searchText, baseseq);
}, 800);
this.baseEventSeq = infoList[0].seq;
this.readyToReply();
} else {
if (!!infoList && infoList.length > 0) {
this.baseEventSeq = infoList[0].seq;
this.readyToReply();
}
}
})
)
@ -340,6 +357,9 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.isShowStickerSelector = false;
this.selectedSticker = undefined;
// Chat Search Clear..
this.onCloseSearchArea();
this.firstcheckReadHere = true;
if (!!this.chatForm) {
@ -1001,6 +1021,11 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
);
}
break;
case 'CHAT_SEARCH':
{
this.onShowToggleSearchArea();
}
break;
case 'OPEN_ROOM_USER':
{
this.store.dispatch(
@ -1168,7 +1193,6 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.selectedSticker = null;
}
}
onSelectedSticker(stickerInfo: StickerFilesInfo) {
this.selectedSticker = stickerInfo;
}
@ -1195,4 +1219,134 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
getStickerHistory(): string[] {
return this.localStorageService.get<string[]>(KEY_STICKER_HISTORY);
}
/** About Chat Search */
onShowToggleSearchArea() {
this.isShowSearchArea = !this.isShowSearchArea;
if (!this.isShowSearchArea) {
this.searchedList = [];
this.searchedFocusEvent = null;
this.searchText = '';
}
}
onCloseSearchArea() {
this.isShowSearchArea = false;
this.searchedList = [];
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;
this.searchedList = this.eventList.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;
}
return contents.indexOf(searchText) > -1;
});
if (!!this.searchedList && this.searchedList.length > 0) {
this.searchTotalCount = this.searchedList.length;
if (!!baseSeq && baseSeq > 0) {
this.searchedList.forEach((searched, index) => {
if (searched.seq <= baseSeq) {
this.searchCurrentIndex = index + 1;
this.searchedFocusEvent = searched;
}
});
} else {
this.searchCurrentIndex = this.searchedList.length;
this.searchedFocusEvent = this.searchedList[
this.searchedList.length - 1
];
}
this.store.dispatch(EventStore.infoForSearchEnd({}));
this.goSearchPosition(this.searchedFocusEvent.seq);
} else {
this.searchTotalCount = 0;
this.searchCurrentIndex = 0;
this.searchedFocusEvent = null;
}
}
onPrevSearch() {
this.searchedList.forEach((event, index) => {
if (event.seq === this.searchedFocusEvent.seq && index > 0) {
this.searchCurrentIndex = this.searchCurrentIndex - 1;
this.searchedFocusEvent = this.searchedList[index - 1];
this.goSearchPosition(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);
// }
// });
this.searchedFocusEvent = this.searchedList[this.searchCurrentIndex];
this.searchCurrentIndex = this.searchCurrentIndex + 1;
this.goSearchPosition(this.searchedFocusEvent.seq);
}
onSearchAndPrev() {
if (
!!this.searchText &&
this.searchText.trim().length > 0 &&
this.eventRemain
) {
this.moreSearchProcessing = true;
this.eventMorePosition = this.psChatContent.directiveRef.elementRef.nativeElement.scrollHeight;
this.store.dispatch(
EventStore.infoForSearch({
req: {
roomSeq: this.roomInfo.roomSeq,
baseSeq: this.eventList[0].seq,
requestCount:
environment.productConfig.CommonSetting.eventRequestDefaultCount
},
searchText: this.searchText
})
);
}
}
goSearchPosition(eventSeq: number) {
if (this.psChatContent.directiveRef) {
this.psChatContent.directiveRef.update();
const element = document.getElementById(eventSeq.toString());
if (!!element) {
setTimeout(() => {
this.psChatContent.directiveRef.scrollToTop(element.offsetTop - 200);
});
}
}
}
}

View File

@ -39,6 +39,14 @@ export const infoFailure = createAction(
'[Messenger::Event] Info Failure',
props<{ error: any }>()
);
export const infoForSearch = createAction(
'[Messenger::Event] Info for search',
props<{ req: InfoRequest; searchText?: string }>()
);
export const infoForSearchEnd = createAction(
'[Messenger::Event] Info for search End',
props()
);
export const fileInfo = createAction(
'[Messenger::Event] File Info',

View File

@ -38,7 +38,10 @@ import {
ReadNotification,
SSVC_TYPE_EVENT_SEND_RES,
SSVC_TYPE_EVENT_SEND_NOTI,
EventJson
EventJson,
StickerEventJson,
FileEventJson,
MassTextEventJson
} from '@ucap-webmessenger/protocol-event';
import * as ChatStore from '@app/store/messenger/chat';
@ -75,7 +78,9 @@ import {
fileInfo,
fileInfoSuccess,
fileInfoFailure,
roomOpenAfterForward
roomOpenAfterForward,
infoForSearch,
infoForSearchEnd
} from './actions';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import {
@ -103,6 +108,12 @@ import {
} from '@ucap-webmessenger/protocol-file';
import { environment } from '../../../../environments/environment';
import { RoomUserData } from '@ucap-webmessenger/protocol-sync';
import {
AlertDialogComponent,
AlertDialogResult,
AlertDialogData,
DialogService
} from '@ucap-webmessenger/ui';
@Injectable()
export class Effects {
@ -236,6 +247,128 @@ export class Effects {
{ dispatch: false }
);
infoForSearch$ = createEffect(
() => {
let infoList: Info<EventJson>[];
return this.actions$.pipe(
ofType(infoForSearch),
tap(() => {
infoList = [];
}),
withLatestFrom(
this.store.pipe(
select(
(state: any) =>
state.messenger.event.infoSearchListProcessing as boolean
)
)
),
switchMap(([action, processing]) => {
return this.eventProtocolService.info(action.req).pipe(
map(async res => {
const req = action.req;
switch (res.SSVC_TYPE) {
case SSVC_TYPE_EVENT_INFO_DATA:
infoList.push(...(res as InfoData).infoList);
break;
case SSVC_TYPE_EVENT_INFO_RES:
{
if (req.baseSeq === 0) {
this.store.dispatch(
infoSuccess({
infoList,
res: res as InfoResponse,
remainInfo:
infoList.length === req.requestCount ? true : false
})
);
} else {
this.store.dispatch(
infoMoreSuccess({
infoList,
res: res as InfoResponse,
remainInfo:
infoList.length === req.requestCount ? true : false
})
);
}
// 검색어가 있을경우 조회된 이벤트 리스트 중 검색어를 찾고, 없으면 재귀한다.
if (!!action.searchText && infoList.length > 0) {
const searchList = infoList.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;
}
return contents.indexOf(action.searchText) > -1;
});
if (
searchList.length === 0 &&
infoList.length === action.req.requestCount &&
processing
) {
this.store.dispatch(
infoForSearch({
req: {
roomSeq: req.roomSeq,
baseSeq: infoList[0].seq,
requestCount: req.requestCount
},
searchText: action.searchText
})
);
} else {
if (infoList.length < action.req.requestCount) {
this.store.dispatch(infoForSearchEnd({}));
await this.dialogService.open<
AlertDialogComponent,
AlertDialogData,
AlertDialogResult
>(AlertDialogComponent, {
width: '360px',
disableClose: true,
data: {
title: '',
message: '더이상 검색할 내용이 없습니다.'
}
});
}
}
}
}
break;
}
}),
catchError(error => of(infoFailure({ error })))
);
})
);
},
{ dispatch: false }
);
fileInfo$ = createEffect(
() => {
let fileInfoList: FileInfo[];
@ -740,6 +873,7 @@ export class Effects {
private fileProtocolService: FileProtocolService,
private roomProtocolService: RoomProtocolService,
private sessionStorageService: SessionStorageService,
private dialogService: DialogService,
private logger: NGXLogger
) {}
}

View File

@ -13,7 +13,9 @@ import {
recallInfoList,
delInfoList,
infoMoreSuccess,
fileInfoSuccess
fileInfoSuccess,
infoForSearch,
infoForSearchEnd
} from './actions';
import * as AuthenticationStore from '@app/store/account/authentication';
import * as ChatStore from '@app/store/messenger/chat';
@ -27,6 +29,19 @@ export const reducer = createReducer(
infoListProcessing: true
};
}),
on(infoForSearch, (state, action) => {
return {
...state,
infoListProcessing: true,
infoSearchListProcessing: true
};
}),
on(infoForSearchEnd, (state, action) => {
return {
...state,
infoSearchListProcessing: false
};
}),
on(infoSuccess, (state, action) => {
return {
@ -88,7 +103,9 @@ export const reducer = createReducer(
return {
...state,
infoList: adapterInfoList.upsertOne(eventinfo, { ...state.infoList })
infoList: adapterInfoList.upsertOne(eventinfo, {
...state.infoList
})
};
}),

View File

@ -13,6 +13,7 @@ export interface FileInfoCheckListState extends EntityState<FileDownloadInfo> {}
export interface State {
infoListProcessing: boolean;
infoSearchListProcessing: boolean;
infoList: InfoListState;
infoStatus: InfoResponse | null;
remainInfo: boolean;
@ -52,6 +53,7 @@ const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoChe
export const initialState: State = {
infoListProcessing: false,
infoSearchListProcessing: false,
infoList: infoListInitialState,
infoStatus: null,
remainInfo: false,
@ -100,32 +102,22 @@ export function selectors<S>(selector: Selector<any, State>) {
selector,
(state: State) => state.infoListProcessing
),
remainInfo: createSelector(
infoSearchListProcessing: createSelector(
selector,
(state: State) => state.remainInfo
),
infoList: createSelector(
selector,
(state: State) => state.infoList
),
infoStatus: createSelector(
selector,
(state: State) => state.infoStatus
(state: State) => state.infoSearchListProcessing
),
remainInfo: createSelector(selector, (state: State) => state.remainInfo),
infoList: createSelector(selector, (state: State) => state.infoList),
infoStatus: createSelector(selector, (state: State) => state.infoStatus),
selectAllInfoList: createSelector(
selectInfoList,
ngeSelectAllInfoList
),
selectAllInfoList: createSelector(selectInfoList, ngeSelectAllInfoList),
selectEntitiesInfoList: createSelector(
selectInfoList,
ngeSelectEntitiesInfoList
),
selectInfoList: (seq: number) =>
createSelector(
selectInfoList,
ngeSelectEntitiesInfoList,
(_, entities) => (!!entities ? entities[seq] : undefined)
createSelector(selectInfoList, ngeSelectEntitiesInfoList, (_, entities) =>
!!entities ? entities[seq] : undefined
),
fileInfoListProcessing: createSelector(

View File

@ -19,9 +19,11 @@
<div
*ngFor="let message of messages; let i = index"
class="message-row"
[id]="message.seq"
[ngClass]="{
me: message.senderSeq === loginRes.userSeq,
contact: message.senderSeq !== loginRes.userSeq
contact: message.senderSeq !== loginRes.userSeq,
searched: getEventSearched(message.seq)
}"
>
<ucap-chat-message-box-read-here

View File

@ -237,6 +237,9 @@ $meBox-bg: #ffffff;
text-align: end;
}
}
&.searched {
color: red;
}
}
.message-row.me > .bubble {

View File

@ -35,6 +35,8 @@ export class MessagesComponent implements OnInit {
this.messages = elist;
}
@Input()
searchedList: Info<EventJson>[];
@Input()
eventInfoStatus?: InfoResponse;
@Input()
eventRemain: boolean;
@ -123,6 +125,12 @@ export class MessagesComponent implements OnInit {
return userInfo[0];
}
}
getEventSearched(seq: number): boolean {
return (
!!this.searchedList &&
this.searchedList.filter(event => event.seq === seq).length > 0
);
}
getUnreadCount(message: Info<EventJson>): string | number {
const unreadCnt = this.userInfos.filter(user => {
@ -178,7 +186,6 @@ export class MessagesComponent implements OnInit {
if (
!!this.roomInfo &&
!!this.roomInfo.lastReadEventSeq &&
this.roomInfo.lastReadEventSeq > 0 &&
this.lastEventSeq - this.roomInfo.lastReadEventSeq > 5
) {
if (

View File

@ -0,0 +1,38 @@
<div fxFlex fxLayout="row">
<div fxLayout="row" fxLayoutAlign="start center" class="input">
<form [formGroup]="fgSearch" class="w-100-p">
<mat-form-field floatLabel="never">
<input
matInput
#inputSearch
type="text"
placeholder="대화방 내용 검색"
value=""
formControlName="searchInput"
(keydown.enter)="onKeyDownEnter($event, inputSearch.value)"
/>
<button
mat-button
matSuffix
mat-icon-button
aria-label="Clear"
(click)="inputSearch.value = ''; onClickSearchCancel()"
>
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
{{ curIndex }} / {{ totalCount }}
</form>
</div>
<div class="btn">
<button mat-stroked-button (click)="onClickSearchAndPrev()">
<span class="mdi mdi-arrow-up-bold-box-outline mid-18px"></span>
</button>
<button mat-stroked-button (click)="onClickPrevSearch()">
<span class="mdi mdi-arrow-up-bold mid-18px"></span>
</button>
<button mat-stroked-button (click)="onClickNextSearch()">
<span class="mdi mdi-arrow-down-bold mid-18px"></span>
</button>
</div>
</div>

View File

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

View File

@ -0,0 +1,65 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { NGXLogger } from 'ngx-logger';
@Component({
selector: 'ucap-chat-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
@Input()
totalCount = 0;
@Input()
curIndex = 0;
@Output()
searchText = new EventEmitter<string>();
@Output()
prevSearch = new EventEmitter();
@Output()
nextSearch = new EventEmitter();
@Output()
searchAndPrev = new EventEmitter();
@Output()
closeSearchArea = new EventEmitter();
fgSearch: FormGroup;
isSearch = false;
constructor(private formBuilder: FormBuilder, private logger: NGXLogger) {}
ngOnInit() {
this.fgSearch = this.formBuilder.group({
searchInput: null
});
}
onClickSearchCancel() {
this.isSearch = false;
this.fgSearch.reset();
this.closeSearchArea.emit();
}
onKeyDownEnter(event: KeyboardEvent, search: string) {
event.preventDefault();
event.stopPropagation();
if (search.trim().length > 0) {
this.isSearch = true;
this.searchText.emit(search.trim());
} else {
this.isSearch = false;
}
}
onClickPrevSearch() {
this.prevSearch.emit();
}
onClickNextSearch() {
this.nextSearch.emit();
}
onClickSearchAndPrev() {
this.searchAndPrev.emit();
}
}

View File

@ -31,10 +31,12 @@ import { TextComponent as MBTextComponent } from './components/message-box/text.
import { TranslationComponent as MBTranslationComponent } from './components/message-box/translation.component';
import { VideoComponent as MBVideoComponent } from './components/message-box/video.component';
import { VideoConferenceComponent as MBVideoConferenceComponent } from './components/message-box/video-conference.component';
import { SearchComponent } from './components/search.component';
const COMPONENTS = [
FormComponent,
MessagesComponent,
SearchComponent,
MBDateSplitterComponent,
MBFileComponent,