test: 会话发送和接收, 更新消息状态

dev/mobile
Lei OT 1 year ago
parent 027d1ed4a3
commit c8d5ca9091

@ -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",

@ -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:
};

@ -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() {

@ -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: <OrderFollow /> },
{ path: 'chat/history', element: <ChatHistory /> },
{ path: 'sales/management', element: <SalesManagement /> },
{ path: 'order/chat/:order_sn', element: <Conversations /> },
{ path: 'order/chat', element: <Conversations /> },
{ path: 'account/profile', element: <AccountProfile /> },
],
@ -48,7 +49,7 @@ const router = createBrowserRouter([
{ path: 'dingding/qrcode', element: <DingdingQRCode /> },
],
},
])
]);
ReactDOM.createRoot(document.getElementById('root')).render(
// <React.StrictMode>
@ -60,4 +61,4 @@ ReactDOM.createRoot(document.getElementById('root')).render(
</AuthContext.Provider>
</ThemeContext.Provider>
// </React.StrictMode>
)
);

@ -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);

@ -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',
// },
];

@ -1,12 +0,0 @@
import Chat from './Index';
// import 'antd/dist/reset.css';
function App() {
return (
<div className="App">
<Chat />
</div>
);
}
export default App;

@ -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 (
<div className="App">
<List
itemLayout="horizontal"
dataSource={messages}
renderItem={(message) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar icon="user" />}
title={message.sender}
description={message.message}
/>
</List.Item>
)}
/>
<Input.Group compact>
<Input
placeholder="Type your message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<Button type="primary" onClick={sendMessage}>
Send
</Button>
</Input.Group>
</div>
);
}
export default App;

@ -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;
}

@ -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 (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={200}>
<Menu mode="inline" style={{ height: '100%', borderRight: 0 }}>
{channels.map(channel => (
<Menu.Item key={channel}>{channel}</Menu.Item>
))}
</Menu>
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: 0 }} />
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
<div style={{ padding: 24, background: '#fff', textAlign: 'center' }}>
{messages.map((message, index) => (
<p key={index}>
<strong>{message.user}</strong>: {message.text}
</p>
))}
</div>
</Content>
</Layout>
</Layout>
);
}
export default App;

@ -1,44 +0,0 @@
import { Layout, Menu, List, Timeline, Input } from 'antd';
const { Sider, Content } = Layout;
function ChatApp() {
return (
<Layout>
<Sider theme="light" width={300}>
<Menu>
<Menu.Item>Unread</Menu.Item>
<Menu.Item>Mentions</Menu.Item>
<Menu.Item>Favorites</Menu.Item>
<Menu.Item>Channel List</Menu.Item>
<Menu.Item>Direct Messages</Menu.Item>
</Menu>
<List>
{/* Show channels and DMs */}
</List>
</Sider>
<Content>
<Layout>
<Sider theme="light">
<List>
{/* Show user profile cards */}
</List>
</Sider>
<Content>
<Timeline>
{/* Show messages */}
</Timeline>
<Input.Search
enterButton="Send"
/>
</Content>
</Layout>
</Content>
</Layout>
);
}
export default ChatApp;

