From c8d5ca9091f88950f21b9aedf26d3f9a2073d4a6 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 30 Jan 2024 14:43:36 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E4=BC=9A=E8=AF=9D=E5=8F=91=E9=80=81?= =?UTF-8?q?=E5=92=8C=E6=8E=A5=E6=94=B6,=20=E6=9B=B4=E6=96=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- src/lib/msgUtils.js | 197 ++++++ src/lib/realTimeAPI.js | 13 +- src/main.jsx | 15 +- src/stores/ConversationContext.js | 112 ---- .../Conversations/ConversationContext.js | 621 ++++++++++++++++++ src/views/Conversations/App.jsx | 12 - src/views/Conversations/App1.jsx | 57 -- src/views/Conversations/Chat.css | 28 - src/views/Conversations/ChatApp.jsx | 41 -- src/views/Conversations/ChatApp2.jsx | 44 -- src/views/Conversations/ChatWindow.jsx | 116 ++-- .../Conversations/Components/ContactInfo.jsx | 3 +- .../Conversations/Components/ContactPanel.jsx | 3 +- .../Components/ConversationsList.jsx | 46 +- .../Components/CreatePayment.jsx | 32 + .../Components/CustomerProfile.jsx | 50 +- .../Conversations/Components/InputBox.jsx | 128 +++- .../Components/LocalTimeClock.jsx | 44 ++ .../Conversations/Components/Messages.jsx | 85 +-- .../Components/QuotesHistory.jsx | 64 ++ .../Conversations/ConversationProvider.jsx | 29 +- src/views/Conversations/Conversations.css | 21 +- src/views/Conversations/Index.jsx | 54 -- src/views/Conversations/Index2.jsx | 35 - tailwind.config.js | 8 +- 26 files changed, 1278 insertions(+), 584 deletions(-) create mode 100644 src/lib/msgUtils.js delete mode 100644 src/stores/ConversationContext.js create mode 100644 src/stores/Conversations/ConversationContext.js delete mode 100644 src/views/Conversations/App.jsx delete mode 100644 src/views/Conversations/App1.jsx delete mode 100644 src/views/Conversations/Chat.css delete mode 100644 src/views/Conversations/ChatApp.jsx delete mode 100644 src/views/Conversations/ChatApp2.jsx create mode 100644 src/views/Conversations/Components/CreatePayment.jsx create mode 100644 src/views/Conversations/Components/LocalTimeClock.jsx create mode 100644 src/views/Conversations/Components/QuotesHistory.jsx delete mode 100644 src/views/Conversations/Index.jsx delete mode 100644 src/views/Conversations/Index2.jsx diff --git a/package.json b/package.json index 6f5d71e..979d0b9 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,15 @@ "ahooks": "^3.7.8", "antd": "^5.12.8", "crypto-js": "^4.2.0", + "dayjs": "^1.11.10", "mobx": "^6.12.0", "mobx-react": "^9.1.0", "react": "^18.2.0", "react-chat-elements": "^12.0.11", "react-dom": "^18.2.0", "react-router-dom": "^6.21.1", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^9.0.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/src/lib/msgUtils.js b/src/lib/msgUtils.js new file mode 100644 index 0000000..44f1556 --- /dev/null +++ b/src/lib/msgUtils.js @@ -0,0 +1,197 @@ +import { cloneDeep, isEmpty } from "@/utils/utils"; +import { v4 as uuid } from "uuid"; + +export const replaceTemplateString = (str, replacements) => { + let result = str; + let keys = str.match(/{{(.*?)}}/g).map(key => key.replace(/{{|}}/g, '')); + + for (let i = 0; i < keys.length; i++) { + let replaceValue = replacements[i]; + let template = new RegExp(`{{${keys[i]}}}`, 'g'); + result = result.replace(template, replaceValue); + } + + return result; +} +/** + * +// +8618777396951 lyj +{ + "to": "+8613317835586", // qqs + "msgtype": "text", + "msgcontent": "{\"body\":\"txtmsgtest\"}" +} + */ +export const sentMsgTypeMapped = { + text: { + type: 'text', + contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, to: msg.to, msgtype: 'text', msgcontent: { body: msg.text } }), + contentToRender: (msg) => ({ ...msg, actionId: msg.id, conversationid: msg.id.split('.')[0], }), + }, + whatsappTemplate: { + contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, to: msg.to, msgtype: 'template', msgcontent: msg.template }), + contentToRender: (msg) => { + console.log(msg); + const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({...r, [v.type]: v}), {}) : null; + const templateParam = (templateDataMapped?.body?.parameters || []).map(e => e.text); + const fillTemplate = templateParam.length ? replaceTemplateString(msg.template_origin.components.body?.[0]?.text || '', templateParam) : (msg.template_origin.components.body?.[0]?.text || ''); + // const footer = msg.template_origin.components?.footer?.[0]?.text || ''; + return { + ...msg, + actionId: msg.id, + conversationid: msg.id.split('.')[0], + type: 'text', + title: msg.template_origin.components.header?.[0]?.text || '', + text: `${fillTemplate}`, // msg.template_origin.components.body?.[0]?.text || '', + }; + }, + }, +}; +const whatsappMsgMapped = { + 'whatsapp.inbound_message.received': { + getMsg: (result) => { + console.log('whatsapp.inbound_message.received', result); + return isEmpty(result?.whatsappInboundMessage) ? null : { ...result.whatsappInboundMessage, conversationid: result.conversationid }; + }, + contentToRender: (contentObj) => { + console.log('whatsapp.inbound_message.received to render', contentObj); + // const contentObj = result?.whatsappInboundMessage || result; // debug: + return parseRenderMessageItem(contentObj); + }, + contentToUpdate: () => null, + }, + 'whatsapp.message.updated': { + getMsg: (result) => { + console.log('getMsg', result); + return isEmpty(result?.whatsappMessage) ? null : { ...result.whatsappMessage, conversationid: result.conversationid }; + }, + contentToRender: () => null, // * 仅更新消息状态, 没有输出 + contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.wamid, status: msgStatusRenderMapped[(msgcontent?.status || 'failed')] }), + }, +}; +export const msgStatusRenderMapped = { + 'accepted': 'sent', + 'sent': 'sent', + 'delivered': 'received', + 'read': 'read', + 'failed': 'failed', +}; +export const receivedMsgTypeMapped = { + ...cloneDeep(whatsappMsgMapped), + 'message': { + // 发送消息的同步记录 status: 'accepted' + getMsg: (result) => ({ ...result, conversationid: result.actionId.split('.')[0] }), + contentToRender: () => null, + contentToUpdate: (msgcontent) => ({ + ...msgcontent, + actionId: msgcontent.actionId, + id: msgcontent.wamid, + status: msgStatusRenderMapped[(msgcontent?.status || 'failed')], + conversationid: msgcontent.actionId.split('.')[0], // msgcontent.conversation.id, + }), + }, + 'error': { + getMsg: (result) => result, + contentToRender: () => null, + contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.actionId, status: msgcontent?.status || 'failed', dateString: '发送失败 ❌' }), + }, +}; +export const whatsappMsgTypeMapped = { + error: { + type: (_m) => ({ type: 'system' }), + data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }), + }, + text: { + type: (msg) => ({ type: msg.errorCode ? 'system' : 'text' }), + data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }), + }, + image: { + type: 'photo', + data: (msg) => ({ + data: { id: msg.wamid, uri: msg.image.link, width: 200, height: 200, alt: '' }, + onOpen: () => { + console.log('Open image', msg.image.link); + }, + }), + }, + sticker: { + type: 'photo', + data: (msg) => ({ + data: { id: msg.wamid, uri: msg.sticker.link, width: 150, height: 120, alt: '' }, + }), + }, + video: { + type: 'video', + data: (msg) => ({ + data: { + id: msg.wamid, + videoURL: msg.video.link, + status: { + click: true, + loading: 0, + download: true, + }, + }, + }), + }, + audio: { + type: 'audio', + data: (msg) => ({ + id: msg.wamid, + data: { + audioURL: msg.audio.link, + }, + }), + }, + unsupported: { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) }, + reaction: { + type: 'text', + data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || msg.reaction?.text?.body || 'Reaction', reply: { message: '{content}', title: 'React from' } }), + }, + document: { + type: 'file', + data: (msg) => ({ id: msg.wamid, text: msg.document.filename, data: { uri: msg.document.link, extension: 'PDF', status: { click: false, loading: 0, } } }), + }, + // location: 'location', + // contact: 'contact', + // 'contact-card': 'contact-card', + // 'contact-card-with-photo': 'contact-card-with-photo', + // 'contact-card-with-photo-and-label': 'contact-card-with-photo-and-label', +}; +/** + * render received msg + */ +export const parseRenderMessageItem = (msg) => { + console.log('parseRenderMessageItem', msg); + return { + date: msg?.sendTime || '', + ...(whatsappMsgTypeMapped?.[msg.type]?.data(msg) || {}), + conversationid: msg.conversationid, + ...(typeof whatsappMsgTypeMapped[msg.type].type === 'function' ? whatsappMsgTypeMapped[msg.type].type(msg) : { type: whatsappMsgTypeMapped[msg.type].type || 'text' }), + // type: whatsappMsgTypeMapped?.[msg.type]?.type || 'text', + sender: msg.from, + status: msg?.status || 'waiting', + // title: msg.customerProfile.name, + // replyButton: true, + }; +}; +export const parseRenderMessageList = (messages) => { + return messages.map((msg) => { + return { + ...(whatsappMsgTypeMapped?.[msg.type]?.data(msg) || {}), + ...(whatsappMsgTypeMapped?.[msg.type]?.type(msg) || { type: 'text' }), // type: whatsappMsgTypeMapped?.[msg.type]?.type || 'text', + id: msg.id, + sender: msg.from, + // title: msg.customerProfile.name, + date: msg.sendTime, + }; + }); +}; + +/** + * WhatsApp Templates params + */ +export const whatsappTemplatesParamMapped = { + 'asia_highlights_has_receive_your_inquiry': [['customer_name']], + 'hello_from_asia_highlights': [['agent_name']], // todo: +}; diff --git a/src/lib/realTimeAPI.js b/src/lib/realTimeAPI.js index f0067c8..72a78fe 100644 --- a/src/lib/realTimeAPI.js +++ b/src/lib/realTimeAPI.js @@ -1,6 +1,7 @@ import { webSocket } from 'rxjs/webSocket'; -import { filter, buffer, map, tap } from 'rxjs/operators'; -// import { v4 as uuid } from "uuid"; +import { of, timer, concatMap } from 'rxjs'; +import { filter, buffer, map, tap, retryWhen, retry, delay, take, } from 'rxjs/operators'; +import { v4 as uuid } from "uuid"; export class RealTimeAPI { constructor(param) { @@ -8,7 +9,13 @@ export class RealTimeAPI { } getObservable() { - return this.webSocket; + return this.webSocket.pipe( + retry(10) + // retry({ + // count: 10, + // delay: () => timer(3000) + // }) + ); } disconnect() { diff --git a/src/main.jsx b/src/main.jsx index 7377936..339c1a7 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,7 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { configure } from 'mobx' -import { createBrowserRouter, RouterProvider } from 'react-router-dom' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { configure } from 'mobx'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { AuthContext } from '@/stores/AuthContext' import { ThemeContext } from '@/stores/ThemeContext' import ConversationProvider from '@/views/Conversations/ConversationProvider' @@ -25,7 +25,7 @@ configure({ observableRequiresReaction: false, reactionRequiresObservable: true, disableErrorBoundaries: process.env.NODE_ENV == 'production', -}) +}); const router = createBrowserRouter([ { @@ -37,6 +37,7 @@ const router = createBrowserRouter([ { path: 'order/follow', element: }, { path: 'chat/history', element: }, { path: 'sales/management', element: }, + { path: 'order/chat/:order_sn', element: }, { path: 'order/chat', element: }, { path: 'account/profile', element: }, ], @@ -48,7 +49,7 @@ const router = createBrowserRouter([ { path: 'dingding/qrcode', element: }, ], }, -]) +]); ReactDOM.createRoot(document.getElementById('root')).render( // @@ -60,4 +61,4 @@ ReactDOM.createRoot(document.getElementById('root')).render( // -) +); diff --git a/src/stores/ConversationContext.js b/src/stores/ConversationContext.js deleted file mode 100644 index f4f3cf4..0000000 --- a/src/stores/ConversationContext.js +++ /dev/null @@ -1,112 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; -import { RealTimeAPI } from '@/lib/realTimeAPI'; -import { useGetJson } from '@/hooks/userFetch'; - -export const ConversationContext = createContext(); - -const API_HOST = 'http://127.0.0.1:4523/m2/3888351-0-default'; // local mock -const URL = { - conversationList: `${API_HOST}/142426823`, - templates: `${API_HOST}/142952738`, -}; -// const WS_URL = 'ws://202.103.68.144:8888/whatever/'; -const WS_URL = 'ws://202.103.68.157:8888/whatever/'; -// let realtimeAPI = new RealTimeAPI({ url: URL, protocol: 'aaa' }); -// let realtimeAPI = new RealTimeAPI({ url: WS_URL, protocol: 'WhatsApp' }); - -export const useConversations = () => { - const [errors, setErrors] = useState([]); - const [messages, setMessages] = useState([]); // 页面上激活的对话 - const [conversations, setConversations] = useState({}); // 所有对话 - const [currentID, setCurrentID] = useState(); - const [conversationsList, setConversationsList] = useState([]); // 对话列表 - const [currentConversation, setCurrentConversation] = useState({ - id: '', name: '' - }); - const [templates, setTemplates] = useState([]); - - const [url, setUrl] = useState(URL.conversationList); - const data = useGetJson(url); - const fetchConversations = () => { - setUrl(null); // reset url - setUrl(URL.conversationList); - } - useEffect(() => { - setConversationsList(data); - if (data && data.length) { - switchConversation(data[0]); - } - - return () => {}; - }, [data]); - - - const getTemplates = () => { - setUrl(null); // reset url - setUrl(URL.templates); - } - - const switchConversation = (cc) => { - console.log('switch to ', cc.id, cc); - setCurrentID(cc.id); - setCurrentConversation(cc); - setMessages(conversations[cc.id] || []); - }; - - const addError = (reason) => { - setErrors(prevErrors => [...prevErrors, { reason }]); - } - - const addMessageToConversations = (customerId, message) => { - setConversations((prevList) => ({ - ...prevList, - [customerId]: [...(prevList[customerId] || []), message], - })); - }; - - const addMessage = (message) => { - setMessages((prevMessages) => [...prevMessages, message]); - addMessageToConversations(currentConversation.id, message); - }; - - const handleMessage = (data) => { - const { errmsg, result: msgObj } = data; - - const msg = data.result; - if (!msg) { - return false; - } - if (typeof msg.type === 'string' && msg.type === 'error') { - addError('Error Connecting to Server'); - } - addMessage({ ...msg.message, sender: 'other', id: Date.now().toString(16) }); - }; - - const sendMessage = (msg) => { - const msgObj = { - type: 'message', - message: msg, - }; - // realtimeAPI.sendMessage(msgObj); - addMessage(msgObj.message); - // debug: - // const msgObjR = { - // type: 'message', - // message: { type: 'text', text: { body: 'Received: ' + msg.text.body,} }, - // }; - // addMessage({ ...msgObjR.message, sender: 'other', id: Date.now().toString(16) }); - }; - - // realtimeAPI.onError(addError.bind(null, 'Error')); - // realtimeAPI.onMessage(handleMessage); - // realtimeAPI.onCompletion(addError.bind(null, 'Not Connected to Server')); - // realtimeAPI.keepAlive(); // Ping Server - - return { - errors, messages, conversationsList, currentConversation, sendMessage, - fetchConversations, switchConversation, - templates, setTemplates, getTemplates, - }; -} - -export const useConversationContext = () => useContext(ConversationContext); diff --git a/src/stores/Conversations/ConversationContext.js b/src/stores/Conversations/ConversationContext.js new file mode 100644 index 0000000..10c4ec2 --- /dev/null +++ b/src/stores/Conversations/ConversationContext.js @@ -0,0 +1,621 @@ +import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; +import { RealTimeAPI } from '@/lib/realTimeAPI'; +import { receivedMsgTypeMapped, sentMsgTypeMapped } from '@/lib/msgUtils'; +import { groupBy, isEmpty } from '@/utils/utils'; + +export const ConversationContext = createContext(); +export const useConversationContext = () => useContext(ConversationContext); +export async function fetchJSON(url, data) { + let params = ''; + let ifp = ''; + if (data) { + params = new URLSearchParams(data).toString(); + ifp = params ? '?' : ifp; + } + ifp = url.includes('?') ? '' : ifp; + const host = /^https?:\/\//i.test(url) ? '' : ''; // HT_HOST; + const response = await fetch(`${host}${url}${ifp}${params}`); + return await response.json(); +} + +// const API_HOST = 'http://202.103.68.144:8888'; +const API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_callback'; + +export const useConversations = ({loginUser, realtimeAPI}) => { + const { userId } = loginUser; + + const [errors, setErrors] = useState([]); + const [messages, setMessages] = useState([]); // active conversation + const [activeConversations, setActiveConversations] = useState({}); // all active conversation + const [currentID, setCurrentID] = useState(); + const [conversationsList, setConversationsList] = useState([]); // open conversations + const [currentConversation, setCurrentConversation] = useState({ sn: '', customer_name: '', coli_sn: '' }); + const currentConversationRef = useRef(currentConversation); + useEffect(() => { + currentConversationRef.current = currentConversation; + }, [currentConversation]); + + useEffect(() => { + console.log(errors, 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'); + + return () => {}; + }, [errors]) + + useEffect(() => { + realtimeAPI.onError(addError.bind(null, 'Error')); + realtimeAPI.onMessage(handleMessage); + realtimeAPI.onCompletion(addError.bind(null, 'Not Connected to Server')); + realtimeAPI.keepAlive(); // Ping Server + + // Cleanup function to remove the event listeners when the component is unmounted + return () => { + realtimeAPI.disconnect(); + }; + }, []); + + useEffect(() => { + getConversationsList(); + getTemplates(); + return () => {}; + }, []); + + // useEffect(() => { + // if (!currentConversation.id && conversationsList.length > 0) { + // switchConversation(conversationsList[0]); + // } + // return () => {}; + // }, [conversationsList]); + + + const getConversationsList = async () => { + const { result: data } = await fetchJSON(`${API_HOST}/getconversations`, { opisn: userId }); + // const _data = []; + const _data = testConversations; + const list = [..._data, ...data]; + const dataMapped = list.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); + setConversationsList(list); + setActiveConversations({...dataMapped, ...activeConversations}); + console.log(list, dataMapped); + if (list && list.length) { + switchConversation(list[0]); + } + }; + + // const templates = AllTemplates.filter((_t) => _t.status !== 'REJECTED').map((ele) => ({ ...ele, components: groupBy(ele.components, (_c) => _c.type.toLowerCase()) })); // test: 0 + // const [templates, setTemplates] = useState([]); + const [templatesList, setTemplatesList] = useState([]); + const getTemplates = async () => { + const data = await fetchJSON(`${API_HOST}/listtemplates`); + const canUseTemplates = (data?.result?.items || []).filter((_t) => _t.status !== 'REJECTED').map((ele) => ({ ...ele, components: groupBy(ele.components, (_c) => _c.type.toLowerCase()) })); + setTemplatesList(canUseTemplates); + }; + + const [customerOrderProfile, setCustomerProfile] = useState({}); + const getCustomerProfile = async (colisn) => { + const { result } = await fetchJSON(`${API_HOST}/getorderinfo`, { colisn }); + const data = result?.[0] || {}; + setCustomerProfile(data); + + if (!isEmpty(data.conversation)) { + setConversationsList((pre) => [...data.conversations, ...pre]); + setCurrentConversation(data.conversation[0]); + const thisCMapped = data.conversation.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); + setActiveConversations((pre) => ({ ...pre, ...thisCMapped })); + setMessages([]); // todo: 获取当前会话的历史消息 + } else { + // reset chat window + setMessages([]); + setCurrentConversation({ sn: '', customer_name: '', coli_sn: '' }); + // todo: 加入新会话 + } + }; + + const switchConversation = (cc) => { + setCurrentID(`${cc.sn}`); + setCurrentConversation({...cc, id: cc.sn, customer_name: cc.whatsapp_name}); + // Get customer profile when switching conversation + // getCustomerProfile(??); + }; + + // Get customer profile when switching conversation + useEffect(() => { + console.log('currentConversation', currentConversation); + // const colisn = currentConversation.coli_sn; + // getCustomerProfile(colisn); + setMessages([...(activeConversations[currentID] || [])]); + return () => {}; + }, [currentConversation]); + + /** + * ***************************************************************************************************** + * websocket -------------------------------------------------------------------------------------------- + * ***************************************************************************************************** + */ + const addError = (reason) => { + setErrors((prevErrors) => [...prevErrors, { reason }]); + }; + + const addMessageToConversations = (targetId, message) => { + setActiveConversations((prevList) => ({ + ...prevList, + [targetId]: [...(prevList[targetId] || []), message], + })); + // console.log('activeConversations', activeConversations); + if (targetId !== currentID) { + setConversationsList((prevList) => { + return prevList.map((ele) => { + if (ele.id === targetId) { + return { ...ele, new_msgs: ele.new_msgs + 1 }; + } + return ele; + }); + }); + } + }; + + useEffect(() => { + console.log(messages, 'messages'); + + return () => { + + } + }, [messages]) + + + const addMessage = (message) => { + setMessages((prevMessages) => [...prevMessages, message]); + // addMessageToConversations(currentConversationRef.current.sn, message); + addMessageToConversations(message.conversationid, message); + }; + + const updateMessage = (message) => { + let targetMsgs; + setMessages((prevMessages) => { + targetMsgs = prevMessages.map(ele => { + if (ele.id === ele.actionId && ele.actionId === message.actionId) { + return {...ele, id: message.id, status: message.status, dateString: message.dateString}; + } + else if (ele.id === message.id) { + return {...ele, id: message.id, status: message.status, dateString: message.dateString}; + } + return ele; + }); + return targetMsgs; + }); + // 更新会话中的消息 + const targetId = message.conversationid; // currentConversationRef.current.sn; + setActiveConversations((prevList) => ({ + ...prevList, + [targetId]: targetMsgs, + })); + + }; + + const handleMessage = (data) => { + console.log('handleMessage------------------', ); + /** + * ! event handlers in JavaScript capture the environment (including variables) at the time they are defined, not when they are executed. + */ + const { errcode, errmsg, result } = data; + + if (!result) { + return false; + } + let resultType = result?.action || result.type; + if (errcode !== 0) { + // addError('Error Connecting to Server'); + resultType = 'error'; + } + console.log(resultType, 'result.type'); + const msgObj = receivedMsgTypeMapped[resultType].getMsg(result); + const msgRender = receivedMsgTypeMapped[resultType].contentToRender(msgObj); + const msgUpdate = receivedMsgTypeMapped[resultType].contentToUpdate(msgObj); + console.log('msgRender msgUpdate', msgRender, msgUpdate); + if ( ['whatsapp.message.updated', 'message', 'error'].includes(resultType)) { + updateMessage(msgUpdate); + // return false; + } + if ( ! isEmpty(msgRender)) { + addMessage(msgRender); + } + console.log('handleMessage*******************', ); + }; + + const sendMessage = (msgObj) => { + const contentToSend = sentMsgTypeMapped[msgObj.type].contentToSend(msgObj); + realtimeAPI.sendMessage({ ...contentToSend, opi_sn: userId }); + const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj); + console.log(contentToRender, 'contentToRender sendMessage------------------'); + addMessage(contentToRender); + }; + + return { + errors, + messages, + conversationsList, + currentConversation, + sendMessage, + getConversationsList, + switchConversation, + templates: templatesList, // setTemplates, getTemplates, + // templates, // debug: 0 + customerOrderProfile, getCustomerProfile + }; +}; + + + + + + + + + +// test: 0 "type": "whatsapp.inbound_message.received", +const all = [ +// { +// 'id': '65b06828619a1d82777eb4c6', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBQzNDNzBFNjFCREJBNDIyQjQ2AA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:30:14.000Z', +// 'type': 'image', +// 'image': { +// 'link': +// 'https://api.ycloud.com/v2/whatsapp/media/download/934379820978291?sig=t%3D1706059814%2Cs%3D91a79a0e4007ad2f6a044a28307affe663f7f81903b3537bd80e758d3c0d0563&payload=eyJpZCI6IjkzNDM3OTgyMDk3ODI5MSIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNjgyODYxOWExZDgyNzc3ZWI0YzYiLCJtaW1lVHlwZSI6ImltYWdlL2pwZWciLCJzaGEyNTYiOiJPVTJjdkN2eHplMUdMMmQ5NUxyTGVaNmpNb2ZscUZYM1RvcXdTTUNWZkxNPSJ9', +// 'id': '934379820978291', +// 'sha256': 'OU2cvCvxze1GL2d95LrLeZ6jMoflqFX3ToqwSMCVfLM=', +// 'mime_type': 'image/jpeg', +// }, +// }, +// { +// 'id': '65b06ce6619a1d8277c97fc0', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBMUJBOUZCODY4NkNBMkM2NUEzAA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:50:29.000Z', +// 'type': 'text', +// 'text': { +// 'body': 'eeee', +// }, +// }, +// { +// 'id': '65b06b2f619a1d8277b5ab06', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBRkU0RUZGRUI1OUQzQUFBMEExAA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:43:09.000Z', +// 'type': 'audio', +// 'audio': { +// 'link': +// 'https://api.ycloud.com/v2/whatsapp/media/download/901696271448320?sig=t%3D1706060589%2Cs%3Dca75dbd57e4867783390c913491263f07c9738d69c141d4ae622c76df9fa033b&payload=eyJpZCI6IjkwMTY5NjI3MTQ0ODMyMCIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNmIyZjYxOWExZDgyNzdiNWFiMDYiLCJtaW1lVHlwZSI6ImF1ZGlvL29nZzsgY29kZWNzPW9wdXMiLCJzaGEyNTYiOiJoZUNSUDdEMjM3bG9ydkZ4eFhSdHZpU1ZsNDR3Rlk4TytaMFhic2k5cy9rPSJ9', +// 'id': '901696271448320', +// 'sha256': 'heCRP7D237lorvFxxXRtviSVl44wFY8O+Z0Xbsi9s/k=', +// 'mime_type': 'audio/ogg; codecs=opus', +// }, +// }, +// { +// 'id': '65b06b12619a1d8277b3c0c4', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBREZEMEM0MURDNjJGREVEQjY3AA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:42:40.000Z', +// 'type': 'video', +// 'video': { +// 'link': +// 'https://api.ycloud.com/v2/whatsapp/media/download/742404324517058?sig=t%3D1706060560%2Cs%3D53eeb1508c2103e310fb14a72563a8e07c5a84c7e6192a25f3608ac9bea32334&payload=eyJpZCI6Ijc0MjQwNDMyNDUxNzA1OCIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNmIxMjYxOWExZDgyNzdiM2MwYzQiLCJtaW1lVHlwZSI6InZpZGVvL21wNCIsInNoYTI1NiI6IlNJcjRlZFlPb1BDTGtETEgrVTY2d3dkMDgra2JndFV5OHRDd2RjQU5FaFU9In0', +// 'caption': 'and', +// 'id': '742404324517058', +// 'sha256': 'SIr4edYOoPCLkDLH+U66wwd08+kbgtUy8tCwdcANEhU=', +// 'mime_type': 'video/mp4', +// }, +// }, +// { +// 'id': '65b06aa7619a1d8277ac806e', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBOTFBOTU5RDE2QjgxQTQ1MEE2AA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:40:53.000Z', +// 'type': 'sticker', +// 'sticker': { +// 'link': +// 'https://api.ycloud.com/v2/whatsapp/media/download/1156118002042289?sig=t%3D1706060453%2Cs%3Dfbd5f881856614e35715b1e3e1097b3bbe56f8a36aaa67bfbef25a37d9143d51&payload=eyJpZCI6IjExNTYxMTgwMDIwNDIyODkiLCJ3YWJhSWQiOiIxOTAyOTAxMzQxNTY4ODAiLCJpbmJvdW5kTWVzc2FnZUlkIjoiNjViMDZhYTc2MTlhMWQ4Mjc3YWM4MDZlIiwibWltZVR5cGUiOiJpbWFnZS93ZWJwIiwic2hhMjU2IjoibUNaLzdhNnNaNlRNYTE0WW9rUkNTZnVsdGpZNmFRRVZFNVoxMVRwanNQOD0ifQ', +// 'id': '1156118002042289', +// 'sha256': 'mCZ/7a6sZ6TMa14YokRCSfultjY6aQEVE5Z11TpjsP8=', +// 'mime_type': 'image/webp', +// 'animated': false, +// }, +// }, +// { +// 'id': '65b06a91619a1d8277aaf05e', +// 'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBRjUxNzdCQ0FEOTlFQzc5MzQ1AA==', +// 'wabaId': '190290134156880', +// 'from': '+8613317835586', +// 'customerProfile': { +// 'name': 'qqs', +// }, +// 'to': '+8617607730395', +// 'sendTime': '2024-01-24T01:40:32.000Z', +// 'type': 'unsupported', +// 'errors': [ +// { +// 'code': '131051', +// 'title': 'Message type unknown', +// 'message': 'Message type unknown', +// 'error_data': { +// 'details': 'Message type is currently not supported.', +// }, +// }, +// ], +// }, +// { +// "id": "65b38323619a1d827778986d", +// "wamid": "wamid.HBgMOTE4NTg3OTAxMDg2FQIAEhgWM0VCMDMwMjc5OThGN0EyN0JERjY5QwA=", +// "wabaId": "190290134156880", +// "from": "+918587901086", +// "customerProfile": { +// "name": "Shailesh" +// }, +// "to": "+8617607730395", +// "sendTime": "2024-01-26T10:02:09.000Z", +// "type": "document", +// "document": { +// "link": "https://api.ycloud.com/v2/whatsapp/media/download/773594381476592?sig=t%3D1706263329%2Cs%3D0e0221fc921b29f6e2f896a2f62a3b99a60d09b40e7332d509e7b0cd2a4a630e&payload=eyJpZCI6Ijc3MzU5NDM4MTQ3NjU5MiIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsIndhbWlkIjoid2FtaWQuSEJnTU9URTROVGczT1RBeE1EZzJGUUlBRWhnV00wVkNNRE13TWpjNU9UaEdOMEV5TjBKRVJqWTVRd0E9IiwibWltZVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQud29yZHByb2Nlc3NpbmdtbC5kb2N1bWVudCIsInNoYTI1NiI6ImFUK3hnSGRhaGNtekVFa0g3bmdZbHloVSt1R2ZuTnIvdlEwNGtKSTBaMFU9In0", +// "filename": "NEPAL INDIA TOUR - NOV 2024 - revised.docx", +// "id": "773594381476592", +// "sha256": "aT+xgHdahcmzEEkH7ngYlyhU+uGfnNr/vQ04kJI0Z0U=", +// "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" +// } +// } +]; +const all2 = [ + { + 'id': '63f71fb8741c165b434292fb', + 'wamid': 'wamid.HBgNOD...', + 'wabaId': 'WABA-ID', + 'from': 'CUSTOMER-PHONE-NUMBER', + 'customerProfile': { + 'name': 'Joe', + }, + 'to': 'BUSINESS-PHONE-NUMBER', + 'sendTime': '2023-02-22T12:00:00.000Z', + 'type': 'reaction', + 'reaction': { + 'message_id': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBMUJBOUZCODY4NkNBMkM2NUEzAA==', + 'emoji': '👍', + }, + }, + { + 'id': '65b1de2f3f0bb66a91377930', + 'wamid': 'wamid.HBgNODYxODc3NzM5Njk1MRUCABEYEkM4NTU5MjMyRDFCRkE5NjM2RAA=', + 'status': 'sent', + 'from': '+8617607730395', + 'to': '+8618777396951', + 'wabaId': '190290134156880', + 'type': 'template', + 'template': { + 'name': 'hello', + 'language': { + 'code': 'zh_CN', + }, + }, + 'conversation': { + 'id': 'ddb659076d0e970f16fb19520c116a1b', + 'originType': 'marketing', + 'expireTime': '2024-01-26T04:07:00.000Z', + }, + 'createTime': '2024-01-25T04:06:07.850Z', + 'updateTime': '2024-01-25T04:06:09.151Z', + 'sendTime': '2024-01-25T04:06:08.000Z', + 'totalPrice': 0.0782, + 'currency': 'USD', + 'bizType': 'whatsapp', + }, +]; + +const templateExample = [ + { + 'id': 'evt_eEMtA0PkkyACiS5o', + 'type': 'whatsapp.template.reviewed', + 'apiVersion': 'v2', + 'createTime': '2023-02-20T12:00:00.000Z', + 'whatsappTemplate': { + 'wabaId': 'WABA-ID', + 'name': 'template_name', + 'language': 'en', + 'category': 'MARKETING', + 'status': 'APPROVED', + 'reason': 'NONE', + 'statusUpdateEvent': 'APPROVED', + }, + }, +]; + +const AllTemplates = [ + { + 'wabaId': '190290134156880', + 'name': 'say_hi', + 'language': 'en', + 'components': [ + { + 'type': 'BODY', + 'text': + "Hi {{customer_name}} I'm {{your_name}} from Asia Highlights.\n\nWe provide *families* and *couples* with personalized and stress-free experiences, whether for _milestone trips_, _birthday trips_, _graduation trips_, or _bucketlist trips_.", + 'example': { + 'body_text': [['Mike', 'Jimmy']], + }, + }, + { + 'type': 'BUTTONS', + 'buttons': [ + { + 'type': 'URL', + 'text': 'Asia Highlights', + 'url': 'https://www.asiahighlights.com/', + }, + ], + }, + ], + 'category': 'UTILITY', + 'status': 'REJECTED', + 'qualityRating': 'UNKNOWN', + 'reason': 'INCORRECT_CATEGORY', + 'createTime': '2024-01-23T02:26:33.012Z', + 'updateTime': '2024-01-23T02:26:35.397Z', + 'statusUpdateEvent': 'REJECTED', + }, + { + 'wabaId': '190290134156880', + 'name': 'i_hope_this_message_finds_you_well', + 'language': 'en', + 'components': [ + { + 'type': 'BODY', + 'text': 'Hi {{customer_name}}, I hope this message finds you well. Did you see my previous message?', + 'example': { + 'body_text': [['Mike']], + }, + }, + ], + 'category': 'UTILITY', + 'status': 'REJECTED', + 'qualityRating': 'UNKNOWN', + 'reason': 'INCORRECT_CATEGORY', + 'createTime': '2024-01-23T02:22:20.232Z', + 'updateTime': '2024-01-23T02:22:22.937Z', + 'statusUpdateEvent': 'REJECTED', + }, + { + 'wabaId': '190290134156880', + 'name': 'asia_highlights_has_receive_your_inquiry', + 'language': 'en', + 'components': [ + { + 'type': 'BODY', + 'text': + 'Dear {{customer_name}},\n\nThank you for choosing Asia Highlights. Your inquiry has been submitted to Asia Highlights. One of our travel advisors will respond within 24 hours.', + 'example': { + 'body_text': [['Jimmy Liow']], + }, + }, + { + 'type': 'HEADER', + 'format': 'TEXT', + 'text': 'Asia highlights has receive your inquiry', + }, + { + 'type': 'FOOTER', + 'text': 'Kind regards, Asia Highlights Team', + }, + { + 'type': 'BUTTONS', + 'buttons': [ + { + 'type': 'URL', + 'text': 'Asia Highlights', + 'url': 'https://www.asiahighlights.com/', + }, + ], + }, + ], + 'category': 'UTILITY', + 'status': 'APPROVED', + 'qualityRating': 'UNKNOWN', + 'reason': 'NONE', + 'createTime': '2024-01-19T05:59:32.933Z', + 'updateTime': '2024-01-19T05:59:55.581Z', + 'statusUpdateEvent': 'APPROVED', + }, + { + 'wabaId': '190290134156880', + 'name': 'hello', + 'language': 'zh_CN', + 'components': [ + { + 'type': 'BODY', + 'text': '你好,这是一个测试程序', + }, + { + 'type': 'HEADER', + 'format': 'TEXT', + 'text': 'Hello 同学', + }, + { + 'type': 'FOOTER', + 'text': 'Global Highlights', + }, + { + 'type': 'BUTTONS', + 'buttons': [ + { + 'type': 'URL', + 'text': 'about us', + 'url': 'https://www.globalhighlights.com/', + }, + ], + }, + ], + 'category': 'MARKETING', + 'status': 'APPROVED', + 'qualityRating': 'UNKNOWN', + 'reason': 'NONE', + 'createTime': '2023-11-17T03:26:10.961Z', + 'updateTime': '2023-11-17T13:36:33.623Z', + 'statusUpdateEvent': 'APPROVED', + }, +]; + +const testConversations = [ + { + 'sn': 3001, + 'opi_sn': 354, + 'coli_sn': 0, + 'whatsapp_phone_number': '+8618777396951', + "last_received_time": new Date().toDateString(), + "last_send_time": new Date().toDateString(), + 'unread_msg_count': Math.floor(Math.random() * (100 - 2 + 1) + 2), + 'whatsapp_name': 'LiaoYijun', + 'customer_name': 'LiaoYijun', + }, + { + 'sn': 3002, + 'opi_sn': 354, + 'coli_sn': 0, + 'whatsapp_phone_number': '+8613317835586', + "last_received_time": new Date().toDateString(), + "last_send_time": new Date().toDateString(), + 'unread_msg_count': Math.floor(Math.random() * (100 - 2 + 1) + 2), + 'whatsapp_name': 'QinQianSheng', + 'customer_name': 'QinQianSheng', + }, + // { + // 'sn': 3003, + // 'opi_sn': 354, + // 'coli_sn': 240129003, + // 'whatsapp_phone_number': '+8618777396951', + // "last_received_time": new Date().toDateString(), + // "last_send_time": new Date().toDateString(), + // 'unread_msg_count': Math.floor(Math.random() * (100 - 2 + 1) + 2), + // 'whatsapp_name': 'LeiYuanTing', + // }, +]; diff --git a/src/views/Conversations/App.jsx b/src/views/Conversations/App.jsx deleted file mode 100644 index e54cd21..0000000 --- a/src/views/Conversations/App.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import Chat from './Index'; -// import 'antd/dist/reset.css'; - -function App() { - return ( -
- -
- ); -} - -export default App; diff --git a/src/views/Conversations/App1.jsx b/src/views/Conversations/App1.jsx deleted file mode 100644 index b1a985d..0000000 --- a/src/views/Conversations/App1.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useState } from 'react'; -import { List, Input, Avatar, Button } from 'antd'; - -const messages = [ - { - sender: 'John Doe', - message: 'Hey!', - }, - { - sender: 'Jane Doe', - message: 'Hi John!', - }, -]; - -function App() { - const [message, setMessage] = useState(''); - - const sendMessage = () => { - // Update messages with new message data - const newMessage = { - sender: 'You', - message, - }; - messages.push(newMessage); - setMessage(''); - }; - - return ( -
- ( - - } - title={message.sender} - description={message.message} - /> - - )} - /> - - setMessage(e.target.value)} - /> - - -
- ); -} - -export default App; diff --git a/src/views/Conversations/Chat.css b/src/views/Conversations/Chat.css deleted file mode 100644 index 7d22a0c..0000000 --- a/src/views/Conversations/Chat.css +++ /dev/null @@ -1,28 +0,0 @@ -.chat-layout { - height: 100vh; -} - -.chat-sider { - overflow: auto; -} - -.chat-content { - padding: 24px; - overflow: auto; -} - -.chat-input { - position: fixed; - bottom: 0; - width: calc(100% - 300px); - display: flex; - padding: 24px; -} - -.chat-input .ant-input { - margin-right: 24px; -} - -.chat-button { - height: 40px; -} \ No newline at end of file diff --git a/src/views/Conversations/ChatApp.jsx b/src/views/Conversations/ChatApp.jsx deleted file mode 100644 index 73d043f..0000000 --- a/src/views/Conversations/ChatApp.jsx +++ /dev/null @@ -1,41 +0,0 @@ -// App.js - -import React from 'react'; -import { Layout, Menu } from 'antd'; -// import './App.css'; - -const { Header, Sider, Content } = Layout; - -function App() { - const channels = ['General', 'Random']; - const messages = [ - { user: 'User1', text: 'Hello!' }, - { user: 'User2', text: 'Hi!' }, - ]; - - return ( - - - - {channels.map(channel => ( - {channel} - ))} - - - -
- -
- {messages.map((message, index) => ( -

- {message.user}: {message.text} -

- ))} -
-
- - - ); -} - -export default App; diff --git a/src/views/Conversations/ChatApp2.jsx b/src/views/Conversations/ChatApp2.jsx deleted file mode 100644 index e3e8114..0000000 --- a/src/views/Conversations/ChatApp2.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Layout, Menu, List, Timeline, Input } from 'antd'; - -const { Sider, Content } = Layout; -function ChatApp() { - return ( - - - - Unread - Mentions - Favorites - Channel List - Direct Messages - - - - {/* Show channels and DMs */} - - - - - - - - {/* Show user profile cards */} - - - - - - {/* Show messages */} - - - - - - - - ); -} - -export default ChatApp; diff --git a/src/views/Conversations/ChatWindow.jsx b/src/views/Conversations/ChatWindow.jsx index db34d65..e121bcd 100644 --- a/src/views/Conversations/ChatWindow.jsx +++ b/src/views/Conversations/ChatWindow.jsx @@ -1,81 +1,81 @@ -import { useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { Layout, List, Avatar, Flex, Typography } from 'antd'; +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from "react-router-dom"; +import { Layout, List, Avatar, Flex, Typography, Spin } from 'antd'; import Messages from './Components/Messages'; import InputBox from './Components/InputBox'; import ConversationsList from './Components/ConversationsList'; import CustomerProfile from './Components/CustomerProfile'; +import LocalTimeClock from './Components/LocalTimeClock'; -import { useConversationContext } from '@/stores/ConversationContext'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; import './Conversations.css'; import { useAuthContext } from '@/stores/AuthContext.js'; const { Sider, Content, Header, Footer } = Layout; -const CList = [ - { name: 'Customer_1', label: 'Customer_1', key: 'Customer_1', value: 'Customer_1' }, - { name: 'Customer_2', label: 'Customer_2', key: 'Customer_2', value: 'Customer_2' }, - { name: 'Customer_3', label: 'Customer_3', key: 'Customer_3', value: 'Customer_3' }, - { name: 'Customer_4', label: 'Customer_4', key: 'Customer_4', value: 'Customer_4' }, -]; /** * */ -const ChatWindow = observer(() => { +const ChatWindow = (() => { + const { order_sn } = useParams(); const { loginUser: currentUser } = useAuthContext(); - const { errors, messages, sendMessage, currentConversation } = useConversationContext(); + const { errors, sendMessage, currentConversation, customerOrderProfile: orderInfo, getCustomerProfile } = useConversationContext(); + const { quotes, contact, last_contact, ...order } = orderInfo; + + // console.log(order_sn, currentUser); + useEffect(() => { + if (order_sn) { + getCustomerProfile(order_sn); + } + + return () => {}; + }, [order_sn]); + return ( - - - - + + + + + - - -
- - {currentConversation.name} - - {currentConversation.name} - {/*
HXY231119017
*/} + + +
+ + + + + {currentConversation.customer_name} + {currentConversation.whatsapp_phone_number} + + + + {/* {order?.location} */} + + {/* {customerDateTime} */} + + - - {/* ( - mark]}> - - {item.name} - - } - title={item.name} - description='{最近的消息}' - /> - - )} - /> */} -
- -
- -
-
-
- sendMessage(v)} /> -
-
- {/* sendMessage(v)} /> */} -
+
+ +
+ +
+
+
+ sendMessage(v)} /> +
+
+ {/* sendMessage(v)} /> */} +
- - - -
+ + + +
+ ); }); diff --git a/src/views/Conversations/Components/ContactInfo.jsx b/src/views/Conversations/Components/ContactInfo.jsx index 142f7e4..5087178 100644 --- a/src/views/Conversations/Components/ContactInfo.jsx +++ b/src/views/Conversations/Components/ContactInfo.jsx @@ -1,9 +1,8 @@ import { useContext } from 'react'; -import { observer } from "mobx-react"; import { stores_Context } from '../config'; import { Table } from 'antd'; -const ContactInfo = observer((props) => { +const ContactInfo = ((props) => { // const { } = useContext(stores_Context); return ( <> diff --git a/src/views/Conversations/Components/ContactPanel.jsx b/src/views/Conversations/Components/ContactPanel.jsx index ba7a6c8..db8881c 100644 --- a/src/views/Conversations/Components/ContactPanel.jsx +++ b/src/views/Conversations/Components/ContactPanel.jsx @@ -1,9 +1,8 @@ import { useContext } from 'react'; -import { observer } from "mobx-react"; import { stores_Context } from '../config'; import { Table } from 'antd'; -const ContactPanel = observer((props) => { +const ContactPanel = ((props) => { // const { } = useContext(stores_Context); return ( <> diff --git a/src/views/Conversations/Components/ConversationsList.jsx b/src/views/Conversations/Components/ConversationsList.jsx index 5d0633f..a8fe23c 100644 --- a/src/views/Conversations/Components/ConversationsList.jsx +++ b/src/views/Conversations/Components/ConversationsList.jsx @@ -1,47 +1,45 @@ import { useRef, useEffect, useState } from 'react'; -import { observer } from 'mobx-react'; +import { useNavigate } from "react-router-dom"; import { List, Avatar, Flex } from 'antd'; -import { useConversationContext } from '@/stores/ConversationContext'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; import { ChatItem, ChatList } from 'react-chat-elements'; +import { useGetJson } from '@/hooks/userFetch'; /** * [] */ -const Conversations = observer(({ conversations }) => { +const Conversations = (() => { + const navigate = useNavigate(); const { switchConversation, conversationsList } = useConversationContext(); - console.log(conversationsList); + // console.log(conversationsList); const [chatlist, setChatlist] = useState([]); useEffect(() => { setChatlist( (conversationsList || []).map((item) => ({ ...item, - avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`, - alt: item.name, - title: item.name, - subtitle: item.lastMessage, - date: item.last_time, - unread: item.new_msgs, + avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${item.whatsapp_name}`, + alt: item.whatsapp_name, + id: item.sn, + title: item.whatsapp_name, + subtitle: item.whatsapp_phone_number, + // subtitle: item.lastMessage, + date: item.last_received_time, // last_send_time + unread: item.unread_msg_count, })) ); return () => {}; }, [conversationsList]); + const onSwitchConversation = (item) => { + switchConversation(item); + if (item.coli_sn) { + navigate(`/order/chat/${item.coli_sn}`, { replace: true }); + } + } + return ( <> - switchConversation(item)} /> - {/* ( - // actions={[mark]} - switchConversation(item)}> - {item.name}} - title={item.name} - // description='{最近的消息}' - /> - - )} - /> */} + onSwitchConversation(item)} /> ); }); diff --git a/src/views/Conversations/Components/CreatePayment.jsx b/src/views/Conversations/Components/CreatePayment.jsx new file mode 100644 index 0000000..128c479 --- /dev/null +++ b/src/views/Conversations/Components/CreatePayment.jsx @@ -0,0 +1,32 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { Popover, Flex, Button } from 'antd'; + +const CreatePayment = ((props) => { + + const [open, setOpen] = useState(false); + const handleOpenChange = (newOpen) => { + setOpen(newOpen); + }; + const onSend = () => { + setOpen(false); + // todo: send + } + return ( + <> + Close} + content={ + + } + title='🔗付款链接' + trigger='click' + open={open} + onOpenChange={handleOpenChange}> + {/* */} + {/* + + + ); +}); +export default CreatePayment; diff --git a/src/views/Conversations/Components/CustomerProfile.jsx b/src/views/Conversations/Components/CustomerProfile.jsx index 2fcad26..c61638d 100644 --- a/src/views/Conversations/Components/CustomerProfile.jsx +++ b/src/views/Conversations/Components/CustomerProfile.jsx @@ -1,8 +1,10 @@ -import { observer } from 'mobx-react'; -import { Card, Flex, Avatar, Typography, Radio, Button } from 'antd'; +import { Card, Flex, Avatar, Typography, Radio, Button, Table } from 'antd'; import { useAuthContext } from '@/stores/AuthContext.js'; -import { useConversationContext } from '@/stores/ConversationContext'; -import { useGetJson } from '@/hooks/userFetch'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; +import { HomeOutlined, LoadingOutlined, SettingFilled, SmileOutlined, SyncOutlined, PhoneOutlined, MailOutlined } from '@ant-design/icons'; + +import CreatePayment from './CreatePayment'; +import QuotesHistory from './QuotesHistory'; const orderTags = [ { value: 'potential', label: '潜力' }, @@ -19,49 +21,47 @@ const orderStatus = [ const { Meta } = Card; -const CustomerProfile = observer(({ customer }) => { - const { errors } = useConversationContext(); +const CustomerProfile = (({ customer }) => { + const { errors, customerOrderProfile: orderInfo } = useConversationContext(); const { loginUser: currentUser } = useAuthContext(); - const orderInfo = useGetJson('http://127.0.0.1:4523/m2/3888351-0-default/144062941'); - const { quotes, contact, last_contact, ...order } = orderInfo || {}; + const { quotes, contact, last_contact, ...order } = orderInfo; return ( -
+
{}} optionType='button' buttonStyle={'solid'} />}> - {}} optionType='button' buttonStyle={'solid'} />} - description={' '} + {}} optionType='button' buttonStyle={'solid'} />} + description={ {}} optionType='button' buttonStyle={'solid'} />} /> - - + + {/* */} - {contact?.name} -
- {contact?.phone} {contact?.email}{' '} -
+ {contact?.[0]?.name} + {contact?.[0]?.phone} + {contact?.[0]?.email} {/*
{order?.order_no}
*/} -
+ {/*
{order?.location} {order?.local_datetime}
-
+
*/}
最新报价
-

