feat: 收件箱侧边栏展示

pull/64/head
moonrailgun 2 years ago
parent f1238badbd
commit db917d26b9

@ -195,6 +195,7 @@ export {
useHasGroupPermission, useHasGroupPermission,
} from './redux/hooks/useGroupPermission'; } from './redux/hooks/useGroupPermission';
export { useUserInfo, useUserId } from './redux/hooks/useUserInfo'; export { useUserInfo, useUserId } from './redux/hooks/useUserInfo';
export { useInboxList } from './redux/hooks/useInboxList';
export { useUnread } from './redux/hooks/useUnread'; export { useUnread } from './redux/hooks/useUnread';
export { export {
userActions, userActions,

@ -6,3 +6,4 @@ export * as group from './group';
export * as message from './message'; export * as message from './message';
export * as plugin from './plugin'; export * as plugin from './plugin';
export * as user from './user'; export * as user from './user';
export * as inbox from './inbox';

@ -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;
}

@ -0,0 +1,9 @@
import type { InboxItem } from '../../model/inbox';
import { useAppSelector } from './useAppSelector';
/**
*
*/
export function useInboxList(): InboxItem[] {
return useAppSelector((state) => state.chat.inbox ?? []);
}

@ -24,6 +24,7 @@ import {
} from '../model/converse'; } from '../model/converse';
import { appendUserDMConverse } from '../model/user'; import { appendUserDMConverse } from '../model/user';
import { sharedEvent } from '../event'; import { sharedEvent } from '../event';
import type { InboxItem } from '../model/inbox';
/** /**
* Redux * Redux
@ -127,6 +128,10 @@ function initial(socket: AppSocket, store: AppStore) {
socket.request<GroupInfo[]>('group.getUserGroups').then((groups) => { socket.request<GroupInfo[]>('group.getUserGroups').then((groups) => {
store.dispatch(groupActions.appendGroups(groups)); store.dispatch(groupActions.appendGroups(groups));
}); });
socket.request<InboxItem[]>('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)); store.dispatch(groupActions.removeGroup(groupId));
}); });
socket.listen('chat.inbox.updated', () => {
// 检测到收件箱列表被更新,需要重新获取
socket.request<InboxItem[]>('chat.inbox.all').then((list) => {
store.dispatch(chatActions.setInboxList(list));
});
});
// 其他的额外的通知 // 其他的额外的通知
socketEventListeners.forEach(({ eventName, eventFn }) => { socketEventListeners.forEach(({ eventName, eventFn }) => {
socket.listen(eventName, eventFn); socket.listen(eventName, eventFn);

@ -5,6 +5,7 @@ import _uniqBy from 'lodash/uniqBy';
import _orderBy from 'lodash/orderBy'; import _orderBy from 'lodash/orderBy';
import _last from 'lodash/last'; import _last from 'lodash/last';
import { isValidStr } from '../../utils/string-helper'; import { isValidStr } from '../../utils/string-helper';
import type { InboxItem } from '../../model/inbox';
export interface ChatConverseState extends ChatConverseInfo { export interface ChatConverseState extends ChatConverseInfo {
messages: ChatMessage[]; messages: ChatMessage[];
@ -19,6 +20,7 @@ export interface ChatState {
currentConverseId: string | null; // 当前活跃的会话id currentConverseId: string | null; // 当前活跃的会话id
converses: Record<string, ChatConverseState>; // <会话Id, 会话信息> converses: Record<string, ChatConverseState>; // <会话Id, 会话信息>
ack: Record<string, string>; // <会话Id, 本地最后一条会话Id> ack: Record<string, string>; // <会话Id, 本地最后一条会话Id>
inbox: InboxItem[];
/** /**
* mapping * mapping
@ -31,6 +33,7 @@ const initialState: ChatState = {
currentConverseId: null, currentConverseId: null,
converses: {}, converses: {},
ack: {}, ack: {},
inbox: [],
lastMessageMap: {}, lastMessageMap: {},
}; };
@ -311,6 +314,14 @@ const chatSlice = createSlice({
); );
message.reactions.splice(reactionIndex, 1); message.reactions.splice(reactionIndex, 1);
}, },
/**
*
*/
setInboxList(state, action: PayloadAction<InboxItem[]>) {
const list = action.payload;
state.inbox = list;
},
}, },
}); });

@ -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<ConverseNameProps> = React.memo((props) => {
const { converseId, className, style } = props;
const converse = useAppSelector((state) => state.chat.converses[converseId]);
const converseName = useDMConverseName(converse);
return (
<span className={className} style={style}>
{converseName}
</span>
);
});
ConverseName.displayName = 'ConverseName';

@ -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<GroupNameProps> = React.memo((props) => {
const { groupId, className, style } = props;
const groupInfo = useGroupInfo(groupId);
return (
<span className={className} style={style}>
{groupInfo?.name}
</span>
);
});
GroupName.displayName = 'GroupName';

