test: 会话发送和接收, 更新消息状态
parent
027d1ed4a3
commit
c8d5ca9091
@ -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,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;
|
@ -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;
|
@ -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;
|
@ -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 <></>;
|
||||
});
|
||||
|
Loading…
Reference in New Issue