feat: add text panel message search right panel

pull/147/merge
moonrailgun 2 years ago
parent 0ff8f558ef
commit 176528b303

@ -1,5 +1,4 @@
import type { DependencyList } from 'react'; import type { DependencyList } from 'react';
import { isDevelopment, t } from '..';
import { showErrorToasts } from '../manager/ui'; import { showErrorToasts } from '../manager/ui';
import type { FunctionReturningPromise } from '../types'; import type { FunctionReturningPromise } from '../types';
import { useAsyncFn } from './useAsyncFn'; import { useAsyncFn } from './useAsyncFn';

@ -84,6 +84,25 @@ export async function deleteMessage(messageId: string): Promise<boolean> {
return data; return data;
} }
/**
*
* @param converseId id
* @param messageText
*/
export async function searchMessage(
text: string,
converseId: string,
groupId?: string
): Promise<ChatMessage[]> {
const { data } = await request.post('/api/chat/message/searchMessage', {
text,
converseId,
groupId,
});
return data;
}
/** /**
* idid * idid
*/ */

@ -70,7 +70,7 @@ export function formatShortTime(date: dayjs.ConfigType): string {
} }
/** /**
* : * YYYY-MM-DD HH:mm:ss
*/ */
export function formatFullTime(date: dayjs.ConfigType): string { export function formatFullTime(date: dayjs.ConfigType): string {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss'); return dayjs(date).format('YYYY-MM-DD HH:mm:ss');

@ -61,156 +61,160 @@ const MessageActionIcon: React.FC<{ icon: string }> = (props) => (
/** /**
* *
*/ */
const NormalMessage: React.FC<ChatMessageItemProps> = React.memo((props) => { export const NormalMessage: React.FC<ChatMessageItemProps> = React.memo(
const { showAvatar, payload } = props; (props) => {
const userInfo = useCachedUserInfo(payload.author ?? ''); const { showAvatar, payload, hideAction = false } = props;
const [isActionBtnActive, setIsActionBtnActive] = useState(false); const userInfo = useCachedUserInfo(payload.author ?? '');
const [isActionBtnActive, setIsActionBtnActive] = useState(false);
const reactions = useMessageReactions(payload); const reactions = useMessageReactions(payload);
const emojiAction = useChatMessageReactionAction(payload); const emojiAction = useChatMessageReactionAction(payload);
const moreActions = useChatMessageItemAction(payload, { const moreActions = useChatMessageItemAction(payload, {
onClick: () => { onClick: () => {
setIsActionBtnActive(false); setIsActionBtnActive(false);
}, },
}); });
// 禁止对消息进行操作,因为此时消息尚未发送到远程 // 禁止对消息进行操作,因为此时消息尚未发送到远程
const disableOperate = const disableOperate =
payload.isLocal === true || payload.sendFailed === true; hideAction === true ||
payload.isLocal === true ||
payload.sendFailed === true;
return ( return (
<div
className={clsx(
'chat-message-item flex px-2 mobile:px-0 group relative select-text',
{
'bg-black bg-opacity-10': isActionBtnActive,
'hover:bg-black hover:bg-opacity-5': !isActionBtnActive,
}
)}
data-message-id={payload._id}
>
{/* 头像 */}
<div className="w-18 mobile:w-14 flex items-start justify-center pt-0.5">
{showAvatar ? (
<Popover
content={
!_isEmpty(userInfo) && (
<UserPopover userInfo={userInfo as UserBaseInfo} />
)
}
placement="top"
trigger="click"
>
<Avatar
className="cursor-pointer"
size={40}
src={userInfo.avatar}
name={userInfo.nickname}
/>
</Popover>
) : (
<div className="hidden group-hover:block opacity-40">
{formatShortTime(payload.createdAt)}
</div>
)}
</div>
{/* 主体 */}
<div <div
className="flex flex-col flex-1 overflow-auto group" className={clsx(
onContextMenu={stopPropagation} 'chat-message-item flex px-2 mobile:px-0 group relative select-text text-sm',
{
'bg-black bg-opacity-10': isActionBtnActive,
'hover:bg-black hover:bg-opacity-5': !isActionBtnActive,
}
)}
data-message-id={payload._id}
> >
{showAvatar && ( {/* 头像 */}
<div className="flex items-center"> <div className="w-18 mobile:w-14 flex items-start justify-center pt-0.5">
<div className="font-bold"> {showAvatar ? (
{userInfo.nickname ?? <span>&nbsp;</span>} <Popover
</div> content={
<div className="hidden group-hover:block opacity-40 ml-1 text-sm"> !_isEmpty(userInfo) && (
<UserPopover userInfo={userInfo as UserBaseInfo} />
)
}
placement="top"
trigger="click"
>
<Avatar
className="cursor-pointer"
size={40}
src={userInfo.avatar}
name={userInfo.nickname}
/>
</Popover>
) : (
<div className="hidden group-hover:block opacity-40">
{formatShortTime(payload.createdAt)} {formatShortTime(payload.createdAt)}
</div> </div>
</div> )}
)} </div>
{/* 消息内容 */} {/* 主体 */}
<AutoFolder <div
maxHeight={340} className="flex flex-col flex-1 overflow-auto group"
backgroundColor="var(--tc-content-background-color)" onContextMenu={stopPropagation}
showFullText={
<div className="inline-block rounded-full bg-white dark:bg-black opacity-80 py-2 px-3 hover:opacity-100">
{t('点击展开更多')}
</div>
}
> >
<div className="chat-message-item_body leading-6 break-words"> {showAvatar && (
<MessageQuote payload={payload} /> <div className="flex items-center">
<div className="font-bold">
{userInfo.nickname ?? <span>&nbsp;</span>}
</div>
<div className="hidden group-hover:block opacity-40 ml-1 text-sm">
{formatShortTime(payload.createdAt)}
</div>
</div>
)}
<span>{getMessageRender(payload.content)}</span> {/* 消息内容 */}
<AutoFolder
maxHeight={340}
backgroundColor="var(--tc-content-background-color)"
showFullText={
<div className="inline-block rounded-full bg-white dark:bg-black opacity-80 py-2 px-3 hover:opacity-100">
{t('点击展开更多')}
</div>
}
>
<div className="chat-message-item_body leading-6 break-words">
<MessageQuote payload={payload} />
{payload.sendFailed === true && ( <span>{getMessageRender(payload.content)}</span>
<Icon
className="inline-block ml-1" {payload.sendFailed === true && (
icon="emojione:cross-mark-button" <Icon
/> className="inline-block ml-1"
)} icon="emojione:cross-mark-button"
/>
)}
{/* 解释器按钮 */}
{useRenderPluginMessageInterpreter(payload.content)}
</div>
</AutoFolder>
{/* 解释器按钮 */} {/* 额外渲染 */}
{useRenderPluginMessageInterpreter(payload.content)} <div>
{pluginMessageExtraParsers.map((parser) => (
<React.Fragment key={parser.name}>
{parser.render(payload)}
</React.Fragment>
))}
</div> </div>
</AutoFolder>
{/* 额外渲染 */} {/* 消息反应 */}
<div> {reactions}
{pluginMessageExtraParsers.map((parser) => (
<React.Fragment key={parser.name}>
{parser.render(payload)}
</React.Fragment>
))}
</div> </div>
{/* 消息反应 */} {/* 操作 */}
{reactions} {!disableOperate && (
</div> <div
className={clsx(
{/* 操作 */} 'bg-white dark:bg-black rounded absolute right-2 cursor-pointer -top-3 shadow-sm flex',
{!disableOperate && ( {
<div 'opacity-0 group-hover:opacity-100 bg-opacity-80 hover:bg-opacity-100':
className={clsx( !isActionBtnActive,
'bg-white dark:bg-black rounded absolute right-2 cursor-pointer -top-3 shadow-sm flex', 'opacity-100 bg-opacity-100': isActionBtnActive,
{ }
'opacity-0 group-hover:opacity-100 bg-opacity-80 hover:bg-opacity-100': )}
!isActionBtnActive,
'opacity-100 bg-opacity-100': isActionBtnActive,
}
)}
>
<TcPopover
overlayClassName="chat-message-item_action-popover"
content={emojiAction}
placement="bottomLeft"
trigger={['click']}
onOpenChange={setIsActionBtnActive}
> >
<div> <TcPopover
<MessageActionIcon icon="mdi:emoticon-happy-outline" /> overlayClassName="chat-message-item_action-popover"
</div> content={emojiAction}
</TcPopover> placement="bottomLeft"
trigger={['click']}
onOpenChange={setIsActionBtnActive}
>
<div>
<MessageActionIcon icon="mdi:emoticon-happy-outline" />
</div>
</TcPopover>
<Dropdown <Dropdown
menu={moreActions} menu={moreActions}
placement="bottomLeft" placement="bottomLeft"
trigger={['click']} trigger={['click']}
onOpenChange={setIsActionBtnActive} onOpenChange={setIsActionBtnActive}
> >
<div> <div>
<MessageActionIcon icon="mdi:dots-horizontal" /> <MessageActionIcon icon="mdi:dots-horizontal" />
</div> </div>
</Dropdown> </Dropdown>
</div> </div>
)} )}
</div> </div>
); );
}); }
);
NormalMessage.displayName = 'NormalMessage'; NormalMessage.displayName = 'NormalMessage';
/** /**
@ -250,6 +254,7 @@ SystemMessageWithNickname.displayName = 'SystemMessageWithNickname';
interface ChatMessageItemProps { interface ChatMessageItemProps {
showAvatar: boolean; showAvatar: boolean;
payload: LocalChatMessage; payload: LocalChatMessage;
hideAction?: boolean;
} }
const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo((props) => { const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo((props) => {
const payload = props.payload; const payload = props.payload;

@ -0,0 +1,62 @@
import { NormalMessage } from '@/components/ChatBox/ChatMessageList/Item';
import { Empty, Input } from 'antd';
import React from 'react';
import {
ChatMessage,
model,
showToasts,
t,
useAsyncRequest,
} from 'tailchat-shared';
export const MessageSearchPanel: React.FC<{
groupId?: string;
converseId: string;
}> = React.memo((props) => {
const { groupId, converseId } = props;
const [{ loading, value = [] }, handleSearch] = useAsyncRequest(
async (searchText: string) => {
if (searchText.length < 3) {
showToasts(t('搜索内容太短无法搜索'));
return;
}
const messages = await model.message.searchMessage(
searchText,
converseId,
groupId
);
return messages ?? [];
}
);
const searchedMessages = value as ChatMessage[];
return (
<div className="p-2">
<Input.Search
className="mb-2"
placeholder="请输入关键字"
loading={loading}
onSearch={handleSearch}
/>
{/* Result List */}
<div>
{searchedMessages.length === 0 && (
<Empty description={t('没有任何搜索结果')} />
)}
{searchedMessages.map((message) => (
<NormalMessage
key={message._id}
showAvatar={true}
payload={message}
hideAction={true}
/>
))}
</div>
</div>
);
});
MessageSearchPanel.displayName = 'MessageSearchPanel';

