diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts index 0156c9c3..b1b6bb8b 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts @@ -137,7 +137,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewChecked { this.userInfoListSubscription = this.store .pipe( - select(AppStore.MessengerSelector.RoomSelector.userInfoList), + select(AppStore.MessengerSelector.RoomSelector.selectUserinfolist), tap(userInfoList => { this.userInfoList = userInfoList; }) diff --git a/projects/ucap-webmessenger-app/src/app/services/notification.service.ts b/projects/ucap-webmessenger-app/src/app/services/notification.service.ts index a08f943b..20f000e7 100644 --- a/projects/ucap-webmessenger-app/src/app/services/notification.service.ts +++ b/projects/ucap-webmessenger-app/src/app/services/notification.service.ts @@ -154,11 +154,7 @@ export class AppNotificationService { 'Notification::eventProtocolService::ReadNotification', noti ); - this.store.dispatch( - EventStore.readNotification({ - noti - }) - ); + this.store.dispatch(EventStore.readNotification(noti)); } break; case SSVC_TYPE_EVENT_CANCEL_NOTI: diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/event/actions.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/event/actions.ts index eeacf52a..d34019f3 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/event/actions.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/event/actions.ts @@ -133,7 +133,7 @@ export const readFailure = createAction( export const readNotification = createAction( '[Messenger::Event] Read Notification', - props<{ noti: ReadNotification }>() + props() ); /** 대화 회수 */ diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/event/effects.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/event/effects.ts index 84cca3d3..e2455f2f 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/event/effects.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/event/effects.ts @@ -32,10 +32,14 @@ import { ReadResponse, DelResponse, CancelResponse, - EventType + EventType, + ReadNotification, + SSVC_TYPE_EVENT_SEND_RES, + SSVC_TYPE_EVENT_SEND_NOTI } from '@ucap-webmessenger/protocol-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 { @@ -149,7 +153,7 @@ export class Effects { exhaustMap(req => this.eventProtocolService.read(req).pipe( map((res: ReadResponse) => { - return readNotification({ noti: res }); + return readNotification(res as ReadNotification); }), catchError(error => of(readFailure({ error }))) ) @@ -161,14 +165,33 @@ export class Effects { () => { return this.actions$.pipe( ofType(readNotification), - map(action => action.noti), - tap(noti => { - this.store.dispatch( - SyncStore.updateUnreadCount({ - roomSeq: noti.roomSeq, - noReadCnt: 0 - }) - ); + withLatestFrom( + this.store.pipe( + select((state: any) => state.messenger.room.roomInfo as RoomInfo) + ), + this.store.pipe( + select( + (state: any) => + state.account.authentication.loginRes as LoginResponse + ) + ) + ), + // map(([action, roomInfo]) => ([action.noti, roomInfo])), + tap(([action, roomInfo, loginRes]) => { + // 현재 오픈된 방에 대한 read noti/res 가 유입될 경우 roomUserList 의 lastReadSeq 를 갱신한다. + if (roomInfo.roomSeq === action.roomSeq) { + this.store.dispatch(RoomStore.updateRoomUserLastReadSeq(action)); + } + + // noti/res 를 일으킨 주체가 본인이라면 현재 오픈된 방 여부에 상관없이 해당방에 대한 noReadCnt 를 초기화 한다. + if (action.SENDER_SEQ === loginRes.userSeq) { + this.store.dispatch( + SyncStore.updateUnreadCount({ + roomSeq: action.roomSeq, + noReadCnt: 0 + }) + ); + } }) ); }, @@ -376,12 +399,24 @@ export class Effects { // opened room :: event add if (!!roomInfo && roomInfo.roomSeq === action.roomSeq) { this.store.dispatch(appendInfoList({ info: action.info })); + + if ( + action.SVC_TYPE === SVC_TYPE_EVENT && + action.SSVC_TYPE === SSVC_TYPE_EVENT_SEND_NOTI + ) { + this.store.dispatch( + read({ + roomSeq: action.roomSeq, + lastReadSeq: action.info.seq + }) + ); + } } // not opened room :: unread count increased if ( action.SVC_TYPE === SVC_TYPE_EVENT && - action.SSVC_TYPE === SSVC_TYPE_EVENT_INFO_RES + action.SSVC_TYPE === SSVC_TYPE_EVENT_SEND_RES ) { /** * 다른 디바이스에서 대화를 송신 할경우 RES 가 noti 로 유입될 수 있다. diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/room/actions.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/room/actions.ts index 29974252..fe6af385 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/room/actions.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/room/actions.ts @@ -15,6 +15,7 @@ import { ExitRequest, ExitResponse } from '@ucap-webmessenger/protocol-room'; +import { ReadNotification } from '@ucap-webmessenger/protocol-event'; export const info = createAction( '[Messenger::Room] Info', @@ -60,6 +61,11 @@ export const updateOnlyAlarm = createAction( props<{ roomInfo: RoomInfo }>() ); +export const updateRoomUserLastReadSeq = createAction( + '[Messenger::Room] Update Room User Only LastReadSeq', + props() +); + export const update = createAction( '[Messenger::Room] Update', props<{ req: UpdateRequest }>() diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/room/reducers.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/room/reducers.ts index f963adb6..bc929d8a 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/room/reducers.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/room/reducers.ts @@ -1,8 +1,13 @@ import { createReducer, on } from '@ngrx/store'; -import { initialState } from './state'; -import { infoSuccess, updateSuccess } from './actions'; +import { initialState, adapterUserInfo, adapterUserInfoShort } from './state'; +import { + infoSuccess, + updateSuccess, + updateRoomUserLastReadSeq +} from './actions'; import * as AuthenticationStore from '@app/store/account/authentication'; +import { UserInfo } from '@ucap-webmessenger/protocol-room'; export const reducer = createReducer( initialState, @@ -10,8 +15,12 @@ export const reducer = createReducer( return { ...state, roomInfo: action.roomInfo, - userInfoList: action.userInfoList, - userInfoShortList: action.userInfoShortList + userInfoList: adapterUserInfo.addAll(action.userInfoList, { + ...state.userInfoList + }), + userInfoShortList: adapterUserInfoShort.addAll(action.userInfoShortList, { + ...state.userInfoShortList + }) }; }), @@ -26,6 +35,23 @@ export const reducer = createReducer( }; }), + on(updateRoomUserLastReadSeq, (state, action) => { + const userSeq = action.SENDER_SEQ; + + const userInfo: UserInfo = { + ...state.userInfoList.entities[userSeq], + lastReadEventSeq: action.lastReadSeq + }; + + return { + ...state, + userInfoList: adapterUserInfo.updateOne( + { id: userSeq, changes: userInfo }, + { ...state.userInfoList } + ) + }; + }), + on(AuthenticationStore.logout, (state, action) => { return { ...initialState diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/room/state.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/room/state.ts index 4662af0a..f30d2dcb 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/room/state.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/room/state.ts @@ -1,3 +1,4 @@ +import { EntityState, createEntityAdapter } from '@ngrx/entity'; import { Selector, createSelector } from '@ngrx/store'; import { InfoRequest, @@ -6,31 +7,71 @@ import { UserInfo } from '@ucap-webmessenger/protocol-room'; +export interface UserInfoShortListState extends EntityState {} +export interface UserInfoListState extends EntityState {} + export interface State { roomInfo: RoomInfo | null; - userInfoShortList: UserInfoShort[] | null; - userInfoList: UserInfo[] | null; + userInfoShortList: UserInfoShortListState; + userInfoList: UserInfoListState; } +export const adapterUserInfoShort = createEntityAdapter({ + selectId: userInfo => userInfo.seq +}); +export const adapterUserInfo = createEntityAdapter({ + selectId: userInfo => userInfo.seq +}); + +const userInfoShortListInitialState: UserInfoShortListState = adapterUserInfoShort.getInitialState( + {} +); +const userInfoListInitialState: UserInfoListState = adapterUserInfo.getInitialState( + {} +); + export const initialState: State = { roomInfo: null, - userInfoShortList: null, - userInfoList: null + userInfoShortList: userInfoShortListInitialState, + userInfoList: userInfoListInitialState }; +const { + selectAll: ngeSelectAllUserInfoShort, + selectEntities: ngeSelectEntitiesUserInfoShort, + selectIds: ngeSelectIdsUserInfoShort, + selectTotal: ngeSelectTotalUserInfoShort +} = adapterUserInfoShort.getSelectors(); +const { + selectAll: ngeSelectAllUserInfo, + selectEntities: ngeSelectEntitiesUserInfo, + selectIds: ngeSelectIdsUserInfo, + selectTotal: ngeSelectTotalUserInfo +} = adapterUserInfo.getSelectors(); + export function selectors(selector: Selector) { + const selectUserInfoShortList = createSelector( + selector, + (state: State) => state.userInfoShortList + ); + + const selectUserInfoList = createSelector( + selector, + (state: State) => state.userInfoList + ); + return { roomInfo: createSelector( selector, (state: State) => state.roomInfo ), - userInfoShortList: createSelector( - selector, - (state: State) => state.userInfoShortList + selectUserInfoShortList: createSelector( + selectUserInfoShortList, + ngeSelectAllUserInfoShort ), - userInfoList: createSelector( - selector, - (state: State) => state.userInfoList + selectUserinfolist: createSelector( + selectUserInfoList, + ngeSelectAllUserInfo ) }; } diff --git a/projects/ucap-webmessenger-app/src/app/store/messenger/sync/state.ts b/projects/ucap-webmessenger-app/src/app/store/messenger/sync/state.ts index dac4c18c..4acdab65 100644 --- a/projects/ucap-webmessenger-app/src/app/store/messenger/sync/state.ts +++ b/projects/ucap-webmessenger-app/src/app/store/messenger/sync/state.ts @@ -8,7 +8,6 @@ import { } from '@ucap-webmessenger/protocol-sync'; import { RoomInfo, - UserInfoShort, UserInfo as RoomUserInfo } from '@ucap-webmessenger/protocol-room'; diff --git a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.html b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.html index b94c43df..4def8769 100644 --- a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.html +++ b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.html @@ -146,7 +146,12 @@
- {{ message.sendDate | date: 'a hh:mm' }} +
    +
  • {{ getUnreadCount(message) }}
  • +
  • + {{ message.sendDate | date: 'a hh:mm' }} +
  • +
diff --git a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.scss b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.scss index d44a47bc..4fdbc36e 100644 --- a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.scss +++ b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.scss @@ -140,23 +140,23 @@ $meBox-bg: #ffffff; display: flex; flex-direction: column; } -.information-msg{ - width:100%; - height:100%; - text-align:center; - background-color:#cccccc; - padding:10px; - margin:10px 0; +.information-msg { + width: 100%; + height: 100%; + text-align: center; + background-color: #cccccc; + padding: 10px; + margin: 10px 0; } .message-row { margin-bottom: 30px; - .date-splitter{ - display:block; - width:100%; - margin-bottom:10px; + .date-splitter { + display: block; + width: 100%; + margin-bottom: 10px; } - .chat-row{ + .chat-row { position: relative; display: flex; flex-direction: row; @@ -165,14 +165,14 @@ $meBox-bg: #ffffff; } } &.me { - .chat-row{ + .chat-row { flex-direction: row-reverse; .profile-info { flex-direction: row-reverse; display: flex; justify-content: flex-end; } - } + } } .message-main { @@ -219,10 +219,15 @@ $meBox-bg: #ffffff; .secondary-text { align-self: flex-end; font-size: 11px; - color:#666666; + color: #666666; word-wrap: break-word; white-space: nowrap; } + &.me { + .secondary-text { + text-align: end; + } + } } .message-row.me > .bubble { @@ -267,4 +272,4 @@ $meBox-bg: #ffffff; top: 5px; } } -} \ No newline at end of file +} diff --git a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.ts b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.ts index 0b8b912a..d3d43a79 100644 --- a/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.ts +++ b/projects/ucap-webmessenger-ui-chat/src/lib/components/messages.component.ts @@ -12,10 +12,8 @@ import { EventType, InfoResponse } from '@ucap-webmessenger/protocol-event'; -import { - LoginResponse, - UserInfo -} from '@ucap-webmessenger/protocol-authentication'; +import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; +import { UserInfo } from '@ucap-webmessenger/protocol-room'; import { NGXLogger } from 'ngx-logger'; import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; import { FileInfo } from '../models/file-info.json'; @@ -61,6 +59,9 @@ export class MessagesComponent implements OnInit { this.profileImageRoot || this.sessionVerInfo.profileRoot; } + /** + * UserInfo getter + */ getUserName(seq: number): string { if (!this.userInfos) { return ''; @@ -88,6 +89,17 @@ export class MessagesComponent implements OnInit { return ''; } + getUnreadCount(message: Info): string | number { + const unreadCnt = this.userInfos.filter(user => { + if (message.senderSeq === user.seq) { + // 본인 글은 unreadCount 에 포함하지 않는다. + return false; + } + return user.lastReadEventSeq < message.seq; + }).length; + return unreadCnt === 0 ? '' : unreadCnt; + } + /** * 정보성 Event 인지 판단. * @description 정보성 event 일 경우 프로필, 일시 를 표현하지 않는다. @@ -106,7 +118,7 @@ export class MessagesComponent implements OnInit { return false; } - /** Date Splitter check */ + /** Date Splitter show check */ getDateSplitter(curIndex: number): boolean { if (curIndex === 0) { return true; @@ -141,6 +153,7 @@ export class MessagesComponent implements OnInit { this.save.emit(value); } + /** [Event] Context Menu */ onContextMenuMessage(event: MouseEvent, message: Info) { this.contextMenu.emit({ event, message }); }