@ -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 (
<Layout className='full-height' style={{ maxHeight: 'calc(100% - 150px)', height: 'calc(100% - 150px)' }}>
<Sider width={240} theme={'light'} className='scrollable-column' style={{ height: '70vh' }}>
<ConversationsList />
</Sider>
<Spin spinning={false} tip={'正在连接...'} >
<Layout className='h-screen chatwindow-wrapper' style={{ maxHeight: 'calc(100% - 198px)', height: 'calc(100% - 198px)' }}>
<Sider width={240} theme={'light'} className='h-full overflow-y-auto' style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<ConversationsList />
</Sider>
<Content className='h70' style={{ maxHeight: '70vh', height: '70vh' }}>
<Layout style={{ height: '100%' }}>
<Header className='ant-layout-sider-light ant-card' style={{ padding: '10px', height: 'auto' }}>
<Flex gap={16}>
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${currentConversation.name}`}>{currentConversation.name}</Avatar>
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{currentConversation.name}</Typography.Text>
{/* <div> HXY231119017</div> */}
<Content style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<Layout className='h-full'>
<Header className='ant-layout-sider-light ant-card p-1 h-auto'>
<Flex gap={16}>
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${currentConversation.customer_name}`} />
<Flex flex={'1'} justify='space-between'>
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{currentConversation.customer_name}</Typography.Text>
<Typography.Text>{currentConversation.whatsapp_phone_number}</Typography.Text>
</Flex>
<Flex vertical={true} justify='space-between'>
<Typography.Text>
{/* <LocalTimeClock /> <Typography.Text strong>{order?.location} </Typography.Text> */}
</Typography.Text>
{/* <Typography.Text>{customerDateTime}</Typography.Text> */}
</Flex>
</Flex>
</Flex>
</Flex>
{/* <List
dataSource={[]}
renderItem={(item, ii) => (
<List.Item actions={[<a key='list-loadmore-edit'>mark</a>]}>
<List.Item.Meta
avatar={
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`}
>
{item.name}
</Avatar>
}
title={item.name}
description='{最近的消息}'
/>
</List.Item>
)}
/> */}
</Header>
<Content style={{ maxHeight: '70vh', height: '70vh' }}>
<div className='scrollable-column'>
<Messages />
</div>
</Content>
<Footer className='ant-layout-sider-light' style={{ padding: '10px' }}>
<InputBox onSend={(v) => sendMessage(v)} />
</Footer>
</Layout>
{/* <InputBox onSend={(v) => sendMessage(v)} /> */}
</Content>
</Header>
<Content style={{ maxHeight: '74vh', height: '74vh' }}>
<div className='h-full overflow-y-auto'>
<Messages />
</div>
</Content>
<Footer className='ant-layout-sider-light p-1'>
<InputBox onSend={(v) => sendMessage(v)} />
</Footer>
</Layout>
{/* <InputBox onSend={(v) => sendMessage(v)} /> */}
</Content>
<Sider width={300} theme={'light'} className='scrollable-column' style={{ maxHeight: '70vh', height: '70vh' }}>
<CustomerProfile customer={{}} />
</Sider>
</Layout>
<Sider width={300} theme={'light'} className='h-full overflow-y-auto' style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<CustomerProfile customer={{}} />
</Sider>
</Layout>
</Spin>
);
});

@ -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 (
<>

@ -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 (
<>

@ -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 (
<>
<ChatList className='chat-list' dataSource={chatlist} onClick={(item) => switchConversation(item)} />
{/* <List
dataSource={conversationsList || []}
renderItem={(item, ii) => (
// actions={[<a key='list-loadmore-edit'>mark</a>]}
<List.Item onClick={() => switchConversation(item)}>
<List.Item.Meta
avatar={<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`}>{item.name}</Avatar>}
title={item.name}
// description='{}'
/>
</List.Item>
)}
/> */}
<ChatList className='' dataSource={chatlist} onClick={(item) => onSwitchConversation(item)} />
</>
);
});

@ -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 (
<>
<Popover
// content={<a onClick={hide}>Close</a>}
content={
<Flex></Flex>
}
title='🔗付款链接'
trigger='click'
open={open}
onOpenChange={handleOpenChange}>
{/* <Button type="primary">Click me</Button> */}
{/* <Button type='primary' shape='circle' icon={<></>} size={'large'} /> */}
<Button size={'small'}>Book</Button>
</Popover>
</>
);
});
export default CreatePayment;

