From c81e8077901db22582fb18013e6b1a3a6dbe3036 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 4 Mar 2024 10:56:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF=E7=A0=81;?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=9B=B4=E6=96=B0;=E4=BF=AE=E6=94=B9store=20=E5=8F=96?= =?UTF-8?q?=E5=80=BC;=E6=B6=88=E6=81=AF=E6=A0=B7=E5=BC=8F;=E6=96=B0?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=97=AA=E7=83=81=E6=8F=90=E9=86=92;?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=A7=A3=E6=9E=90=20=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E5=92=8C=E8=A1=A8=E6=83=85;WhatsApp=E5=8F=B7=E7=A0=81=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/ConversationActions.js | 2 +- src/lib/msgUtils.js | 32 ++++---- src/stores/ConversationStore.js | 45 +++++++---- src/views/AuthApp.jsx | 54 +++++++++++-- .../Components/ConversationsList.jsx | 39 +++++---- .../Conversations/Components/Input/Emoji.jsx | 2 +- .../Components/Input/ImageUpload.jsx | 5 +- .../Components/Input/Template.jsx | 5 +- .../Components/InputComposer.jsx | 18 +++-- .../Conversations/Components/Messages.jsx | 81 ++++++++++++------- .../Components/MessagesHeader.jsx | 29 +++++-- src/views/Conversations/Conversations.css | 7 +- 12 files changed, 214 insertions(+), 105 deletions(-) diff --git a/src/actions/ConversationActions.js b/src/actions/ConversationActions.js index 1b8bd66..5f6c809 100644 --- a/src/actions/ConversationActions.js +++ b/src/actions/ConversationActions.js @@ -20,7 +20,7 @@ export const fetchConversationsList = async (params) => { export const fetchMessages = async (params) => { const { result } = await fetchJSON(`${API_HOST}/getcusmessages`, params); - return parseRenderMessageList(result); + return parseRenderMessageList(result || []); } export const fetchCustomerProfile = async (colisn) => { diff --git a/src/lib/msgUtils.js b/src/lib/msgUtils.js index dbfdc6c..e7715e8 100644 --- a/src/lib/msgUtils.js +++ b/src/lib/msgUtils.js @@ -15,10 +15,11 @@ export const replaceTemplateString = (str, replacements) => { } export const autoLinkText = (text) => { - let regex = /(https?:\/\/[^\s]+)/g; + return text; + // let regex = /(https?:\/\/[^\s]+)/g; - let newText = text.replace(regex, '$1'); - return newText; + // let newText = text.replace(regex, '$1'); + // return newText; } /** * @@ -298,7 +299,7 @@ export const parseRenderMessageItem = (msg) => { whatsapp_name: msg?.customerProfile?.name || '', whatsapp_phone_number: msg.from, whatsapp_msg_type: msg.type, - ...((isEmpty(msg.context) && isEmpty(msg.reaction)) || msg.context.forwarded === true + ...((isEmpty(msg.context) && isEmpty(msg.reaction)) || msg.context?.forwarded === true ? {} : { reply: { @@ -319,7 +320,7 @@ export const parseRenderMessageItem = (msg) => { * 从数据库读取的记录 */ export const parseRenderMessageList = (messages, conversationid = null) => { - return messages.map((msg) => { + return messages.map((msg, i) => { const msgContent = msg.msgtext_AsJOSN; msgContent.template = msg.msgtype === 'template' ? { ...msgContent.template, ...msg.template_AsJOSN } : {}; const msgType = msgContent.type; @@ -341,14 +342,15 @@ export const parseRenderMessageList = (messages, conversationid = null) => { dateString: msgStatusRenderMapped[msgContent?.status || 'failed'] === 'failed' ? `发送失败 ${whatsappError?.[msgContent.errorCode] || msgContent.errorMessage} ❌` : '', } : {}), - ...(isEmpty(msg.messageorigin_AsJOSN) && isEmpty(msgContent.context) + ...((isEmpty(msg.messageorigin_AsJOSN) && isEmpty(msgContent.context)) + // ...((isEmpty(msg.messageorigin_AsJOSN) || isEmpty(msgContent.context)) ? {} : { reply: { - message: msg.messageorigin_AsJOSN.text?.body || msg.messageorigin_AsJOSN.text, - title: msg.messageorigin_AsJOSN?.customerProfile?.name || msg.messageorigin_AsJOSN.senderName || 'me', - ...(typeof whatsappMsgTypeMapped[msg.messageorigin_AsJOSN.type]?.renderForReply === 'function' - ? whatsappMsgTypeMapped[msg.messageorigin_AsJOSN.type].renderForReply(msg.messageorigin_AsJOSN) + message: msg.messageorigin_AsJOSN?.text?.body || msg.messageorigin_AsJOSN?.text, + title: msg.messageorigin_AsJOSN?.customerProfile?.name || msg.messageorigin_AsJOSN?.senderName || 'me', + ...(typeof whatsappMsgTypeMapped[msg.messageorigin_AsJOSN?.type]?.renderForReply === 'function' + ? whatsappMsgTypeMapped[msg.messageorigin_AsJOSN?.type].renderForReply(msg.messageorigin_AsJOSN) : {}), // titleColor: msg.messageorigin_AsJOSN?.customerProfile?.name ? '#a791ff' : "#128c7e", titleColor: msg.messageorigin_direction === 'inbound' ? '#a791ff' : "#128c7e", @@ -362,9 +364,9 @@ export const parseRenderMessageList = (messages, conversationid = null) => { }); }; export const whatsappError = { - '131026': '消息无法投递. [未注册/使用旧版/未同意政策]', - '131047': '会话超过24小时.', - '131053': '文件上传失败.', - '131048': '账户被风控.', // 消息发送太多, 达到垃圾数量限制 - '131031': '账户已锁定.', + '131026': '[131026] 消息无法投递(未注册/使用旧版/未同意政策).', + '131047': '[131047] 会话超过24小时.', + '131053': '[131053] 文件上传失败.', + '131048': '[131048] 账户被风控.', // 消息发送太多, 达到垃圾数量限制 + '131031': '[131031] 账户已锁定.', }; diff --git a/src/stores/ConversationStore.js b/src/stores/ConversationStore.js index fc1df35..5fd1855 100644 --- a/src/stores/ConversationStore.js +++ b/src/stores/ConversationStore.js @@ -45,7 +45,7 @@ const websocketSlice = (set, get) => ({ setWebsocketRetrytimes: (retrytimes) => set({ websocketRetrytimes: retrytimes, websocketRetrying: retrytimes > 0 }), connectWebsocket: (userId) => { - const { setWebsocket, setWebsocketOpened, setWebsocketRetrytimes, addError, handleMessage } = get(); + const { setWebsocket, setWebsocketOpened, setWebsocketRetrytimes, addError, handleMessage, activeConversations } = get(); const realtimeAPI = new RealTimeAPI( { @@ -56,7 +56,15 @@ const websocketSlice = (set, get) => ({ setWebsocketOpened(true); setWebsocketRetrytimes(0); }, - () => setWebsocketOpened(false), + () => { + setWebsocketOpened(false) + const newMsgList = Object.keys(activeConversations).reduce((acc, key) => { + const newMsgList = activeConversations[key].slice(-10); + acc[key] = newMsgList; + return acc; + }, {}); + set({ activeConversations: newMsgList }); + }, (n) => setWebsocketRetrytimes(n) ); @@ -108,6 +116,8 @@ const websocketSlice = (set, get) => ({ if (permission === 'granted') { const notification = new Notification(`${msgRender.senderName}`, { body: msgRender?.text || `[ ${msgRender.type} ]`, + requireInteraction: true, // 设置手动关闭 + tag: 'global-sales-notification', // 通知ID,同类通知建议设置相同ID,避免通知过多遮挡桌面 ...(msgRender.type === 'photo' ? { image: msgRender.data.uri } : {}), }); notification.onclick = function () { @@ -149,7 +159,8 @@ const conversationSlice = (set, get) => ({ return set((state) => ({ conversationsList: [...newConversations, ...state.conversationsList], - activeConversations: { ...activeConversations, ...newConversationsMapped } + activeConversations: { ...activeConversations, ...newConversationsMapped }, + totalNotify: state.totalNotify + newConversations.map(ele => ele.unread_msg_count).reduce((acc, cur) => acc + (cur || 0), 0), })); }, delConversationitem: (conversation) => { @@ -158,15 +169,15 @@ const conversationSlice = (set, get) => ({ const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); conversationsList.splice(targetIndex, 1); - return set((state) => ({ + return set({ conversationsList: [...conversationsList], activeConversations: { ...activeConversations, [`${targetId}`]: [] }, currentConversation: {}, - })); + }); }, setCurrentConversation: (conversation) => { // 清空未读 - const { conversationsList } = get(); + const { conversationsList, totalNotify } = get(); const targetId = conversation.sn; const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); targetIndex !== -1 ? conversationsList.splice(targetIndex, 1, { @@ -174,11 +185,12 @@ const conversationSlice = (set, get) => ({ unread_msg_count: 0, }) : null; - return set({ currentConversation: conversation, referenceMsg: {}, conversationsList: [...conversationsList] }); + return set({ totalNotify: totalNotify - (conversation.unread_msg_count || 0), currentConversation: conversation, referenceMsg: {}, conversationsList: [...conversationsList] }); }, }); const messageSlice = (set, get) => ({ + totalNotify: 0, msgListLoading: false, activeConversations: {}, setMsgLoading: (msgListLoading) => set({ msgListLoading }), @@ -216,21 +228,21 @@ const messageSlice = (set, get) => ({ // }); // } - return set((state) => ({ + return set({ activeConversations: { ...activeConversations, [String(targetId)]: targetMsgs }, - conversationsList: [...conversationsList], - })); + // conversationsList: [...conversationsList], + }); }, sentOrReceivedNewMessage: (targetId, message) => { // msgRender: - const { activeConversations, conversationsList, currentConversation } = get(); + const { activeConversations, conversationsList, currentConversation, totalNotify } = get(); const targetMsgs = activeConversations[String(targetId)] || []; const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); - const updateTime = message.type !== 'system' && message.sender !== 'me' ? message.date : null; + const lastReceivedTime = message.type !== 'system' && message.sender !== 'me' ? message.date : null; const newConversation = targetId !== -1 ? { ...conversationsList[targetIndex], - last_received_time: updateTime || conversationsList[targetIndex].last_received_time, + last_received_time: lastReceivedTime || conversationsList[targetIndex].last_received_time, unread_msg_count: String(targetId) !== String(currentConversation.sn) && message.sender !== 'me' ? conversationsList[targetIndex].unread_msg_count + 1 @@ -244,14 +256,15 @@ const messageSlice = (set, get) => ({ }; conversationsList.splice(targetIndex, 1); conversationsList.unshift(newConversation); - return set((state) => ({ + return set({ + totalNotify: totalNotify+newConversation.unread_msg_count, activeConversations: { ...activeConversations, [String(targetId)]: [...targetMsgs, message] }, conversationsList: [...conversationsList], currentConversation: { ...currentConversation, - last_received_time: String(targetId) === String(currentConversation.sn) ? updateTime : currentConversation.last_received_time, + ...(String(targetId) === String(currentConversation.sn) ? { last_received_time: message.date } : {}), }, - })); + }); }, }); diff --git a/src/views/AuthApp.jsx b/src/views/AuthApp.jsx index 3112a5e..e073693 100644 --- a/src/views/AuthApp.jsx +++ b/src/views/AuthApp.jsx @@ -3,10 +3,10 @@ import useAuthStore from '@/stores/AuthStore' import useConversationStore from '@/stores/ConversationStore' import { useThemeContext } from '@/stores/ThemeContext' import { DownOutlined } from '@ant-design/icons' -import { App as AntApp, Avatar, Col, ConfigProvider, Dropdown, Empty, Layout, Menu, Row, Space, Typography, theme } from 'antd' +import { App as AntApp, Avatar, Col, ConfigProvider, Dropdown, Empty, Layout, Menu, Row, Space, Typography, theme, Badge } from 'antd' import zhLocale from 'antd/locale/zh_CN' import 'dayjs/locale/zh-cn' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { Link, NavLink, Outlet, useHref, useNavigate } from 'react-router-dom' import '@/assets/App.css' @@ -22,7 +22,7 @@ function AuthApp() { const { colorPrimary, borderRadius } = useThemeContext() const { loginUser } = useAuthStore() - + const href = useHref() useEffect(() => { @@ -32,7 +32,7 @@ function AuthApp() { } }, [href]) - + const totalNotify = useConversationStore((state) => state.totalNotify); useEffect(() => { if (loginUser.userId > 0) { useConversationStore.getState().connectWebsocket(loginUser.userId); @@ -54,6 +54,44 @@ function AuthApp() { token: { colorBgContainer }, } = theme.useToken() + // Flicker title when new message received + const [isTitleVisible, setIsTitleVisible] = useState(true); + useEffect(() => { + let interval; + if (totalNotify > 0) { + interval = setInterval(() => { + document.title = isTitleVisible ? `✉🔔🔥【${totalNotify}条新消息】` : '聊天式销售平台'; + setIsTitleVisible(!isTitleVisible); + }, 600); + } else { + document.title = '聊天式销售平台'; + } + return () => clearInterval(interval); + }, [totalNotify, isTitleVisible]); + + // Display browser notification and change favicon when new message received + // useEffect(() => { + // if (hasNewMessage) { + // if (Notification.permission === "granted") { + // new Notification("New messages received!", { + // body: "You have new messages.", + // icon: "path-to-your-icon.png", + // }); + // } else if (Notification.permission !== "denied") { + // Notification.requestPermission().then(permission => { + // if (permission === "granted") { + // new Notification("New messages received!", { + // body: "You have new messages.", + // icon: "path-to-your-icon.png", + // }); + // } + // }); + // } + + // setFavicon('path-to-your-new-favicon.ico'); + // } + // }, [hasNewMessage]); + return ( 订单跟踪 }, - { key: '/order/chat', label: 在线聊天 }, + { key: '/order/chat', label: 在线聊天 + }, { key: '/chat/history', label: 聊天历史 }, ]} /> diff --git a/src/views/Conversations/Components/ConversationsList.jsx b/src/views/Conversations/Components/ConversationsList.jsx index da5e751..5a542ac 100644 --- a/src/views/Conversations/Components/ConversationsList.jsx +++ b/src/views/Conversations/Components/ConversationsList.jsx @@ -16,19 +16,16 @@ const Conversations = () => { const { coli_guest_WhatsApp } = orderRow || {}; const { order_sn } = useParams(); const navigate = useNavigate(); - const { loginUser } = useAuthStore(); - const { userId } = loginUser; - const { - initialState, - activeConversations, - currentConversation, - conversationsList, - addToConversationList, - delConversationitem, - setCurrentConversation, - receivedMessageList, - setMsgLoading, - } = useConversationStore(); + const userId = useAuthStore(state => state.loginUser.userId); + const initialState = useConversationStore((state) => state.initialState); + const activeConversations = useConversationStore((state) => state.activeConversations); + const currentConversation = useConversationStore((state) => state.currentConversation); + const conversationsList = useConversationStore((state) => state.conversationsList); + const addToConversationList = useConversationStore((state) => state.addToConversationList); + const delConversationitem = useConversationStore((state) => state.delConversationitem); + const setCurrentConversation = useConversationStore((state) => state.setCurrentConversation); + const receivedMessageList = useConversationStore((state) => state.receivedMessageList); + const setMsgLoading = useConversationStore((state) => state.setMsgLoading); const [switchToC, setSwitchToC] = useState({}); const [shouldFetchCList, setShouldFetchCList] = useState(true); @@ -43,14 +40,16 @@ const Conversations = () => { const getOrderConversationList = async (colisn) => { const { whatsapp_phone_number } = switchToC; const whatsappID = coli_guest_WhatsApp || whatsapp_phone_number || ''; - const data = await fetchOrderConversationsList({ opisn: userId, colisn: colisn, whatsappid: whatsappID }); - if (!isEmpty(data)) { - addToConversationList(data); + let findCurrent = -1; // conversationsList.findIndex((item) => item.coli_sn === Number(colisn)); + if (!isEmpty(whatsappID)) { + const data = await fetchOrderConversationsList({ opisn: userId, colisn: colisn, whatsappid: whatsappID }); + if (!isEmpty(data)) { + addToConversationList(data); + } + findCurrent = conversationsList.findIndex((item) => item.coli_sn === Number(colisn)); // data.findIndex((item) => item.sn === currentConversation.sn); } - let ifCurrent = data.findIndex((item) => item.sn === currentConversation.sn); - ifCurrent = ifCurrent !== -1 ? ifCurrent : conversationsList.findIndex((item) => item.coli_sn === Number(colisn)); - if (ifCurrent !== -1) { - switchConversation(conversationsList[ifCurrent === -1 ? 0 : ifCurrent]); + if (findCurrent !== -1) { + switchConversation(conversationsList[findCurrent]); } else { // reset chat window setCurrentConversation({ sn: '', customer_name: '', coli_sn: order_sn }); diff --git a/src/views/Conversations/Components/Input/Emoji.jsx b/src/views/Conversations/Components/Input/Emoji.jsx index d1a5842..76bd564 100644 --- a/src/views/Conversations/Components/Input/Emoji.jsx +++ b/src/views/Conversations/Components/Input/Emoji.jsx @@ -15,7 +15,7 @@ const InputTemplate = ({ disabled = false, inputEmoji }) => { overlayClassName='p-0' placement={'right'} overlayInnerStyle={{ padding: 0, borderRadius: '8px' }} - fresh + // fresh content={} // title='😀' trigger='click' diff --git a/src/views/Conversations/Components/Input/ImageUpload.jsx b/src/views/Conversations/Components/Input/ImageUpload.jsx index d4489da..9293712 100644 --- a/src/views/Conversations/Components/Input/ImageUpload.jsx +++ b/src/views/Conversations/Components/Input/ImageUpload.jsx @@ -43,7 +43,10 @@ const mockGetOSSData = () => ({ }); const ImageUpload = ({ disabled, invokeSendMessage }) => { - const { currentConversation, setComplexMsg, complexMsg } = useConversationStore(); + const currentConversation = useConversationStore(state => state.currentConversation); + const setComplexMsg = useConversationStore(state => state.setComplexMsg); + const complexMsg = useConversationStore(state => state.complexMsg); + const [uploading, setUploading] = useState(false); const [OSSData, setOSSData] = useState(); diff --git a/src/views/Conversations/Components/Input/Template.jsx b/src/views/Conversations/Components/Input/Template.jsx index 33f74f2..86bcf67 100644 --- a/src/views/Conversations/Components/Input/Template.jsx +++ b/src/views/Conversations/Components/Input/Template.jsx @@ -24,8 +24,9 @@ const splitTemplate = (template) => { const InputTemplate = ({ disabled = false, invokeSendMessage }) => { const searchInputRef = useRef(null); const { notification } = App.useApp(); - const { loginUser } = useAuthStore() - const { currentConversation, templates } = useConversationStore(); + const loginUser = useAuthStore(state => state.loginUser); + const currentConversation = useConversationStore(state => state.currentConversation); + const templates = useConversationStore(state => state.templates); // 用于替换变量: customer, agent const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { username: [{ key: 'agent_name' }, { key: 'your_name' }] }) }; useEffect(() => { diff --git a/src/views/Conversations/Components/InputComposer.jsx b/src/views/Conversations/Components/InputComposer.jsx index 1df52b8..4ca228c 100644 --- a/src/views/Conversations/Components/InputComposer.jsx +++ b/src/views/Conversations/Components/InputComposer.jsx @@ -26,10 +26,16 @@ import InputImageUpload from './Input/ImageUpload'; import dayjs from 'dayjs'; const InputComposer = () => { - const { - loginUser: { userId }, - } = useAuthStore(); - const { websocket, websocketOpened, currentConversation, referenceMsg, setReferenceMsg, complexMsg, setComplexMsg, sentOrReceivedNewMessage } = useConversationStore(); + const userId = useAuthStore(state => state.loginUser.userId); + const websocket = useConversationStore(state => state.websocket); + const websocketOpened = useConversationStore(state => state.websocketOpened); + const currentConversation = useConversationStore(state => state.currentConversation); + const referenceMsg = useConversationStore(state => state.referenceMsg); + const setReferenceMsg = useConversationStore(state => state.setReferenceMsg); + const complexMsg = useConversationStore(state => state.complexMsg); + const setComplexMsg = useConversationStore(state => state.setComplexMsg); + const sentOrReceivedNewMessage = useConversationStore(state => state.sentOrReceivedNewMessage); + const talkabled = !isEmpty(currentConversation.sn) && websocketOpened; const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true; const textabled = talkabled && !gt24h; @@ -84,7 +90,7 @@ const InputComposer = () => {
{referenceMsg.id && ( -
+
{referenceMsg.senderName} {referenceMsg.originText}
@@ -122,7 +128,7 @@ const InputComposer = () => { /> - + setTextContent(`${textContent}${s}`)} /> {/* */} {/*