feat: 消息回复与显示

pull/13/head
moonrailgun 4 years ago
parent 312d96fb7a
commit acceba8726

@ -0,0 +1,50 @@
import React, { useCallback, useContext, useState } from 'react';
import _noop from 'lodash/noop';
import type { ReplyMsgType } from '../utils/msg-helper';
/**
*
*/
interface ChatBoxContextProps {
replyMsg: ReplyMsgType | null;
setReplyMsg: (msg: ReplyMsgType | null) => void;
}
const ChatBoxContext = React.createContext<ChatBoxContextProps>({
replyMsg: null,
setReplyMsg: _noop,
});
ChatBoxContext.displayName = 'ChatBoxContext';
export const ChatBoxContextProvider: React.FC = React.memo((props) => {
const [replyMsg, setReplyMsg] = useState<ReplyMsgType | null>(null);
return (
<ChatBoxContext.Provider
value={{
replyMsg,
setReplyMsg,
}}
>
{props.children}
</ChatBoxContext.Provider>
);
});
ChatBoxContextProvider.displayName = 'ChatBoxContextProvider';
export function useChatBoxContext(): ChatBoxContextProps & {
hasContext: boolean;
clearReplyMsg: () => void;
} {
const context = useContext(ChatBoxContext);
const clearReplyMsg = useCallback(() => {
context.setReplyMsg(null);
}, [context.setReplyMsg]);
return {
hasContext: context.setReplyMsg !== _noop,
replyMsg: context.replyMsg,
setReplyMsg: context.setReplyMsg,
clearReplyMsg,
};
}

@ -144,6 +144,7 @@
"kf7d829eb": "Wait to process",
"kf87f3059": "Plugin Store",
"kfa01c850": "No private message found",
"kfa493f3f": "Reply",
"kfaddd61d": "Chat Service",
"kfbecf2a7": "Are you sure you want to delete the panel group [{{name}}] and all subordinate panels"
}

@ -144,6 +144,7 @@
"kf7d829eb": "待处理",
"kf87f3059": "插件中心",
"kfa01c850": "找不到私信会话",
"kfa493f3f": "回复",
"kfaddd61d": "聊天服务",
"kfbecf2a7": "确定要删除面板组 【{{name}}】 以及下级的所有面板么"
}

@ -34,6 +34,10 @@ export { buildPortal, DefaultEventEmitter } from './components/Portal';
export { TcProvider } from './components/Provider';
// contexts
export {
ChatBoxContextProvider,
useChatBoxContext,
} from './contexts/ChatBoxContext';
export { useColorScheme } from './contexts/ColorSchemeContext';
// i18n

@ -0,0 +1,4 @@
import type { ChatMessage } from '../model/message';
const replyMsgFields = ['_id', 'content', 'author'] as const;
export type ReplyMsgType = Pick<ChatMessage, typeof replyMsgFields[number]>;

@ -0,0 +1,22 @@
import { Skeleton } from 'antd';
import React from 'react';
export const ChatBoxPlaceholder: React.FC = React.memo(() => {
const paragraph = { rows: 1 };
return (
<div className="px-2 w-2/3">
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
<Skeleton className="mb-2" avatar={true} paragraph={paragraph} />
</div>
);
});
ChatBoxPlaceholder.displayName = 'ChatBoxPlaceholder';

