Merge branch 'main' into dev/chat

# Conflicts:
#	src/views/ChatHistory.jsx
dev/chat
Lei OT 2 years ago
commit 22cf74c100

@ -88,7 +88,7 @@ export const fetchConversationsSearch = async (params) => {
customer_name: `${ele.whatsapp_name || ''}`.trim(),
whatsapp_name: `${ele.whatsapp_name || ''}`.trim(),
opi_sn: ele.OPI_SN || ele.opi_sn || 0,
OPI_Name: `${ele.OPI_Name || ''}`.trim(),
OPI_Name: `${ele.OPI_Name || ele.opi_name || ''}`.trim(),
matchMsgList: parseRenderMessageList((ele.msglist_AsJOSN || [])), // .reverse()),
}));
return list;

@ -33,12 +33,10 @@ const router = createBrowserRouter([
children: isMobileApp
? [
{
path: 'm',
element: <MobileApp />,
children: [
{ path: 'conversation', element: <MobileConversation /> },
// { path: 'chat/:order_sn', element: <MobileChat /> },
// { path: 'chat', element: <MobileChat /> },
{ index: true, element: <MobileConversation /> },
{ path: 'm/conversation', element: <MobileConversation /> },
],
},
{ path: 'm/chat/:order_sn', element: <MobileChat /> },
@ -63,7 +61,7 @@ const router = createBrowserRouter([
path: '/p',
element: <Standlone />,
children: [
{ path: 'dingding/qrcode', element: <DingdingQRCode /> },
{ path: 'dingding/qrcode', element: isMobileApp ? <MobileLogin /> : <DingdingQRCode /> },
{ path: 'dingding/callback', element: <DingdingCallback /> },
{ path: 'dingding/logout', element: <DingdingLogout /> },
{ path: 'mobile-login', element: <MobileLogin /> },

@ -255,11 +255,12 @@ const messageSlice = (set, get) => ({
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
let newConversations = [];
if (targetIndex !== -1 && message.status === 'received') { // 'delivered'
if (targetIndex !== -1) { // 'delivered'
// 更新列表的时间
conversationsList.splice(targetIndex, 1, {
...conversationsList[targetIndex],
last_received_time: message.deliverTime, // todo: 需要+8 hours
last_received_time: message.status === 'received' ? message.deliverTime : conversationsList[targetIndex].last_received_time, // todo: 需要+8 hours
conversation_expiretime: message?.conversation?.expireTime || conversationsList[targetIndex].conversation_expiretime || '', // 保留使用UTC时间
});
} else if (targetIndex === -1) {
// 当前客户端不存在的会话 todo: 设置为当前(在WhatsApp返回号码不一致时)
@ -273,6 +274,7 @@ const messageSlice = (set, get) => ({
whatsapp_name: message?.senderName || message?.sender || '',
customer_name: message?.senderName || message?.sender || '',
whatsapp_phone_number: message.from,
conversation_expiretime: message?.conversation?.expireTime || '', // 保留使用UTC时间
}];
}

@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react';
import { Divider, Button, Input, Layout, DatePicker, Form, List, Spin, Flex, Image } from 'antd';
import { App, Divider, Button, Input, Layout, DatePicker, Form, List, Spin, Flex, Image } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { ChatItem, MessageBox } from 'react-chat-elements';
import { MESSAGE_PAGE_SIZE, fetchConversationsSearch, fetchMessagesHistory } from '@/actions/ConversationActions';
@ -67,6 +67,9 @@ const SearchForm = memo(function ({ initialValues, onSubmit }) {
});
function ChatHistory() {
const { message: appMessage } = App.useApp()
// const [formValues, setFormValues] = useState({});
const [formValues, setFormValues] = useFormStore(((state) => [state.chatHistoryForm, state.setChatHistoryForm]));
const [selectedConversation, setSelectedConversation] = useFormStore(((state) => [state.chatHistorySelectChat, state.setChatHistorySelectChat]));
@ -227,7 +230,7 @@ function ChatHistory() {
return prev;
}, []);
return (
<span className={`text-sm leading-5 emoji-text ${className} ${extraClass} `}>
<span className={`text-sm leading-5 emoji-text ${className} ${extraClass} `} key={'msg-text'}>
{(objArr || []).map((part, index) => {
if (part.type === 'link') {
return (
@ -321,7 +324,7 @@ function ChatHistory() {
</Sider>
<Content style={{ maxHeight: 'calc(100vh - 279px)', height: 'calc(100vh - 279px)', minWidth: '360px' }}>
<Flex className='h-full relative'>
{(selectedConversation.matchMsgList || []).length > 1 && isNotEmpty(formValues.search) && (
{(selectedConversation.matchMsgList || []).length > 0 && isNotEmpty(formValues.search) && (
<div className='w-80 overflow-y-auto overflow-x-hidden'>
<p className='text-center'><mark>{formValues.search}</mark> 的相关记录, 点击定位上下文</p>
{selectedConversation.matchMsgList.map((item) => (
@ -334,7 +337,7 @@ function ChatHistory() {
letter: (item.sender === 'me' ? selectedConversation.OPI_Name || item.senderName : item.senderName).split(' ')[0],
}}
alt={`${item.senderName}`}
title={item.senderName}
title={item.sender === 'me' ? selectedConversation.OPI_Name || item.senderName : item.senderName}
subtitle={item.originText}
date={item.msgtime}
// dateString={item.msgtime}
@ -387,6 +390,7 @@ function ChatHistory() {
{
onClickButton: () => {
navigator.clipboard.writeText(message.text);
appMessage.success('复制成功😀');
},
Component: () => <div>复制</div>,
},
@ -394,7 +398,7 @@ function ChatHistory() {
}
: {})}
renderAddCmp={
<div className='border-dashed border-0 border-t border-slate-300 text-slate-600 space-x-2 emoji'>
<div key={'msg-prefix'} className='border-dashed border-0 border-t border-slate-300 text-slate-600 space-x-2 emoji'>
<span
className={`p-1 rounded-b ${message.msg_direction === 'outbound' ? 'text-white' : ''} `}
style={{ backgroundColor: message.msg_direction === 'outbound' ? stringToColour(message.senderName) : 'unset' }}>

@ -1,7 +1,7 @@
import { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Dropdown, Input } from 'antd';
import { fetchOrderConversationsList, fetchConversationItemClose, fetchMessages, MESSAGE_PAGE_SIZE, } from '@/actions/ConversationActions';
import { fetchOrderConversationsList, fetchConversationItemClose, } from '@/actions/ConversationActions';
import { ChatItem } from 'react-chat-elements';
import { isEmpty } from '@/utils/utils';
import useConversationStore from '@/stores/ConversationStore';

@ -12,23 +12,33 @@ dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
const ExpireTimeClock = () => {
const currentConversation = useConversationStore((state) => state.currentConversation);
const ExpireTimeClock = ({ expireTime }) => {
// const expireTime = useConversationStore((state) => state.currentConversation.conversation_expiretime);
const [customerDateTime, setCustomerDateTime] = useState();
const [customerDateTime, setCustomerDateTime] = useState('');
const [isExpired, setIsExpired] = useState(false);
useEffect(() => {
const intervalId = setInterval(() => {
setCustomerDateTime(dayjs(currentConversation.conversation_expireTime).tz('Asia/Shanghai').fromNow());
}, 1000); // Update every second
// .tz('Asia/Shanghai') UTC Asia/Shanghai GMT+8
setCustomerDateTime(dayjs(expireTime).add(8, 'hours').fromNow());
}, 1000);
return () => clearInterval(intervalId);
}, []);
}, [expireTime]);
return currentConversation.conversation_expireTime ? (
useEffect(() => {
const _ago = customerDateTime.slice(-3) === 'ago';
setIsExpired(_ago);
return () => {};
}, [customerDateTime]);
return expireTime && !isExpired ? (
<>
<Typography.Text className='text-primary'>
<Typography.Text className={'text-primary'}>
<ClockCircleOutlined className='px-1' />
Expire {customerDateTime}
{isExpired ? 'Expired' : 'Expire'} {customerDateTime}
</Typography.Text>
</>
) : null;

@ -57,8 +57,9 @@ const InputComposer = ({ mobile }) => {
const sentOrReceivedNewMessage = useConversationStore((state) => state.sentOrReceivedNewMessage);
const talkabled = !isEmpty(currentConversation.sn) && websocketOpened;
const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true;
const textabled = talkabled && !gt24h;
const isExpired = !isEmpty(currentConversation.conversation_expiretime) ? dayjs(currentConversation.conversation_expiretime).add(8, 'hours').isBefore(dayjs()) : true;
const gt24h = !isEmpty(currentConversation.last_received_time) ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true;
const textabled = talkabled && (!(gt24h && isExpired)); // ,
const textInputRef = useRef(null);
const [textContent, setTextContent] = useState('');
@ -216,10 +217,10 @@ const InputComposer = ({ mobile }) => {
<div>
{referenceMsg.id && (
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
<div className='referrer-msg border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'>
<div className='flex flex-col referrer-msg border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'>
<span className=' text-primary pr-1 italic align-top'>{referenceMsg.senderName}</span>
{referenceMsg.type === 'photo' && <Image width={100} src={referenceMsg.data.uri} />}
<span className='px-1'>{referenceMsg.originText}</span>
<span className='px-1 whitespace-pre-wrap'>{referenceMsg.originText}</span>
</div>
<Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setReferenceMsg({})} />
</Flex>
@ -252,14 +253,20 @@ const InputComposer = ({ mobile }) => {
size='large'
maxLength={2000}
showCount={textabled}
placeholder={gt24h ? 'This session has expired. Please send a template message to activate the session' : 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'}
placeholder={
gt24h
? 'This session has expired. Please send a template message to activate the session'
: mobile === undefined
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
: 'Enter 换行, 点击 Send 发送'
}
rows={2}
disabled={!textabled}
value={textContent}
onChange={(e) => setTextContent(e.target.value)}
className='rounded-b-none emoji'
onPressEnter={(e) => {
if (!e.shiftKey) {
if (!e.shiftKey && mobile === undefined) {
e.preventDefault();
handleSendText();
}
@ -278,7 +285,7 @@ const InputComposer = ({ mobile }) => {
<Button type='text' className='' icon={<FilePdfOutlined />} size={'middle'} /> */}
</Flex>
<Flex gap={4} align={'center'}>
<ExpireTimeClock />
<ExpireTimeClock expireTime={currentConversation.conversation_expiretime} />
<Button key={'send-btn'} onClick={handleSendText} type='primary' size='middle' icon={<SendOutlined />} disabled={!textabled || pastedUploading}>
Send
</Button>

@ -54,7 +54,7 @@ const MessagesHeader = () => {
<Spin spinning={msgListLoading} />
</Flex>
<Flex vertical={true} justify='space-between'>
<Typography.Text><ExpireTimeClock /></Typography.Text>
<Typography.Text><ExpireTimeClock expireTime={currentConversation.conversation_expiretime} /></Typography.Text>
{/* <Typography.Text>{customerDateTime}</Typography.Text> */}
</Flex>
</Flex>

@ -1,12 +1,15 @@
import { useEffect, useRef, useState, forwardRef, memo } from 'react';
import { MessageBox } from 'react-chat-elements';
import { Button } from 'antd';
import { App, Button } from 'antd';
import { DownOutlined, LoadingOutlined } from '@ant-design/icons';
import { useShallow } from 'zustand/react/shallow';
import useConversationStore from '@/stores/ConversationStore';
import { isEmpty, } from '@/utils/utils';
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, ...props }) => {
const { message: appMessage } = App.useApp()
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
// const messagesEndRef = useRef(null);
@ -45,7 +48,7 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
return prev;
}, []);
return (
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`}>
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
{(objArr || []).map((part, index) => {
if (part.type === 'link') {
return (
@ -120,15 +123,16 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
? [
{
onClickButton: () => handleContactClick(message.data),
Component: () => <div>发消息</div>,
Component: () => <div key={'talk-now'}>发消息</div>,
},
]
: []),
{
onClickButton: () => {
navigator.clipboard.writeText(message.text);
appMessage.success('复制成功😀')
},
Component: () => <div>复制</div>,
Component: () => <div key={'copy'}>复制</div>,
},
],
}

@ -2,9 +2,9 @@ import '@/assets/App.css'
import AppLogo from '@/assets/logo-gh.png'
import { useThemeContext } from '@/stores/ThemeContext'
import useAuthStore from '@/stores/AuthStore'
import { Col, Layout, Row, Typography, theme, Space, Avatar } from 'antd'
import { Col, Layout, Row, Typography, theme, Space, Avatar, Dropdown, } from 'antd'
import { DownOutlined } from '@ant-design/icons'
import { NavLink, Outlet } from 'react-router-dom'
import { NavLink, Outlet, Link } from 'react-router-dom'
const { Header, Footer, Content } = Layout
const { Title } = Typography
@ -25,8 +25,22 @@ function MobileApp() {
<NavLink to='/m/conversation'>
<img src={AppLogo} className='logo' alt='App logo' />
</NavLink>
<Space><Avatar
src={loginUser.avatarUrl}>{loginUser?.username?.substring(1)}</Avatar><span style={{ color: colorPrimary }}>{loginUser.username}</span><DownOutlined /></Space>
<Dropdown
menu={{
items: [
{
label: <Link to='/p/dingding/logout'>退出</Link>,
key: '3',
},
]
}}
trigger={['click']}
>
<a onClick={(e) => e.preventDefault()} style={{ color: colorPrimary }}>
<Space><Avatar
src={loginUser.avatarUrl}>{loginUser?.username?.substring(1)}</Avatar>{loginUser.username}<DownOutlined /></Space>
</a>
</Dropdown>
</Col>
</Row>
</Header>

Loading…
Cancel
Save