@ -1,32 +1,86 @@
import React from 'react'; import React, { useMemo } from 'react';
import { CommonSidebarWrapper } from '@/components/CommonSidebarWrapper'; 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<InboxSidebarProps> = React.memo((props) => {
const inbox = useInboxList();
const list = useMemo(() => _orderBy(inbox, 'createdAt', 'desc'), [inbox]);
return ( return (
<CommonSidebarWrapper data-tc-role="sidebar-inbox"> <CommonSidebarWrapper data-tc-role="sidebar-inbox">
<div className="overflow-auto"> <div className="overflow-auto">
{Array.from({ length: 20 }).map((_, i) => { {list.map((item) => {
const { type } = item;
if (type === 'message') {
const message: Partial<model.inbox.InboxItem['message']> =
item.message ?? {};
let title: React.ReactNode = '';
if (isValidStr(message.groupId)) {
title = <GroupName groupId={message.groupId} />;
} else if (isValidStr(message.converseId)) {
title = <ConverseName converseId={message.converseId} />;
}
return (
<InboxSidebarItem
key={item._id}
title={title}
desc={getMessageRender(message.messageSnippet ?? '')}
source={'Tailchat'}
selected={props.selectedItem === item._id}
onSelect={() => props.onSelect(item._id)}
/>
);
}
})}
</div>
</CommonSidebarWrapper>
);
});
InboxSidebar.displayName = 'InboxSidebar';
const InboxSidebarItem: React.FC<{
title: React.ReactNode;
desc: React.ReactNode;
source: string;
selected: boolean;
onSelect: () => void;
}> = React.memo((props) => {
return ( return (
<div <div
key={i} className={clsx(
className="p-2 overflow-auto cursor-pointer hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10" 'p-2 overflow-auto cursor-pointer hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10',
{
'bg-black bg-opacity-10 dark:bg-white dark:bg-opacity-10':
props.selected,
}
)}
onClick={props.onSelect}
> >
<div className="text-lg">Title {i}</div> <div className="text-lg overflow-ellipsis overflow-hidden">
{props.title || <span>&nbsp;</span>}
</div>
<div className="break-all text-opacity-80 text-black dark:text-opacity-80 dark:text-white text-sm p-1 border-l-2 border-gray-500 border-opacity-50"> <div className="break-all text-opacity-80 text-black dark:text-opacity-80 dark:text-white text-sm p-1 border-l-2 border-gray-500 border-opacity-50">
DescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDesc {props.desc}
</div> </div>
<div className="text-xs text-opacity-50 text-black dark:text-opacity-50 dark:text-white"> <div className="text-xs text-opacity-50 text-black dark:text-opacity-50 dark:text-white">
{t('来自')}: Tailchat {t('来自')}: {props.source}
</div> </div>
</div> </div>
); );
})}
</div>
</CommonSidebarWrapper>
);
}); });
InboxSidebar.displayName = 'InboxSidebar'; InboxSidebarItem.displayName = 'InboxSidebarItem';

@ -1,11 +1,17 @@
import React from 'react'; import React, { useState } from 'react';
import { PageContent } from '../PageContent'; import { PageContent } from '../PageContent';
import { InboxSidebar } from './Sidebar'; import { InboxSidebar } from './Sidebar';
export const Inbox: React.FC = React.memo(() => { export const Inbox: React.FC = React.memo(() => {
const [selectedItem, setSelectedItem] = useState('');
return ( return (
<PageContent data-tc-role="content-inbox" sidebar={<InboxSidebar />}> <PageContent
<div>Inbox</div> data-tc-role="content-inbox"
sidebar={
<InboxSidebar selectedItem={selectedItem} onSelect={setSelectedItem} />
}
>
<div>Inbox {selectedItem}</div>
</PageContent> </PageContent>
); );
}); });

@ -1,18 +1,21 @@
import { Icon } from 'tailchat-design'; import { Icon } from 'tailchat-design';
import React from 'react'; import React from 'react';
import { t } from 'tailchat-shared'; import { t, useInboxList } from 'tailchat-shared';
import { NavbarNavItem } from './NavItem'; import { NavbarNavItem } from './NavItem';
/** /**
* *
*/ */
export const InboxNav: React.FC = React.memo(() => { export const InboxNav: React.FC = React.memo(() => {
const inbox = useInboxList();
return ( return (
<NavbarNavItem <NavbarNavItem
className="bg-gray-700" className="bg-gray-700"
name={t('收件箱')} name={t('收件箱')}
to={'/main/inbox'} to={'/main/inbox'}
showPill={true} showPill={true}
badge={inbox.filter((i) => !i.readed).length}
data-testid="inbox" data-testid="inbox"
> >
<Icon className="text-3xl text-white" icon="mdi:inbox-arrow-down" /> <Icon className="text-3xl text-white" icon="mdi:inbox-arrow-down" />

@ -11,7 +11,7 @@ export const NavbarNavItem: React.FC<
className?: ClassValue; className?: ClassValue;
to?: string; to?: string;
showPill?: boolean; showPill?: boolean;
badge?: boolean; badge?: boolean | number;
onClick?: () => void; onClick?: () => void;
['data-testid']?: string; ['data-testid']?: string;
}> }>
@ -70,7 +70,7 @@ export const NavbarNavItem: React.FC<
{badge === true ? ( {badge === true ? (
<Badge status="error" /> <Badge status="error" />
) : ( ) : (
<Badge count={Number(badge) || 0} /> <Badge size="small" count={Number(badge) || 0} />
)} )}
</div> </div>
</div> </div>

@ -63,3 +63,9 @@
background-color: inherit; background-color: inherit;
} }
} }
.ant-badge {
.ant-badge-count.ant-badge-count-sm.ant-badge-multiple-words {
padding: 0 4px;
}
}

@ -14,11 +14,13 @@ class InboxMessage {
/** /**
* Id * Id
*/ */
@prop()
groupId?: string; groupId?: string;
/** /**
* Id * Id
*/ */
@prop()
converseId: string; converseId: string;
@prop({ @prop({

Loading…
Cancel
Save