@ -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 (
<div className=' divide-x-0 divide-y divide-dotted divide-slate-400/[.24]'>
<div className=' divide-x-0 divide-y divide-slate-300 divide-dotted'>
<Card className='p-2'
bordered={false}
title={order?.order_no}
extra={<Radio.Group size={'small'} options={orderTags} value={'important'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}>
<Meta
title={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
description={' '}
<Meta className='py-2 text-right'
// title={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
description={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
/>
<Flex gap={16}>
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${contact?.name}`} />
<Flex gap={10}>
{/* <Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${contact?.name}`} /> */}
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{contact?.name}</Typography.Text>
<div>
{contact?.phone} <span>{contact?.email}</span>{' '}
</div>
<Typography.Text strong>{contact?.[0]?.name}</Typography.Text>
<Typography.Text ><PhoneOutlined className=' pr-1' />{contact?.[0]?.phone}</Typography.Text>
<Typography.Text ><MailOutlined className=' pr-1' />{contact?.[0]?.email}</Typography.Text>
{/* <div>{order?.order_no}</div> */}
<div>
{/* <div>
{order?.location} <span>{order?.local_datetime}</span>
</div>
<div></div>
<div></div> */}
</Flex>
</Flex>
</Card>
<Flex vertical={true} className='p-2 '>
<div>最新报价</div>
<p className='m-0 py-2 '>{quotes?.[0]?.name}</p>
<p className='m-0 py-2 line-clamp-2 '>{quotes?.[0]?.lettertitle}</p>
<Flex justify={'space-between'} >
<Button size={'small'}>Book</Button>
<Button size={'small'}>报价历史</Button>
<CreatePayment />
<QuotesHistory />
</Flex>
</Flex>
<p className='p-2 overflow-auto h-40 '>{order?.order_detail}</p>
<pre className='p-2 overflow-auto h-32' dangerouslySetInnerHTML={{__html: order?.order_detail}}></pre>
<Flex vertical={true} className='p-2 '>
<div>沟通记录</div>
<p className='m-0 py-2 '>{quotes?.[0]?.name}</p>
<Table size={'small'} columns={[{ title: '进程', dataIndex: 'title' }, { title: '状态', dataIndex: 'title2' },]} />
</Flex>
</div>
);

@ -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 (
<div>
<Input.Search placeholder='Type message here' enterButton='Send' size='large' onSearch={onOK} value={message} onChange={(e) => setMessage(e.target.value)} />
{/* <Input
placeholder='Type a message'
multiline={true}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.shiftKey && e.charCode === 13) {
onOK();
<Flex gap={8}>
<Popover
content={
<List
className='w-96 h-4/6 overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={templates}
renderItem={(item, index) => (
<List.Item
actions={[
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send
</Button>,
]}>
<List.Item.Meta className=' text-neutral-800' title={item.components.header?.[0]?.text || item.name} description={item.components.body?.[0]?.text} />
</List.Item>
)}
/>
}
}}
rightButtons={<button onClick={onOK}>Send</button>}
/> */}
title='🙋打招呼'
trigger='click'
open={openTemplates}
onOpenChange={handleOpenChange}>
{/* <Button type="primary">Click me</Button> */}
<Button type='primary' shape='circle' icon={<MessageOutlined />} size={'large'} disabled={!talkabled} />
</Popover>
<Input.Search disabled={!talkabled}
placeholder='Type message here'
enterButton='Send'
size='large'
onSearch={handleSendText}
value={textContent}
onChange={(e) => setTextContent(e.target.value)}
/>
</Flex>
<div></div>
</div>
);
});

@ -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 (
<>
<Typography.Text>{customerDateTime}</Typography.Text>
</>
);
});
export default LocalTimeClock;

@ -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) => (
<MessageBox
key={message.id}
position={ message.sender === 'me' ? 'right' : 'left' }
type={'text'}
text={message.text.body}
/>
))}
{/* <List
dataSource={conversationsStore.messages}
style={{ flex: '1 1' }}
renderItem={(message) => (
<List.Item>
<List.Item.Meta
title={message.sender !== 'me' ? message.sender : ''}
description={message.sender !== 'me' ? `(${message.id}) ${message.content}` : ''}
/>
{message.sender === 'me' && <div>{message.content} ({message.id})</div>}
</List.Item>
)}
/> */}
</>
<div>
{messagesList.map((message, index) => (
<MessageBox
// className={message.sender === 'me' ? 'whatsappme-container' : ''}
key={message.id}
position={message.sender === 'me' ? 'right' : 'left'}
{...message}
onOpen={() => 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}
/>
))}
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
<div ref={messagesEndRef}></div>
</div>
);
});

