perf: 模板: 有header, footer, buttons

perf: 历史记录: 显示绝对时间
fix: 模板: 只发送body
perf: 提示文字
style: 历史记录: 引用的来源title color
perf: 历史记录: 会话的的时间
dev/mobile
Lei OT 2 years ago
parent dd1921c6e7
commit c466ac953b

@ -4,6 +4,7 @@ import { fetchJSON, postJSON } from '@/utils/request'
import { parseRenderMessageList } from '@/lib/msgUtils'; import { parseRenderMessageList } from '@/lib/msgUtils';
import { API_HOST } from '@/config'; import { API_HOST } from '@/config';
import { isEmpty } from '@/utils/commons'; import { isEmpty } from '@/utils/commons';
import dayjs from 'dayjs';
export const fetchTemplates = async () => { export const fetchTemplates = async () => {
const data = await fetchJSON(`${API_HOST}/listtemplates`); const data = await fetchJSON(`${API_HOST}/listtemplates`);
@ -62,6 +63,14 @@ export const fetchConversationItemClose = async (body) => {
return result; return result;
}; };
/**
* @param {object} body { phone_number, name }
*/
export const postNewConversationItem = async (body) => {
const { errcode, result } = await postJSON(`${API_HOST}/newconversation`, body);
return errcode !== 0 ? {} : result;
};
/** /**
* @param {object} params { opisn, whatsappid } * @param {object} params { opisn, whatsappid }
*/ */
@ -89,6 +98,7 @@ export const fetchConversationsSearch = async (params) => {
whatsapp_name: `${ele.whatsapp_name || ''}`.trim(), whatsapp_name: `${ele.whatsapp_name || ''}`.trim(),
opi_sn: ele.OPI_SN || ele.opi_sn || 0, opi_sn: ele.OPI_SN || ele.opi_sn || 0,
OPI_Name: `${ele.OPI_Name || ele.opi_name || ''}`.trim(), OPI_Name: `${ele.OPI_Name || ele.opi_name || ''}`.trim(),
dateText: dayjs((ele.last_received_time || ele.last_send_time)).format('MM-DD HH:mm'),
matchMsgList: parseRenderMessageList((ele.msglist_AsJOSN || [])), // .reverse()), matchMsgList: parseRenderMessageList((ele.msglist_AsJOSN || [])), // .reverse()),
})); }));
return list; return list;

