From db917d26b9e015f245c851049e3982a5e4c53f34 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 15 Jan 2023 16:06:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B6=E4=BB=B6=E7=AE=B1=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/shared/index.tsx | 1 + client/shared/model/__all__.ts | 1 + client/shared/model/inbox.ts | 17 ++++ client/shared/redux/hooks/useInboxList.ts | 9 ++ client/shared/redux/setup.ts | 12 +++ client/shared/redux/slices/chat.ts | 11 +++ client/web/src/components/ConverseName.tsx | 21 +++++ client/web/src/components/GroupName.tsx | 20 +++++ .../src/routes/Main/Content/Inbox/Sidebar.tsx | 90 +++++++++++++++---- .../src/routes/Main/Content/Inbox/index.tsx | 12 ++- .../web/src/routes/Main/Navbar/InboxNav.tsx | 5 +- client/web/src/routes/Main/Navbar/NavItem.tsx | 4 +- client/web/src/styles/antd/overwrite.less | 6 ++ server/models/chat/inbox.ts | 2 + 14 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 client/shared/model/inbox.ts create mode 100644 client/shared/redux/hooks/useInboxList.ts create mode 100644 client/web/src/components/ConverseName.tsx create mode 100644 client/web/src/components/GroupName.tsx diff --git a/client/shared/index.tsx b/client/shared/index.tsx index e3f7b586..a123ac91 100644 --- a/client/shared/index.tsx +++ b/client/shared/index.tsx @@ -195,6 +195,7 @@ export { useHasGroupPermission, } from './redux/hooks/useGroupPermission'; export { useUserInfo, useUserId } from './redux/hooks/useUserInfo'; +export { useInboxList } from './redux/hooks/useInboxList'; export { useUnread } from './redux/hooks/useUnread'; export { userActions, diff --git a/client/shared/model/__all__.ts b/client/shared/model/__all__.ts index 915ec61a..a0561132 100644 --- a/client/shared/model/__all__.ts +++ b/client/shared/model/__all__.ts @@ -6,3 +6,4 @@ export * as group from './group'; export * as message from './message'; export * as plugin from './plugin'; export * as user from './user'; +export * as inbox from './inbox'; diff --git a/client/shared/model/inbox.ts b/client/shared/model/inbox.ts new file mode 100644 index 00000000..38a58b7d --- /dev/null +++ b/client/shared/model/inbox.ts @@ -0,0 +1,17 @@ +/** + * 收件箱记录项类型 + */ +export interface InboxItem { + _id: string; + userId: string; + readed: boolean; + type: 'message'; + message?: { + groupId?: string; + converseId: string; + messageId: string; + messageSnippet: string; + }; + createdAt: string; + updatedAt: string; +} diff --git a/client/shared/redux/hooks/useInboxList.ts b/client/shared/redux/hooks/useInboxList.ts new file mode 100644 index 00000000..f581dab8 --- /dev/null +++ b/client/shared/redux/hooks/useInboxList.ts @@ -0,0 +1,9 @@ +import type { InboxItem } from '../../model/inbox'; +import { useAppSelector } from './useAppSelector'; + +/** + * 返回收件箱列表 + */ +export function useInboxList(): InboxItem[] { + return useAppSelector((state) => state.chat.inbox ?? []); +} diff --git a/client/shared/redux/setup.ts b/client/shared/redux/setup.ts index 71163bb9..1835a1bd 100644 --- a/client/shared/redux/setup.ts +++ b/client/shared/redux/setup.ts @@ -24,6 +24,7 @@ import { } from '../model/converse'; import { appendUserDMConverse } from '../model/user'; import { sharedEvent } from '../event'; +import type { InboxItem } from '../model/inbox'; /** * 初始化 Redux 上下文 @@ -127,6 +128,10 @@ function initial(socket: AppSocket, store: AppStore) { socket.request('group.getUserGroups').then((groups) => { store.dispatch(groupActions.appendGroups(groups)); }); + + socket.request('chat.inbox.all').then((list) => { + store.dispatch(chatActions.setInboxList(list)); + }); } /** @@ -264,6 +269,13 @@ function listenNotify(socket: AppSocket, store: AppStore) { store.dispatch(groupActions.removeGroup(groupId)); }); + socket.listen('chat.inbox.updated', () => { + // 检测到收件箱列表被更新,需要重新获取 + socket.request('chat.inbox.all').then((list) => { + store.dispatch(chatActions.setInboxList(list)); + }); + }); + // 其他的额外的通知 socketEventListeners.forEach(({ eventName, eventFn }) => { socket.listen(eventName, eventFn); diff --git a/client/shared/redux/slices/chat.ts b/client/shared/redux/slices/chat.ts index 403b7241..83c9c0d3 100644 --- a/client/shared/redux/slices/chat.ts +++ b/client/shared/redux/slices/chat.ts @@ -5,6 +5,7 @@ import _uniqBy from 'lodash/uniqBy'; import _orderBy from 'lodash/orderBy'; import _last from 'lodash/last'; import { isValidStr } from '../../utils/string-helper'; +import type { InboxItem } from '../../model/inbox'; export interface ChatConverseState extends ChatConverseInfo { messages: ChatMessage[]; @@ -19,6 +20,7 @@ export interface ChatState { currentConverseId: string | null; // 当前活跃的会话id converses: Record; // <会话Id, 会话信息> ack: Record; // <会话Id, 本地最后一条会话Id> + inbox: InboxItem[]; /** * 会话最新消息mapping @@ -31,6 +33,7 @@ const initialState: ChatState = { currentConverseId: null, converses: {}, ack: {}, + inbox: [], lastMessageMap: {}, }; @@ -311,6 +314,14 @@ const chatSlice = createSlice({ ); message.reactions.splice(reactionIndex, 1); }, + + /** + * 设置收件箱 + */ + setInboxList(state, action: PayloadAction) { + const list = action.payload; + state.inbox = list; + }, }, }); diff --git a/client/web/src/components/ConverseName.tsx b/client/web/src/components/ConverseName.tsx new file mode 100644 index 00000000..28a0ec4a --- /dev/null +++ b/client/web/src/components/ConverseName.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useAppSelector, useDMConverseName } from 'tailchat-shared'; + +interface ConverseNameProps { + converseId: string; + className?: string; + style?: React.CSSProperties; +} + +export const ConverseName: React.FC = React.memo((props) => { + const { converseId, className, style } = props; + const converse = useAppSelector((state) => state.chat.converses[converseId]); + const converseName = useDMConverseName(converse); + + return ( + + {converseName} + + ); +}); +ConverseName.displayName = 'ConverseName'; diff --git a/client/web/src/components/GroupName.tsx b/client/web/src/components/GroupName.tsx new file mode 100644 index 00000000..e7b5b3e3 --- /dev/null +++ b/client/web/src/components/GroupName.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useGroupInfo } from 'tailchat-shared'; + +interface GroupNameProps { + groupId: string; + className?: string; + style?: React.CSSProperties; +} + +export const GroupName: React.FC = React.memo((props) => { + const { groupId, className, style } = props; + const groupInfo = useGroupInfo(groupId); + + return ( + + {groupInfo?.name} + + ); +}); +GroupName.displayName = 'GroupName'; diff --git a/client/web/src/routes/Main/Content/Inbox/Sidebar.tsx b/client/web/src/routes/Main/Content/Inbox/Sidebar.tsx index c89bd0bf..230ea2b3 100644 --- a/client/web/src/routes/Main/Content/Inbox/Sidebar.tsx +++ b/client/web/src/routes/Main/Content/Inbox/Sidebar.tsx @@ -1,32 +1,86 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { CommonSidebarWrapper } from '@/components/CommonSidebarWrapper'; -import { t } from 'tailchat-shared'; +import { isValidStr, model, t, useInboxList } from 'tailchat-shared'; +import clsx from 'clsx'; +import _orderBy from 'lodash/orderBy'; +import { GroupName } from '@/components/GroupName'; +import { ConverseName } from '@/components/ConverseName'; +import { getMessageRender } from '@/plugin/common'; + +interface InboxSidebarProps { + selectedItem: string; + onSelect: (itemId: string) => void; +} /** * 收件箱侧边栏组件 */ -export const InboxSidebar: React.FC = React.memo(() => { +export const InboxSidebar: React.FC = React.memo((props) => { + const inbox = useInboxList(); + const list = useMemo(() => _orderBy(inbox, 'createdAt', 'desc'), [inbox]); + return (
- {Array.from({ length: 20 }).map((_, i) => { - return ( -
-
Title {i}
-
- DescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDesc -
-
- {t('来自')}: Tailchat -
-
- ); + {list.map((item) => { + const { type } = item; + + if (type === 'message') { + const message: Partial = + item.message ?? {}; + let title: React.ReactNode = ''; + if (isValidStr(message.groupId)) { + title = ; + } else if (isValidStr(message.converseId)) { + title = ; + } + + return ( + props.onSelect(item._id)} + /> + ); + } })}
); }); InboxSidebar.displayName = 'InboxSidebar'; + +const InboxSidebarItem: React.FC<{ + title: React.ReactNode; + desc: React.ReactNode; + source: string; + selected: boolean; + onSelect: () => void; +}> = React.memo((props) => { + return ( +
+
+ {props.title ||  } +
+
+ {props.desc} +
+
+ {t('来自')}: {props.source} +
+
+ ); +}); +InboxSidebarItem.displayName = 'InboxSidebarItem'; diff --git a/client/web/src/routes/Main/Content/Inbox/index.tsx b/client/web/src/routes/Main/Content/Inbox/index.tsx index fd2fc71b..1ef14aa5 100644 --- a/client/web/src/routes/Main/Content/Inbox/index.tsx +++ b/client/web/src/routes/Main/Content/Inbox/index.tsx @@ -1,11 +1,17 @@ -import React from 'react'; +import React, { useState } from 'react'; import { PageContent } from '../PageContent'; import { InboxSidebar } from './Sidebar'; export const Inbox: React.FC = React.memo(() => { + const [selectedItem, setSelectedItem] = useState(''); return ( - }> -
Inbox
+ + } + > +
Inbox {selectedItem}
); }); diff --git a/client/web/src/routes/Main/Navbar/InboxNav.tsx b/client/web/src/routes/Main/Navbar/InboxNav.tsx index bcc7454b..1f58d4c1 100644 --- a/client/web/src/routes/Main/Navbar/InboxNav.tsx +++ b/client/web/src/routes/Main/Navbar/InboxNav.tsx @@ -1,18 +1,21 @@ import { Icon } from 'tailchat-design'; import React from 'react'; -import { t } from 'tailchat-shared'; +import { t, useInboxList } from 'tailchat-shared'; import { NavbarNavItem } from './NavItem'; /** * 收件箱 */ export const InboxNav: React.FC = React.memo(() => { + const inbox = useInboxList(); + return ( !i.readed).length} data-testid="inbox" > diff --git a/client/web/src/routes/Main/Navbar/NavItem.tsx b/client/web/src/routes/Main/Navbar/NavItem.tsx index b2f37ac2..236b51d7 100644 --- a/client/web/src/routes/Main/Navbar/NavItem.tsx +++ b/client/web/src/routes/Main/Navbar/NavItem.tsx @@ -11,7 +11,7 @@ export const NavbarNavItem: React.FC< className?: ClassValue; to?: string; showPill?: boolean; - badge?: boolean; + badge?: boolean | number; onClick?: () => void; ['data-testid']?: string; }> @@ -70,7 +70,7 @@ export const NavbarNavItem: React.FC< {badge === true ? ( ) : ( - + )} diff --git a/client/web/src/styles/antd/overwrite.less b/client/web/src/styles/antd/overwrite.less index 6e7abc8a..b87d19d7 100644 --- a/client/web/src/styles/antd/overwrite.less +++ b/client/web/src/styles/antd/overwrite.less @@ -63,3 +63,9 @@ background-color: inherit; } } + +.ant-badge { + .ant-badge-count.ant-badge-count-sm.ant-badge-multiple-words { + padding: 0 4px; + } +} diff --git a/server/models/chat/inbox.ts b/server/models/chat/inbox.ts index 5ea0d005..f9a083ba 100644 --- a/server/models/chat/inbox.ts +++ b/server/models/chat/inbox.ts @@ -14,11 +14,13 @@ class InboxMessage { /** * 消息所在群组Id */ + @prop() groupId?: string; /** * 消息所在会话Id */ + @prop() converseId: string; @prop({