移动端

dev/chat
Lei OT 2 years ago
parent 02cc765e9a
commit 84ee40d1b8

@ -28,7 +28,7 @@ const router = createBrowserRouter([
element: <AuthApp />,
errorElement: <ErrorPage />,
children: [
{
{
element: <DesktopApp />,
children: [
{ index: true, element: <OrderFollow /> },
@ -40,11 +40,12 @@ const router = createBrowserRouter([
{ path: 'account/profile', element: <AccountProfile /> },
]
},
{
path: 'm',
{
path: 'm',
element: <MobileApp />,
children: [
{ path: 'conversation', element: <MobileConversation /> },
{ path: 'chat/:order_sn', element: <MobileChat /> },
{ path: 'chat', element: <MobileChat /> },
]
},

@ -10,7 +10,8 @@ import useAuthStore from '@/stores/AuthStore';
/**
* []
*/
const Conversations = () => {
const Conversations = ({ mobile }) => {
const routerReplace = mobile === undefined ? false : true;
const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams();
@ -22,8 +23,6 @@ const Conversations = () => {
const conversationsList = useConversationStore((state) => state.conversationsList);
const addToConversationList = useConversationStore((state) => state.addToConversationList);
const delConversationitem = useConversationStore((state) => state.delConversationitem);
const receivedMessageList = useConversationStore((state) => state.receivedMessageList);
const setMsgLoading = useConversationStore((state) => state.setMsgLoading);
const [tabSelectedConversation, setTabSelectedConversation] = useState({});
const [tabCnt, setTabCnt] = useState(-1);
@ -70,34 +69,16 @@ const Conversations = () => {
return false;
}
};
const getMessages = async (item) => {
setMsgLoading(true);
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number, lasttime: '' });
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[0].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
};
useEffect(() => {
const messagesList = activeConversations[`${currentConversation.sn}`] || [];
if (currentConversation.sn && messagesList.length < 20) {
getMessages(currentConversation);
}
return () => {};
}, [currentConversation.sn]);
const onSwitchConversation = async (item) => {
setCurrentConversation(item);
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
if (isEmpty(item.coli_sn)) {
navigate(`/order/chat`, { replace: true });
navigate(routePrefix, { replace: true });
} else {
setSwitchToC(item);
setShouldFetchCList(false);
navigate(`/order/chat/${item.coli_sn}`, { replace: true });
navigate(`${routePrefix}/${item.coli_sn}`, { replace: routePrefix });
}
// if (!isEmpty(item.coli_sn)) {
// setSwitchToC(item);
@ -113,7 +94,7 @@ const Conversations = () => {
await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn });
delConversationitem(item);
if (String(order_sn) === String(item.coli_sn)) {
navigate(`/order/chat`, { replace: true });
navigate(`/order/chat`, { replace: routerReplace });
}
};

@ -2,7 +2,7 @@ import { useState } from 'react';
import { Popover, Button } from 'antd';
import EmojiPicker from 'emoji-picker-react';
const InputTemplate = ({ disabled = false, inputEmoji }) => {
const InputTemplate = ({ mobile, disabled = false, inputEmoji }) => {
const [openPopup, setOpenPopup] = useState(false);
const handlePickEmoji = (emojiData) => {
@ -13,7 +13,7 @@ const InputTemplate = ({ disabled = false, inputEmoji }) => {
<>
<Popover
overlayClassName='p-0'
placement={'right'}
placement={mobile === undefined ? 'right' : 'top'}
overlayInnerStyle={{ padding: 0, borderRadius: '8px' }}
forceRender={true}
content={<EmojiPicker skinTonesDisabled={true} emojiStyle='google' onEmojiClick={handlePickEmoji} />}

@ -20,7 +20,7 @@ const splitTemplate = (template) => {
}, []);
return obj;
};
const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
const searchInputRef = useRef(null);
const { notification } = App.useApp();
const loginUser = useAuthStore((state) => state.loginUser);
@ -126,7 +126,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
return (
<>
<Popover
overlayClassName='w-3/5'
overlayClassName={[mobile === undefined ? 'w-3/5' : 'w-full'].join(' ')}
fresh
forceRender
destroyTooltipOnHide={true}

@ -46,14 +46,14 @@ const fileTypesExt = {
audio: ['aac', 'mp4', 'm4a', 'mp3', 'amr', 'ogg'],
};
const InputComposer = () => {
const userId = useAuthStore(state => state.loginUser.userId);
const websocket = useConversationStore(state => state.websocket);
const websocketOpened = useConversationStore(state => state.websocketOpened);
const currentConversation = useConversationStore(state => state.currentConversation);
const [referenceMsg, setReferenceMsg] = useConversationStore(state => [state.referenceMsg, state.setReferenceMsg]);
const [complexMsg, setComplexMsg] = useConversationStore(state => [state.complexMsg, state.setComplexMsg]);
const sentOrReceivedNewMessage = useConversationStore(state => state.sentOrReceivedNewMessage);
const InputComposer = ({ mobile }) => {
const userId = useAuthStore((state) => state.loginUser.userId);
const websocket = useConversationStore((state) => state.websocket);
const websocketOpened = useConversationStore((state) => state.websocketOpened);
const currentConversation = useConversationStore((state) => state.currentConversation);
const [referenceMsg, setReferenceMsg] = useConversationStore((state) => [state.referenceMsg, state.setReferenceMsg]);
const [complexMsg, setComplexMsg] = useConversationStore((state) => [state.complexMsg, state.setComplexMsg]);
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;
@ -120,15 +120,14 @@ const InputComposer = () => {
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
// olog('invoke upload send +++ ', contentToSend)
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
}
};
const { message } = App.useApp();
const [pastedUploading, setPastedUploading] = useState(false);
const readPasted = async (file, rename = false) => {
// 使 FileReader
const reader = new FileReader();
const suffix = file.name.slice(file.name.lastIndexOf('.')+1);
const suffix = file.name.slice(file.name.lastIndexOf('.') + 1);
const newName = `${uuid()}.${suffix}`; // rename ? `${uuid()}.${suffix}` : file.name;
const type = Object.keys(fileTypesExt).find((type) => fileTypesExt[type].includes(suffix));
const dataUri = aliOSSHost + newName;
@ -161,7 +160,7 @@ const InputComposer = () => {
let isNotFile = true;
for (let i = 0; i < items.length; i++) {
// if (items[i].type.indexOf("image") !== -1) {
if (items[i].kind.indexOf("file") !== -1) {
if (items[i].kind.indexOf('file') !== -1) {
isNotFile = false;
tmpfile = items[i].getAsFile();
break;
@ -180,17 +179,17 @@ const InputComposer = () => {
setComplexMsg(_tmpFile.msgData);
setPastedUploading(true);
const { file_url } = await postUploadFileItem(tmpfile, _tmpFile.newName);
setPastedUploading(false);
setComplexMsg({..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error'});
setPastedUploading(false); // todo: data uri
setComplexMsg({ ..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error' });
return;
}
};
const focusInput = () => {
textInputRef.current.focus({ cursor: 'end', preventScroll: true, });
textInputRef.current.focus({ cursor: 'end', preventScroll: true });
};
const addEmoji = emoji => {
setTextContent(prevValue => {
const addEmoji = (emoji) => {
setTextContent((prevValue) => {
return prevValue + emoji;
});
};
@ -248,7 +247,9 @@ const InputComposer = () => {
<Input.TextArea
onPaste={handlePaste}
ref={textInputRef}
size='large' maxLength={2000} showCount={textabled}
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支持复制粘贴 [截图/文件] 以备发送'}
rows={2}
disabled={!textabled}
@ -265,8 +266,8 @@ const InputComposer = () => {
/>
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'>
<Flex gap={4} className='*:text-primary *:rounded-none'>
<InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} />
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} />
<InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} {...{ mobile }} />
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} {...{ mobile }} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} />

@ -6,13 +6,20 @@ import MessagesList from './MessagesList';
import { fetchCleanUnreadMsgCount, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions';
import { fetchOrderConversationsList, } from '@/actions/ConversationActions';
import { isEmpty } from '@/utils/utils';
import useAuthStore from '@/stores/AuthStore';
const MessagesWrapper = () => {
const userId = useAuthStore((state) => state.loginUser.userId);
const [currentConversation, updateCurrentConversation, setCurrentConversation] = useConversationStore(useShallow((state) => [state.currentConversation, state.updateCurrentConversation, state.setCurrentConversation]));
const conversationsList = useConversationStore(useShallow((state) => state.conversationsList));
const activeConversations = useConversationStore(useShallow((state) => state.activeConversations));
const activeMessages = useConversationStore(useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn]: [])));
const addToConversationList = useConversationStore((state) => state.addToConversationList);
const receivedMessageList = useConversationStore((state) => state.receivedMessageList);
const setMsgLoading = useConversationStore((state) => state.setMsgLoading);
const [longList, setLongList] = useState([]);
const [longListLoading, setLongListLoading] = useState(false);
const [shouldScrollBottom, setShouldScrollBottom] = useState(true);
@ -29,6 +36,26 @@ const MessagesWrapper = () => {
return () => {};
}, [activeMessages, currentConversation.sn]);
const getMessages = async (item) => {
setMsgLoading(true);
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number, lasttime: '' });
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[0].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
};
useEffect(() => {
const messagesList = activeConversations[`${currentConversation.sn}`] || [];
if (currentConversation.sn && messagesList.length < 20) {
getMessages(currentConversation);
}
return () => {};
}, [currentConversation.sn]);
const getMoreMessages = async () => {
setShouldScrollBottom(false);
setLongListLoading(true);

@ -182,8 +182,11 @@
{
display: inline-block;
}
.chatwindow-wrapper .rce-mbox-file>button{
width: 100%;
}
/** Chat history */
/** Chat history ------------------------------------------------------------------------------------ */
/* .chathistory-wrapper .rce-mbox-time {
user-select: auto;
-webkit-user-select: auto;
@ -214,3 +217,15 @@
animation-name: message-box-default-focus-x;
animation-duration: 1s;
}
/**
* Mobile chat ------------------------------------------------------------------------------------
*/
.mobilechat-wrapper .rce-container-mbox .rce-mbox{
max-width: 320px;
}
.mobilechat-wrapper .rce-mtlink,
.mobilechat-wrapper .rce-mtlink-item{
min-width: unset;
}

@ -1,68 +1,26 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react'
import { App, Avatar, List, Layout, Input, DatePicker, Button, Spin } from 'antd'
import { ChatItem, MessageBox } from 'react-chat-elements'
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions'
import { isEmpty } from '@/utils/utils'
import useFormStore from '@/stores/FormStore'
import { useShallow } from 'zustand/react/shallow'
import { Layout } from 'antd';
import MessagesHeader from '@/views/Conversations/Components/MessagesHeader';
import MessagesWrapper from '@/views/Conversations/Components/MessagesWrapper';
import InputComposer from '@/views/Conversations/Components/InputComposer';
import { RightOutlined } from '@ant-design/icons'
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions'
import SearchInput from '@/components/SearchInput'
const { Sider, Content, Header, Footer } = Layout
const { TextArea } = Input
const { RangePicker } = DatePicker
const data = [
{
title: 'Tyler Dru Kelly',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'Hi Nazly Parlindungan Siregar, this is Sharon , travel advisor of Asia Highlights. We got your inquiry for your trip toJapan , are you available for a quick chat to discuss about your trip? I have some ideas to share with you . Looking forward to talking with you!',
},
{
title: 'Chhavi',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'Hi Sharon, thanks for reaching out. I am extremely busy person, please feel free to write down or send me any note and I will respond to you immediately. Cheers🙏',
},
{
title: 'Nathan Posey',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'It is too late to see cherry blossom from April 28, is it ok for you? For your 7 days tour, visit Tokyo and Kyoto is ok. I will suggest a tour with private car for you because your mother cannot walk too much.',
},
]
const { Content, Header, Footer } = Layout;
function Chat() {
const { notification } = App.useApp()
return (
<>
<List
itemLayout='horizontal'
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={item.avatarUrl} />}
title={<a href='https://ant.design'>{item.title}</a>}
description={item.msgTime}
/>
</List.Item>
)}
/>
<TextArea rows={4} placeholder='聊天窗口' maxLength={2000} />
<Button type='primary' onClick={() => {
notification.info({
message: '温馨提示',
description: '功能还在开发中,敬请期待',
placement: 'top',
duration: 60,
})
}}>发送</Button>
<Layout className='h-full chatwindow-wrapper mobilechat-wrapper' style={{ maxHeight: 'calc(100vh - 150px)', height: 'calc(100vh - 150px)', minWidth: '360px' }}>
<Header className='ant-layout-sider-light ant-card h-auto flex justify-between gap-1 items-center'>
<MessagesHeader />
</Header>
<Content className='flex-grow bg-whatsapp-bg relative'>
<MessagesWrapper />
</Content>
<Footer className='ant-layout-sider-light p-0'>
<InputComposer mobile />
</Footer>
</Layout>
</>
)
);
}
export default Chat
export default Chat;

@ -1,90 +1,12 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react'
import { Avatar, List, Button, Input, Layout, Select, DatePicker, Form, Spin } from 'antd'
import { ChatItem, MessageBox } from 'react-chat-elements'
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions'
import { isEmpty } from '@/utils/utils'
import useFormStore from '@/stores/FormStore'
import { useShallow } from 'zustand/react/shallow'
import { useNavigate, useHref } from 'react-router-dom'
import ConversationsList from '@/views/Conversations/Components/ConversationsList';
import { RightOutlined } from '@ant-design/icons'
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions'
import SearchInput from '@/components/SearchInput'
const { Sider, Content, Header, Footer } = Layout
const { Search } = Input
const { RangePicker } = DatePicker
const data = [
{
title: 'Tyler Dru Kelly',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: '15127570944',
},
{
title: 'Chhavi',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311112',
},
{
title: 'Nathan Posey',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311114',
},
{
title: 'Philip',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240312062',
},
{
title: 'Jeanne',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240313032',
},
{
title: 'Susan Puls',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240311213',
},
{
title: 'Ana Beatriz',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240312073',
},
{
title: 'Kathleen Anne Workman',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'HXY240104171',
},
]
function Login() {
const navigate = useNavigate()
function Conversation() {
return (
<>
<List
itemLayout='horizontal'
dataSource={data}
renderItem={(item, index) => (
<List.Item
onClick={() => {
navigate('/m/chat')}}
actions={[<RightOutlined key='goto' />]}
>
<List.Item.Meta
avatar={<Avatar src={item.avatarUrl} />}
title={<a href='https://ant.design'>{item.title}</a>}
description={item.msgTime}
/>
</List.Item>
)}
/>
</>
<div className='chatwindow-wrapper'>
<ConversationsList mobile={true} />
</div>
)
}
export default Login
export default Conversation

Loading…
Cancel
Save