@ -1,4 +1,5 @@
import { cloneDeep, isEmpty, olog } from "@/utils/utils"; import { cloneDeep, isEmpty, olog } from "@/utils/utils";
import dayjs from "dayjs";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
export const replaceTemplateString = (str, replacements) => { export const replaceTemplateString = (str, replacements) => {
@ -150,7 +151,7 @@ export const sentMsgTypeMapped = {
}), }),
}, },
whatsappTemplate: { whatsappTemplate: {
contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, to: msg.to, msgtype: 'template', msgcontent: msg.template }), contentToSend: (msg) => ({ action: 'message', actionId: msg.id, renderId: msg.id, to: msg.to, msgtype: 'template', msgcontent: { ...msg.template, components: msg.template.components.filter(com => com.type.toLowerCase() === 'body') }}), // todo: 其他组件不发送是否可以
contentToRender: (msg) => { contentToRender: (msg) => {
const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({ ...r, [v.type]: v }), {}) : null; 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 templateParam = (templateDataMapped?.body?.parameters || []).map(e => e.text);
@ -161,7 +162,7 @@ export const sentMsgTypeMapped = {
actionId: msg.id, actionId: msg.id,
conversationid: msg.id.split('.')[0], conversationid: msg.id.split('.')[0],
type: 'text', type: 'text',
title: msg.template_origin.components.header?.[0]?.text || '', title: msg.template.name, // || msg.template_origin.components.header?.[0]?.text || '',
text: autoLinkText(templateDataMapped?.body?.text || ''), // msg.template_origin.components.body?.[0]?.text || '', text: autoLinkText(templateDataMapped?.body?.text || ''), // msg.template_origin.components.body?.[0]?.text || '',
}; };
}, },
@ -462,7 +463,7 @@ export const parseRenderMessageItem = (msg) => {
/** /**
* 从数据库读取的记录 * 从数据库读取的记录
*/ */
export const parseRenderMessageList = (messages, conversationid = null) => { export const parseRenderMessageList = (messages) => {
return messages.map((msg, i) => { return messages.map((msg, i) => {
let msgContentString = ''; let msgContentString = '';
if (typeof msg.msgtext_AsJOSN === 'string') { if (typeof msg.msgtext_AsJOSN === 'string') {
@ -480,6 +481,7 @@ export const parseRenderMessageList = (messages, conversationid = null) => {
type: msgContent.type, type: msgContent.type,
...(typeof whatsappMsgTypeMapped[msgType].type === 'function' ? whatsappMsgTypeMapped[msgType].type(msg) : { type: whatsappMsgTypeMapped[msgType].type || 'text' }), ...(typeof whatsappMsgTypeMapped[msgType].type === 'function' ? whatsappMsgTypeMapped[msgType].type(msg) : { type: whatsappMsgTypeMapped[msgType].type || 'text' }),
date: msgContent?.sendTime || msg.msgtime || '', date: msgContent?.sendTime || msg.msgtime || '',
dateText: dayjs(msgContent?.sendTime || msg.msgtime).format('MM-DD HH:mm'),
localDate: (msg.msgtime || '').replace('T', ' '), localDate: (msg.msgtime || '').replace('T', ' '),
from: msgContent.from, from: msgContent.from,
sender: msgContent.from, sender: msgContent.from,
@ -505,8 +507,8 @@ export const parseRenderMessageList = (messages, conversationid = null) => {
...(typeof whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')]?.renderForReply === 'function' ...(typeof whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')]?.renderForReply === 'function'
? whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')].renderForReply(msg.messageorigin_AsJOSN) ? whatsappMsgTypeMapped[(msg.messageorigin_AsJOSN?.type || 'unsupported')].renderForReply(msg.messageorigin_AsJOSN)
: {}), : {}),
// titleColor: msg.messageorigin_AsJOSN?.customerProfile?.name ? '#a791ff' : "#128c7e", titleColor: msg.messageorigin_AsJOSN?.customerProfile?.name ? '#a791ff' : "#128c7e",
titleColor: msg.messageorigin_direction === 'inbound' ? '#a791ff' : "#128c7e", // titleColor: msg.messageorigin_direction === 'inbound' ? '#a791ff' : "#128c7e",
}, },
origin: msg.messageorigin_AsJOSN, origin: msg.messageorigin_AsJOSN,
}), }),

@ -314,8 +314,9 @@ function ChatHistory() {
alt={`${item.whatsapp_name}`} alt={`${item.whatsapp_name}`}
title={item.whatsapp_name || item.whatsapp_phone_number} title={item.whatsapp_name || item.whatsapp_phone_number}
subtitle={`${item.OPI_Name || ''} ${item.coli_id || ''}`} subtitle={`${item.OPI_Name || ''} ${item.coli_id || ''}`}
date={item.last_received_time} date={item.last_received_time || item.last_send_time}
// dateString={item.last_received_time} // dateString={item.last_received_time}
dateString={item.dateText}
className={String(item.conversationid) === String(selectedConversation.conversationid) ? '__active text-primary bg-neutral-100' : ''} className={String(item.conversationid) === String(selectedConversation.conversationid) ? '__active text-primary bg-neutral-100' : ''}
onClick={() => setSelectedConversation(item)} onClick={() => setSelectedConversation(item)}
/> />
@ -340,7 +341,7 @@ function ChatHistory() {
title={item.sender === 'me' ? selectedConversation.OPI_Name || item.senderName : item.senderName} title={item.sender === 'me' ? selectedConversation.OPI_Name || item.senderName : item.senderName}
subtitle={item.originText} subtitle={item.originText}
date={item.msgtime} date={item.msgtime}
// dateString={item.msgtime} dateString={item.dateText}
className={String(item.sn) === String(selectMatch?.sn) ? '__active text-primary bg-neutral-100' : ' bg-white'} className={String(item.sn) === String(selectMatch?.sn) ? '__active text-primary bg-neutral-100' : ' bg-white'}
onClick={() => handleMatchMsgClick(item)} onClick={() => handleMatchMsgClick(item)}
/> />
@ -392,7 +393,7 @@ function ChatHistory() {
navigator.clipboard.writeText(message.text); navigator.clipboard.writeText(message.text);
appMessage.success('复制成功😀'); appMessage.success('复制成功😀');
}, },
Component: () => <div>复制</div>, Component: () => <div key='copy'>复制</div>,
}, },
], ],
} }

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { ClockCircleOutlined } from '@ant-design/icons'; import { ClockCircleOutlined } from '@ant-design/icons';
import useConversationStore from '@/stores/ConversationStore';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
@ -13,7 +12,6 @@ dayjs.extend(timezone);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const ExpireTimeClock = ({ expireTime }) => { const ExpireTimeClock = ({ expireTime }) => {
// const expireTime = useConversationStore((state) => state.currentConversation.conversation_expiretime);
const [customerDateTime, setCustomerDateTime] = useState(''); const [customerDateTime, setCustomerDateTime] = useState('');
const [isExpired, setIsExpired] = useState(false); const [isExpired, setIsExpired] = useState(false);

@ -80,7 +80,7 @@ const ImageUpload = ({ disabled, invokeUploadFileMessage, invokeSendUploadMessag
} }
}} }}
> >
<Tooltip title={'图片, 视频, 语音, 附件'} placement={'bottom'}> <Tooltip title={<><div>webp(100K)</div><div>图片(5M)</div><div>视频(16M)</div><div>语音(16M)</div><div>附件(100M)</div></>} >
<Button key={'addPic'} type='text' disabled={disabled} icon={<FileAddOutlined />} size={'middle'} className='text-primary rounded-none' /> <Button key={'addPic'} type='text' disabled={disabled} icon={<FileAddOutlined />} size={'middle'} className='text-primary rounded-none' />
</Tooltip> </Tooltip>
</Upload> </Upload>