@ -2,12 +2,32 @@ import React from 'react';
import {
ChatMessage,
formatShortTime,
t,
useCachedUserInfo,
useChatBoxContext,
} from 'tailchat-shared';
import { Avatar } from '@/components/Avatar';
import clsx from 'clsx';
import { useRenderPluginMessageInterpreter } from './useRenderPluginMessageInterpreter';
import { getMessageRender } from '@/plugin/common';
import { Icon } from '@iconify/react';
import { Dropdown, Menu } from 'antd';
/**
*
*/
function useChatMessageItemAction(payload: ChatMessage): React.ReactElement {
const context = useChatBoxContext();
return (
<Menu>
{context.hasContext && (
<Menu.Item key="reply" onClick={() => context.setReplyMsg(payload)}>
{t('回复')}
</Menu.Item>
)}
</Menu>
);
}
interface ChatMessageItemProps {
showAvatar: boolean;
@ -18,10 +38,11 @@ export const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo(
const { showAvatar, payload } = props;
const userInfo = useCachedUserInfo(payload.author ?? '');
const actions = useChatMessageItemAction(payload);
return (
<div
className={clsx('flex px-2 group hover:bg-black hover:bg-opacity-10')}
>
<div className="flex px-2 group hover:bg-black hover:bg-opacity-10 relative">
{/* 头像 */}
<div className="w-18 flex items-start justify-center pt-0.5">
{showAvatar ? (
<Avatar size={40} src={userInfo.avatar} name={userInfo.nickname} />
@ -31,6 +52,8 @@ export const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo(
</div>
)}
</div>
{/* 主体 */}
<div className="flex flex-col flex-1 overflow-auto group">
{showAvatar && (
<div className="flex items-center">
@ -48,6 +71,13 @@ export const ChatMessageItem: React.FC<ChatMessageItemProps> = React.memo(
{useRenderPluginMessageInterpreter(payload.content)}
</div>
</div>
{/* 操作 */}
<Dropdown overlay={actions} placement="bottomLeft" trigger={['click']}>
<div className="opacity-0 group-hover:opacity-100 bg-black bg-opacity-5 hover:bg-opacity-10 rounded px-0.5 absolute right-2 top-0.5 cursor-pointer">
<Icon icon="mdi:dots-horizontal" />
</div>
</Dropdown>
</div>
);
}

@ -0,0 +1,36 @@
import React from 'react';
import { t, useChatBoxContext } from 'tailchat-shared';
import _isNil from 'lodash/isNil';
import { getMessageRender } from '@/plugin/common';
import { UserName } from '../UserName';
import { Icon } from '@iconify/react';
export const ChatReply: React.FC = React.memo(() => {
const { replyMsg, clearReplyMsg } = useChatBoxContext();
if (_isNil(replyMsg)) {
return null;
}
return (
<div className="relative">
<div className="absolute bottom-0 left-0 right-0 py-1 px-4">
<div className="rounded bg-white p-2 max-h-44 overflow-auto shadow-sm relative">
<span className="align-top">
{t('回复')}{' '}
{replyMsg.author && <UserName userId={replyMsg.author} />}:{' '}
</span>
<span>{getMessageRender(replyMsg.content)}</span>
<Icon
className="absolute right-1 top-0.5 text-lg cursor-pointer opacity-60 hover:opacity-80"
icon="mdi:close-circle-outline"
onClick={clearReplyMsg}
/>
</div>
</div>
</div>
);
});
ChatReply.displayName = 'ChatReply';

@ -1,29 +1,12 @@
import { Skeleton } from 'antd';
import React, { useRef } from 'react';
import { useConverseMessage } from 'tailchat-shared';
import { ChatBoxContextProvider, useConverseMessage } from 'tailchat-shared';
import { AlertErrorView } from '../AlertErrorView';
import { ChatBoxPlaceholder } from './ChatBoxPlaceholder';
import { ChatInputBox } from './ChatInputBox';
import { ChatMessageList, ChatMessageListRef } from './ChatMessageList';
import { ChatReply } from './ChatReply';
import { useMessageAck } from './useMessageAck';
const ChatBoxPlaceholder: React.FC = React.memo(() => {
return (
<div className="px-2 w-2/3">
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
<Skeleton className="mb-2" avatar={true} paragraph={{ rows: 1 }} />
</div>
);
});
ChatBoxPlaceholder.displayName = 'ChatBoxPlaceholder';
type ChatBoxProps =
| {
converseId: string;
@ -53,26 +36,30 @@ export const ChatBox: React.FC<ChatBoxProps> = React.memo((props) => {
}
return (
<div className="w-full h-full flex flex-col select-text">
<ChatMessageList
ref={chatMessageListRef}
messages={messages}
onUpdateReadedMessage={updateConverseAck}
/>
<ChatBoxContextProvider>
<div className="w-full h-full flex flex-col select-text">
<ChatMessageList
ref={chatMessageListRef}
messages={messages}
onUpdateReadedMessage={updateConverseAck}
/>
<ChatReply />
<ChatInputBox
onSendMsg={(msg) => {
// 发送消息后滚动到底部
handleSendMessage({
converseId: props.converseId,
groupId: props.groupId,
content: msg,
}).then(() => {
chatMessageListRef.current?.scrollToBottom();
});
}}
/>
</div>
<ChatInputBox
onSendMsg={(msg) => {
// 发送消息后滚动到底部
handleSendMessage({
converseId: props.converseId,
groupId: props.groupId,
content: msg,
}).then(() => {
chatMessageListRef.current?.scrollToBottom();
});
}}
/>
</div>
</ChatBoxContextProvider>
);
});
ChatBox.displayName = 'ChatBox';

Loading…
Cancel
Save