{quotes?.[0]?.name}

+

{quotes?.[0]?.lettertitle}

- - + +
-

{order?.order_detail}

+

       
         
沟通记录
-

{quotes?.[0]?.name}

+ ); diff --git a/src/views/Conversations/Components/InputBox.jsx b/src/views/Conversations/Components/InputBox.jsx index aca129c..0ce2e2a 100644 --- a/src/views/Conversations/Components/InputBox.jsx +++ b/src/views/Conversations/Components/InputBox.jsx @@ -1,45 +1,117 @@ -import { useEffect, useState } from 'react'; -import { observer } from 'mobx-react'; -import { Input, Button } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { Input, Button, Tabs, List, Space, Popover, Flex } from 'antd'; // import { Input } from 'react-chat-elements'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; +import { LikeOutlined, MessageOutlined, StarOutlined, SendOutlined, PlusOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { cloneDeep, getNestedValue, isEmpty } from '@/utils/utils'; +import { v4 as uuid } from "uuid"; +import { whatsappTemplatesParamMapped } from '@/lib/msgUtils'; -const InputBox = observer(({ onSend }) => { - const [message, setMessage] = useState(''); +const InputBox = (({ onSend }) => { + const { currentConversation, templates } = useConversationContext(); + const [textContent, setTextContent] = useState(''); - const onOK = () => { - // console.log(message); - if (typeof onSend === 'function' && message.trim() !== '') { + const talkabled = ! isEmpty( currentConversation.sn); + + const handleSendText = () => { + if (typeof onSend === 'function' && textContent.trim() !== '') { const msgObj = { type: 'text', - text: { - body: message, - }, - // contentType: 'text/markdown', + text: textContent, + sender: 'me', + to: currentConversation.whatsapp_phone_number, + id: `${currentConversation.id}.${uuid()}`, // Date.now().toString(16), + date: new Date(), + status: 'waiting', + }; + onSend(msgObj); + setTextContent(''); + } + }; + + const handleSendTemplate = (fromTemplate) => { + console.log(fromTemplate, 'fromTemplate'); + if (typeof onSend === 'function') { + const _conversation = {...cloneDeep(currentConversation), }; + const msgObj = { + type: 'whatsappTemplate', + to: currentConversation.whatsapp_phone_number, + id: `${currentConversation.id}.${uuid()}`, + date: new Date(), + status: 'waiting', + statusTitle: 'Ready to send', sender: 'me', - id: Date.now().toString(16), - readState: false, + template: { + name: fromTemplate.name, + language: { code: fromTemplate.language }, + ...(fromTemplate.components.body[0]?.example?.body_text?.[0]?.length > 0 + ? { components: [ + { + 'type': 'body', + 'parameters': whatsappTemplatesParamMapped[fromTemplate.name].map((v) => ({ type: 'text', text: getNestedValue(_conversation, v) || '' })) + // [ + // { + // 'type': 'text', + // 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name][0]) , + // }, + // { // debug: + // 'type': 'text', + // 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name]?.[1] || whatsappTemplatesParamMapped[fromTemplate.name][0]) , + // }, + // ], + }, + ], } + : {}), + }, + template_origin: fromTemplate, }; onSend(msgObj); - setMessage(''); + setOpenTemplates(false); } }; + const [openTemplates, setOpenTemplates] = useState(false); + const handleOpenChange = (newOpen) => { + setOpenTemplates(newOpen); + }; return (
- setMessage(e.target.value)} /> - - {/* setMessage(e.target.value)} - onKeyPress={(e) => { - if (e.shiftKey && e.charCode === 13) { - onOK(); + + ( + handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={}> + Send + , + ]}> + + + )} + /> } - }} - rightButtons={} - /> */} + title='🙋打招呼' + trigger='click' + open={openTemplates} + onOpenChange={handleOpenChange}> + {/* */} +
); }); diff --git a/src/views/Conversations/Components/LocalTimeClock.jsx b/src/views/Conversations/Components/LocalTimeClock.jsx new file mode 100644 index 0000000..0228803 --- /dev/null +++ b/src/views/Conversations/Components/LocalTimeClock.jsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; +import { Typography } from 'antd'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +const LocalTimeClock = ((props) => { + const { customerOrderProfile: orderInfo } = useConversationContext(); + + const [customerDateTime, setCustomerDateTime] = useState(); + + // todo: 用dayjs 显示指定时区的时间 + useEffect(() => { + const intervalId = setInterval(() => { + // if (customerProfile && customerProfile.timezone) { + const date = new Date(); + const options = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true, + }; + const formatter = new Intl.DateTimeFormat('cn-ZH', options); // todo: + setCustomerDateTime(formatter.format(date)); + // } + }, 1000); // Update every second + + // Cleanup function to clear the interval when the component is unmounted + return () => clearInterval(intervalId); + }, []); + return ( + <> + {customerDateTime} + + ); +}); +export default LocalTimeClock; diff --git a/src/views/Conversations/Components/Messages.jsx b/src/views/Conversations/Components/Messages.jsx index a004456..349063c 100644 --- a/src/views/Conversations/Components/Messages.jsx +++ b/src/views/Conversations/Components/Messages.jsx @@ -1,45 +1,56 @@ -import { useEffect } from 'react'; -import { observer } from 'mobx-react'; -import { List, Avatar, Timeline } from 'antd'; +import { useEffect, useState, useRef } from 'react'; +import { List, Avatar, Timeline, Image } from 'antd'; import { MessageBox } from 'react-chat-elements'; -import { useConversationContext } from '@/stores/ConversationContext'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; -const messagesTemplate = [ - { - id: Date.now().toString(16), - sender: 'Customer_1', - type: 'text', - text: { body: 'Hello, how can I help you today?' } , - } -]; +const Messages = (() => { + const { messages: messagesList } = useConversationContext(); + // const messagesList = parseMessage(messages); + // console.log(messagesList); -const Messages = observer(() => { - const { messages } = useConversationContext() + const messagesEndRef = useRef(null); + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messagesList.length]); + + const [previewVisible, setPreviewVisible] = useState(false); + const [previewSrc, setPreviewSrc] = useState(); + const onPreviewClose = () => { + setPreviewSrc(''); + setPreviewVisible(false); + }; + const handlePreview = (msg) => { + if (msg.type !== 'photo') { + return false; + } + setPreviewVisible(true); + setPreviewSrc(msg.data.uri); + }; return ( - <> - {messages.map((message, index) => ( - - ))} - {/* ( - - - {message.sender === 'me' &&
{message.content} ({message.id})
} -
- )} - /> */} - +
+ {messagesList.map((message, index) => ( + handlePreview(message)} + // letterItem={{ id: 1, letter: 'AS' }} + // 'waiting'| 'sent' | 'received' | 'read' + styles={{backgroundColor: message.sender === 'me' ? '#ccd5ae' : ''}} + {...(message.sender === 'me' ? { + style: { backgroundColor: '#ccd5ae' }, + notchStyle: {fill: '#ccd5ae'}, + className: 'whatsappme-container', + } : {})} + // notchStyle={{fill: '#ccd5ae'}} + // copiableDate={false} + /> + ))} + +
+
); }); diff --git a/src/views/Conversations/Components/QuotesHistory.jsx b/src/views/Conversations/Components/QuotesHistory.jsx new file mode 100644 index 0000000..2a1f029 --- /dev/null +++ b/src/views/Conversations/Components/QuotesHistory.jsx @@ -0,0 +1,64 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { Popover, Flex, Button, List, Popconfirm } from 'antd'; +import { useConversationContext } from '@/stores/Conversations/ConversationContext'; + +const QuotesHistory = ((props) => { + const { customerOrderProfile: orderInfo } = useConversationContext(); + const { quotes, ...order } = orderInfo; + + const [open, setOpen] = useState(false); + const handleOpenChange = (newOpen) => { + setOpen(newOpen); + }; + const onSend = () => { + setOpen(false); + // todo: send + }; + return ( + <> + Close} + content={ + ( + + + + {item.letterdate} + + {}} onCancel={onSend} okText='Yes' cancelText='No'> + + + + + + } + /> + + )} + /> + } + title='📜报价信历史' + trigger='click' + placement={'left'} + open={open} + onOpenChange={handleOpenChange}> + + + + ); +}); +export default QuotesHistory; diff --git a/src/views/Conversations/ConversationProvider.jsx b/src/views/Conversations/ConversationProvider.jsx index dd108ea..9e8dc61 100644 --- a/src/views/Conversations/ConversationProvider.jsx +++ b/src/views/Conversations/ConversationProvider.jsx @@ -1,9 +1,30 @@ -import { ConversationContext, useConversations } from '@/stores/ConversationContext'; +import { useContext } from 'react'; +import { ConversationContext, useConversations, } from '@/stores/Conversations/ConversationContext'; +import { AuthContext } from '@/stores/AuthContext'; +import { RealTimeAPI } from '@/lib/realTimeAPI'; -export const ConversationProvider = ({ children }) => { +// const WS_URL = 'ws://202.103.68.144:8888/whatever/'; +// const WS_URL = 'ws://120.79.9.217:10022/whatever/'; +const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_callback'; // prod: - const conversations = useConversations(); +export const ConversationProvider = ({ children, loginUser, realtimeAPI }) => { + + const conversations = useConversations({loginUser, realtimeAPI}); return {children}; }; -export default ConversationProvider; +// export default ConversationProvider; + +const AuthAndConversationProvider = ({ children }) => { + const { loginUser } = useContext(AuthContext); + const {userId} = loginUser; + const realtimeAPI = new RealTimeAPI({ url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`, protocol: 'WhatsApp' }); + + return ( + + {children} + + ); +}; + +export default AuthAndConversationProvider; diff --git a/src/views/Conversations/Conversations.css b/src/views/Conversations/Conversations.css index ef6d80f..9873798 100644 --- a/src/views/Conversations/Conversations.css +++ b/src/views/Conversations/Conversations.css @@ -1,12 +1,17 @@ -.full-height { - height: 100vh; -} -.scrollable-column { - height: 100%; - overflow-y: auto; +.ant-card .ant-card-head{ + padding: 0 .5em .5em .5em; + min-height: unset; +} +.ant-card .ant-card-body{ + padding: .5em; } -.column { - height: 100%; +/** Chat Window */ +.chatwindow-wrapper .rce-container-mbox .rce-mbox{ + max-width: 400px; +} +.chatwindow-wrapper .rce-mbox-time-block{ + background: linear-gradient(0deg,#00000014,#0000); + color: #00000073; } diff --git a/src/views/Conversations/Index.jsx b/src/views/Conversations/Index.jsx deleted file mode 100644 index 92b3c60..0000000 --- a/src/views/Conversations/Index.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Layout, List, Input, Button } from 'antd'; -import './Chat.css'; - -const { Sider, Content } = Layout; -const { TextArea } = Input; - -const Chat = () => { - const channels = ['Channel 1', 'Channel 2']; - const messages = [ - { user: 'User 1', text: 'Hello!' }, - { user: 'User 2', text: 'Hi!' }, - ]; - - return ( - - - Channels} - bordered - dataSource={channels} - renderItem={item => ( - - {item} - - )} - /> - - - - ( - - - - )} - /> -
-