@ -3,8 +3,9 @@ import { App, Popover, Flex, Button, List, Input } from 'antd';
import { MessageOutlined, SendOutlined } from '@ant-design/icons'; import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, getNestedValue, objectMapper } from '@/utils/utils'; import { cloneDeep, getNestedValue, objectMapper, sortArrayByOrder } from '@/utils/utils';
import { replaceTemplateString } from '@/lib/msgUtils'; import { replaceTemplateString } from '@/lib/msgUtils';
import { isEmpty } from '@/utils/commons';
const splitTemplate = (template) => { const splitTemplate = (template) => {
const placeholders = template.match(/{{(.*?)}}/g) || []; const placeholders = template.match(/{{(.*?)}}/g) || [];
@ -40,7 +41,7 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
const handleSearchTemplates = (val) => { const handleSearchTemplates = (val) => {
if (val.toLowerCase().trim() !== '') { if (val.toLowerCase().trim() !== '') {
const res = templates.filter( const res = templates.filter(
(item) => item.name.includes(val.toLowerCase().trim()) || item.components_origin.some((itemc) => itemc.text.toLowerCase().includes(val.toLowerCase().trim())) (item) => item.name.includes(val.toLowerCase().trim()) || item.components_origin.some((itemc) => (itemc?.text || '').toLowerCase().includes(val.toLowerCase().trim()))
); );
setDataSource(res); setDataSource(res);
return false; return false;
@ -57,18 +58,18 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
template: { template: {
name: fromTemplate.name, name: fromTemplate.name,
language: { code: fromTemplate.language }, language: { code: fromTemplate.language },
components: fromTemplate.components_origin.map((citem) => { components: sortArrayByOrder(fromTemplate.components_origin.map((citem) => {
const keys = ((citem?.text || '').match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')); const keys = ((citem?.text || '').match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
const params = keys.map((v) => ({ type: 'text', text: getNestedValue(mergeInput, [v]) })); const params = keys.map((v) => ({ type: 'text', text: getNestedValue(mergeInput, [v]) }));
const paramText = params.map((p) => p.text); const paramText = params.map((p) => p.text);
const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || ''; const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || '';
valid = keys.length !== paramText.filter((s) => s).length ? false : valid; valid = keys.length !== paramText.filter((s) => s).length ? false : valid;
return { return citem.type.toLowerCase() === 'body' ? {
type: citem.type.toLowerCase(), type: citem.type.toLowerCase(),
parameters: params, parameters: params,
text: fillTemplate, text: fillTemplate,
}; } : {...citem, type: citem.type.toLowerCase(),};
}), }), 'type', ['header', 'body', 'footer', 'buttons'] ),
}, },
template_origin: fromTemplate, template_origin: fromTemplate,
}; };
@ -78,6 +79,7 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
description: '信息未填写完整, 请补充填写', description: '信息未填写完整, 请补充填写',
placement: 'top', placement: 'top',
duration: 3, duration: 3,
closeIcon: false,
}); });
return false; return false;
} }
@ -92,6 +94,49 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
}); });
}; };
const renderHeader = ({ tempItem }) => {
if (isEmpty(tempItem.components.header)) {
return null;
}
const headerObj = tempItem.components.header[0];
return (
<div className='pb-1'>
{'text' === headerObj.format.toLowerCase() && <div>{headerObj.text}</div>}
{'image' === headerObj.format.toLowerCase() && <img src={headerObj.example.header_url} height={100}></img>}
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
<a href={headerObj.example.header_url} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
[&nbsp;{headerObj.format}&nbsp;]({headerObj.example.header_url})
</a>
)}
</div>
);
}
const renderButtons = ({ tempItem }) => {
if (isEmpty(tempItem.components.buttons)) {
return null;
}
const buttons = tempItem.components.buttons.reduce((r, c) => r.concat(c.buttons), []);
return (
<div className='flex gap-1 pt-1'>
{buttons.map((btn, index) =>
btn.type.toLowerCase() === 'url' ? (
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
{btn.text}
</Button>
) : btn.type.toLowerCase() === 'phone_number' ? (
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
{btn.text} ({btn.phone_number})
</Button>
) : (
<Button className='text-blue-500' size={'small'} key={btn.type} rel='noreferrer'>
{btn.text}
</Button>
)
)}
</div>
);
}
const renderForm = ({ tempItem }) => { const renderForm = ({ tempItem }) => {
const templateText = tempItem.components.body?.[0]?.text || ''; const templateText = tempItem.components.body?.[0]?.text || '';
const tempArr = splitTemplate(templateText); const tempArr = splitTemplate(templateText);
@ -167,8 +212,10 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
description={ description={
<> <>
<div className=' max-h-32 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'> <div className=' max-h-32 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'>
<div className='text-slate-500'>{renderForm({ tempItem: item })}</div> {renderHeader({ tempItem: item })}
<div className='text-slate-500 py-1'>{renderForm({ tempItem: item })}</div>
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null} {item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
{renderButtons({ tempItem: item })}
</div> </div>
</> </>
} }

@ -255,7 +255,7 @@ const InputComposer = ({ mobile }) => {
showCount={textabled} showCount={textabled}
placeholder={ placeholder={
gt24h gt24h
? 'This session has expired. Please send a template message to activate the session' ? '会话已过期. 请发送打招呼消息激活对话💬.'
: mobile === undefined : mobile === undefined
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送' ? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
: 'Enter 换行, 点击 Send 发送' : 'Enter 换行, 点击 Send 发送'

@ -4,7 +4,7 @@ import { App, Button } from 'antd';
import { DownOutlined, LoadingOutlined } from '@ant-design/icons'; import { DownOutlined, LoadingOutlined } from '@ant-design/icons';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { isEmpty, } from '@/utils/utils'; import { groupBy, isEmpty, } from '@/utils/utils';
const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, ...props }) => { const MessagesList = ({ messages, handlePreview, reference, longListLoading, getMoreMessages, shouldScrollBottom, loadNextPage, handleContactClick, ...props }) => {
@ -32,7 +32,16 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
useEffect(scrollToBottom, [messages]); useEffect(scrollToBottom, [messages]);
const RenderText = memo(function renderText({ str, className }) { const RenderText = memo(function renderText({ str, className, template }) {
let headerObj, footerObj, buttonsArr;
if (!isEmpty(template)) {
const componentsObj = groupBy(template.components, (item) => item.type);
headerObj = componentsObj.header[0];
footerObj = componentsObj.footer[0];
buttonsArr = componentsObj.buttons.reduce((r, c) => r.concat(c.buttons), []);
}
const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== ''); const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== '');
const links = str.match(/https?:\/\/[\S]+/gi) || []; const links = str.match(/https?:\/\/[\S]+/gi) || [];
const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || []; const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || [];
@ -49,6 +58,17 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
}, []); }, []);
return ( return (
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}> <span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
{headerObj ? (
<div className='text-neutral-500 text-center'>
{'text' === headerObj.format.toLowerCase() && <div>{headerObj.text}</div>}
{'image' === headerObj.format.toLowerCase() && <img src={headerObj.example.header_url} height={100}></img>}
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
<a href={headerObj.example.header_url} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
[&nbsp;{headerObj.format}&nbsp;]
</a>
)}
</div>
) : null}
{(objArr || []).map((part, index) => { {(objArr || []).map((part, index) => {
if (part.type === 'link') { if (part.type === 'link') {
return ( return (
@ -61,6 +81,26 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
return part.key; return part.key;
} }
})} })}
{footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null}
{buttonsArr && buttonsArr.length > 0 ? (
<div className='flex flex-row gap-1'>
{buttonsArr.map((btn, index) =>
btn.type.toLowerCase() === 'url' ? (
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
{btn.text}
</Button>
) : btn.type.toLowerCase() === 'phone_number' ? (
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
{btn.text} ({btn.phone_number})
</Button>
) : (
<Button className='text-blue-500' size={'small'} key={btn.type}>
{btn.text}
</Button>
)
)}
</div>
) : null}
</span> </span>
); );
}); });
@ -99,7 +139,7 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
onReplyMessageClick={() => scrollToMessage(message.reply.id)} onReplyMessageClick={() => scrollToMessage(message.reply.id)}
onOpen={() => handlePreview(message)} onOpen={() => handlePreview(message)}
onTitleClick={() => handlePreview(message)} onTitleClick={() => handlePreview(message)}
text={<RenderText str={message?.text || ''} className={message.status === 'failed' ? 'line-through text-neutral-400' : ''} />} text={<RenderText str={message?.text || ''} className={message.status === 'failed' ? 'line-through text-neutral-400' : ''} template={message.template} />}
{...(message.sender === 'me' {...(message.sender === 'me'
? { ? {
styles: { backgroundColor: '#ccd4ae' }, styles: { backgroundColor: '#ccd4ae' },

Loading…
Cancel
Save