feat: 收件箱侧边栏展示

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

@ -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,

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

@ -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';
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<GroupInfo[]>('group.getUserGroups').then((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));
});
socket.listen('chat.inbox.updated', () => {
// 检测到收件箱列表被更新,需要重新获取
socket.request<InboxItem[]>('chat.inbox.all').then((list) => {
store.dispatch(chatActions.setInboxList(list));
});
});
// 其他的额外的通知
socketEventListeners.forEach(({ eventName, eventFn }) => {
socket.listen(eventName, eventFn);

@ -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<string, ChatConverseState>; // <会话Id, 会话信息>
ack: Record<string, string>; // <会话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<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 { 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 (
<CommonSidebarWrapper data-tc-role="sidebar-inbox">
<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 (
<div
key={i}
className="p-2 overflow-auto cursor-pointer hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10"
className={clsx(
'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">
DescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDescDesc
{props.desc}
</div>
<div className="text-xs text-opacity-50 text-black dark:text-opacity-50 dark:text-white">
{t('来自')}: Tailchat
{t('来自')}: {props.source}
</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 { InboxSidebar } from './Sidebar';
export const Inbox: React.FC = React.memo(() => {
const [selectedItem, setSelectedItem] = useState('');
return (
<PageContent data-tc-role="content-inbox" sidebar={<InboxSidebar />}>
<div>Inbox</div>
<PageContent
data-tc-role="content-inbox"
sidebar={
<InboxSidebar selectedItem={selectedItem} onSelect={setSelectedItem} />
}
>
<div>Inbox {selectedItem}</div>
</PageContent>
);
});

@ -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 (
<NavbarNavItem
className="bg-gray-700"
name={t('收件箱')}
to={'/main/inbox'}
showPill={true}
badge={inbox.filter((i) => !i.readed).length}
data-testid="inbox"
>
<Icon className="text-3xl text-white" icon="mdi:inbox-arrow-down" />

@ -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 ? (
<Badge status="error" />
) : (
<Badge count={Number(badge) || 0} />
<Badge size="small" count={Number(badge) || 0} />
)}
</div>
</div>

@ -63,3 +63,9 @@
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
*/
@prop()
groupId?: string;
/**
* Id
*/
@prop()
converseId: string;
@prop({

Loading…
Cancel
Save