@ -22,6 +22,7 @@ import {
import { useFriendNicknameMap } from 'tailchat-shared/redux/hooks/useFriendNickname'; import { useFriendNicknameMap } from 'tailchat-shared/redux/hooks/useFriendNickname';
import { MembersPanel } from './MembersPanel'; import { MembersPanel } from './MembersPanel';
import { GroupPanelContainer } from './shared/GroupPanelContainer'; import { GroupPanelContainer } from './shared/GroupPanelContainer';
import { MessageSearchPanel } from '../common/MessageSearch';
/** /**
* *
@ -131,6 +132,21 @@ export const TextPanel: React.FC<TextPanelProps> = React.memo(
}) })
} }
/>, />,
<IconBtn
key="search"
title={t('聊天记录搜索')}
shape="square"
icon="mdi:text-search"
iconClassName="text-2xl"
onClick={() =>
setRightPanel({
name: t('聊天记录'),
panel: (
<MessageSearchPanel groupId={groupId} converseId={panelId} />
),
})
}
/>,
]} ]}
> >
<ChatInputMentionsContextProvider <ChatInputMentionsContextProvider

@ -16,6 +16,7 @@ import { OpenedPanelTip } from '@/components/OpenedPanelTip';
import { IconBtn } from '@/components/IconBtn'; import { IconBtn } from '@/components/IconBtn';
import { DMPluginPanelActionProps, pluginPanelActions } from '@/plugin/common'; import { DMPluginPanelActionProps, pluginPanelActions } from '@/plugin/common';
import { CreateDMConverse } from '@/components/modals/CreateDMConverse'; import { CreateDMConverse } from '@/components/modals/CreateDMConverse';
import { MessageSearchPanel } from '../common/MessageSearch';
const ConversePanelTitle: React.FC<{ converse: ChatConverseState }> = const ConversePanelTitle: React.FC<{ converse: ChatConverseState }> =
React.memo(({ converse }) => { React.memo(({ converse }) => {
@ -135,6 +136,19 @@ export const ConversePanel: React.FC<ConversePanelProps> = React.memo(
} }
/> />
), ),
<IconBtn
key="search"
title={t('聊天记录搜索')}
shape="square"
icon="mdi:text-search"
iconClassName="text-2xl"
onClick={() =>
setRightPanel({
name: t('聊天记录'),
panel: <MessageSearchPanel converseId={converseId} />,
})
}
/>,
]); ]);
}} }}
> >

@ -404,6 +404,11 @@ class MessageService extends TcService {
content: { content: {
$regex: text, $regex: text,
}, },
author: {
$not: {
$eq: SYSTEM_USERID,
},
},
}) })
.sort({ _id: -1 }) .sort({ _id: -1 })
.limit(10) .limit(10)

Loading…
Cancel
Save