@ -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 (
<>
<Popover
// content={<a onClick={hide}>Close</a>}
content={
<List
className='w-96 h-4/6 overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={quotes}
renderItem={(item, index) => (
<List.Item className='' key={item.letterid} >
<List.Item.Meta
className=' text-neutral-800'
title={item.lettertitle}
description={
<Flex justify='space-between'>
<Button onClick={onSend} size={'small'} type='link' key={'send'}>
详细报价
</Button>
<span>{item.letterdate}</span>
<Flex gap={8}>
<Popconfirm title='删除报价信' description='确认要删除报价信吗?' onConfirm={() => {}} onCancel={onSend} okText='Yes' cancelText='No'>
<Button size={'small'} type='link' danger key={'send'}>
删除
</Button>
</Popconfirm>
<Button onClick={onSend} size={'small'} type='link' key={'send'}>
复制
</Button>
</Flex>
</Flex>
}
/>
</List.Item>
)}
/>
}
title='📜报价信历史'
trigger='click'
placement={'left'}
open={open}
onOpenChange={handleOpenChange}>
<Button size={'small'}>报价历史</Button>
</Popover>
</>
);
});
export default QuotesHistory;

@ -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 <ConversationContext.Provider value={conversations}>{children}</ConversationContext.Provider>;
};
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 (
<ConversationProvider loginUser={loginUser} realtimeAPI={realtimeAPI} >
{children}
</ConversationProvider>
);
};
export default AuthAndConversationProvider;

@ -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;
}

@ -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 (
<Layout className="chat-layout">
<Sider theme="light" width={300} className="chat-sider">
<List
header={<div>Channels</div>}
bordered
dataSource={channels}
renderItem={item => (
<List.Item>
{item}
</List.Item>
)}
/>
</Sider>
<Layout>
<Content className="chat-content">
<List
itemLayout="horizontal"
dataSource={messages}
renderItem={item => (
<List.Item>
<List.Item.Meta
title={item.user}
description={item.text}
/>
</List.Item>
)}
/>
<div className="chat-input">
<TextArea rows={4} />
<Button type="primary" className="chat-button">
Send
</Button>
</div>
</Content>
</Layout>
</Layout>
);
}
export default Chat;

@ -1,35 +0,0 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { Table } from 'antd';
import WebSocketLib from '@/lib/websocketLib2';
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' },
];
// Assume we have an array of customer URLs and their corresponding auth tokens
const customers = [
{ url: 'ws://202.103.68.144:8888/whatever/', authToken: 'customer1Token' },
];
// Create a WebSocketLib instance for each customer
const connections = customers.map(customer => {
const wsLib = new WebSocketLib(customer.url, customer.authToken, 'WhatApp');
wsLib.connect();
return wsLib;
});
// Now, the agent can send a message to a specific customer like this:
connections[0].sendMessage('Hello, customer 1! '+ Date.now().toString(36));
// Or broadcast a message to all customers like this:
// connections.forEach(conn => conn.sendMessage('Hello, all customers!'));
export default observer((props) => {
// const { } = useContext(stores_Context);
return <></>;
});

@ -1,9 +1,13 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false,
darkMode: 'media',
theme: {
colors: {
'whatsapp': '#25D366',
// 'whatsappme': '#1ba784',
'whatsappme': '#ccd5ae',
},
extend: {
// gridTemplateColumns: {
// 'responsive':repeat(autofill,minmax('300px',1fr))

Loading…
Cancel
Save