import { create } from 'zustand'; import { RealTimeAPI } from '@/channel/realTimeAPI'; import { olog, isEmpty, groupBy } from '@/utils/commons'; import { receivedMsgTypeMapped, handleNotification } from '@/channel/bubbleMsgUtils'; import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions'; import { devtools } from 'zustand/middleware'; import { WS_URL, DATETIME_FORMAT } from '@/config'; import dayjs from 'dayjs'; const replaceObjectsByKey = (arr1, arr2, key) => { const map = new Map(arr2.map(ele => [ele[key], ele])); return arr1.map(item => map.has(item[key]) ? map.get(item[key]) : item); } // const WS_URL = 'ws://202.103.68.144:8888/whatever/'; // const WS_URL = 'ws://120.79.9.217:10022/whatever/'; const conversationRow = { sn: '', opi_sn: '', coli_sn: '', coli_id: '', last_received_time: '', last_send_time: '', unread_msg_count: '', whatsapp_name: '', customer_name: '', whatsapp_phone_number: '', }; const initialConversationState = { // websocket: null, // websocketOpened: null, // websocketRetrying: null, // websocketRetrytimes: null, errors: [], // 错误信息 initialState: false, // templates: [], closedConversationsList: [], // 已关闭的对话列表 conversationsList: [], // 对话列表 topList: [], pageList: [], currentConversation: {}, // 当前对话 activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: [] } referenceMsg: {}, complexMsg: {}, totalNotify: 0, msgListLoading: false, }; const globalNotifySlice = (set) => ({ globalNotify: [], setGlobalNotify: (notify) => set(() => ({ globalNotify: notify })), addGlobalNotify: (notify) => set((state) => ({ globalNotify: [...state.globalNotify, notify] })), removeGlobalNotify: (id) => set((state) => ({ globalNotify: state.globalNotify.filter(item => item.id !== id) })), clearGlobalNotify: () => set(() => ({ globalNotify: [] })), }) // 顾问的自定义标签 const tagsSlice = (set) => ({ tags: [], setTags: (tags) => set({ tags }), addTag: (tag) => set((state) => ({ tags: [...state.tags, tag] })), removeTag: (tag) => set((state) => ({ tags: state.tags.filter((t) => t.key !== tag.key) })), updateTag: (tag) => set((state) => ({ tags: state.tags.map((t) => (t.key === tag.key ? tag : t)) })), resetTags: () => set({ tags: [] }), }); // 会话筛选 const filterObj = { search: '', otype: '', tags: [], loadNextPage: true, lastpagetime: '', lastactivetime: '', // dayjs().subtract(30, "days").format('YYYY-MM-DD 00:00'), // 30 days }; const filterSlice = (set) => ({ filter: structuredClone(filterObj), setFilter: (filter) => set(state => ({ filter: { ...state.filter, ...filter } })), setFilterSearch: (search) => set((state) => ({ filter: { ...state.filter, search } })), setFilterOtype: (otype) => set((state) => ({ filter: {...state.filter, otype } })), setFilterTags: (tags) => set((state) => ({ filter: {...state.filter, tags } })), setFilterLoadNextPage: (loadNextPage) => set((state) => ({ filter: {...state.filter, loadNextPage } })), resetFilter: () => set({ filter: structuredClone(filterObj) }), }) // WABA 模板 const templatesSlice = (set) => ({ templates: [], setTemplates: (templates) => set({ templates }), }); const websocketSlice = (set, get) => ({ websocket: null, websocketOpened: null, websocketRetrying: null, websocketRetrytimes: null, setWebsocket: (websocket) => set({ websocket }), setWebsocketOpened: (opened) => set({ websocketOpened: opened }), setWebsocketRetrying: (retrying) => set({ websocketRetrying: retrying }), setWebsocketRetrytimes: (retrytimes) => set({ websocketRetrytimes: retrytimes, websocketRetrying: retrytimes > 0 }), connectWebsocket: (userId) => { const { setWebsocket, setWebsocketOpened, setWebsocketRetrytimes, addError, handleMessage, activeConversations } = get(); const realtimeAPI = new RealTimeAPI( { url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`, protocol: 'WhatsApp', }, () => { setWebsocketOpened(true); setWebsocketRetrytimes(0); }, () => { setWebsocketOpened(false); const newMsgList = Object.keys(activeConversations).reduce((acc, key) => { const newMsgList = activeConversations[key].slice(-10); acc[key] = newMsgList; return acc; }, {}); set({ activeConversations: newMsgList, currentConversation: {} }); }, (n) => setWebsocketRetrytimes(n) ); realtimeAPI.onError(() => addError('Error')); realtimeAPI.onMessage(handleMessage); realtimeAPI.onCompletion(() => addError('Connection broken')); olog('Connecting to websocket...'); setWebsocket(realtimeAPI); }, disconnectWebsocket: () => { const { websocket } = get(); if (websocket) websocket.disconnect(); return set({ websocket: null }); }, reconnectWebsocket: (userId) => { const { disconnectWebsocket, connectWebsocket } = get(); disconnectWebsocket(); setTimeout(() => { connectWebsocket(userId); }, 500); }, handleMessage: (data) => { olog('handleMessage------------------', data); // console.log(data); const { updateMessageItem, sentOrReceivedNewMessage, addGlobalNotify } = get(); 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', 'email.updated', ].includes(resultType)) { updateMessageItem(msgUpdate); } if (!isEmpty(msgRender)) { sentOrReceivedNewMessage(msgRender.conversationid, msgRender); handleNotification(msgRender.senderName, { body: msgRender?.text || `[ ${msgRender.type} ]`, ...(msgRender.type === 'photo' ? { image: msgRender.data.uri } : {}), }); } // 其他通知, 不是消息 if ([ 'email.action.received', ].includes(resultType)) { const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj); addGlobalNotify(msgNotify); } console.log('handleMessage*******************'); }, }); const referenceMsgSlice = (set) => ({ referenceMsg: {}, setReferenceMsg: (referenceMsg) => set({ referenceMsg }), }); const complexMsgSlice = (set) => ({ complexMsg: {}, setComplexMsg: (complexMsg) => set({ complexMsg }), }); const conversationSlice = (set, get) => ({ conversationsListLoading: false, conversationsList: [], currentConversation: {}, closedConversationsList: [], topList: [], pageList: [], setConversationsListLoading: (conversationsListLoading) => set({ conversationsListLoading }), /** * 首次加载 * 搜索结果 */ setConversationsList: (conversationsList) => { const { activeConversations, } = get(); const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); const conversationsTopStateMapped = groupBy(conversationsList, 'top_state'); return set({ topList: conversationsTopStateMapped[1] || [], // conversationsList: conversationsTopStateMapped[0], pageList: conversationsTopStateMapped[0] || [], conversationsList, activeConversations: { ...conversationsMapped, ...activeConversations } }) }, setClosedConversationList: (closedConversationsList) => { const { activeConversations, } = get(); const listMapped = closedConversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); return set({ closedConversationsList, activeConversations: { ...activeConversations, ...listMapped } }); }, addToConversationList: (newList, position='top') => { const { activeConversations, conversationsList, } = get(); // const conversationsIds = Object.keys(activeConversations); const conversationsIds = conversationsList.map((chatItem) => `${chatItem.sn}`); const newConversations = newList.filter((conversation) => !conversationsIds.includes(`${conversation.sn}`)); const newConversationsMapped = newConversations.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); const newListIds = newList.map((chatItem) => `${chatItem.sn}`); const withoutNew = conversationsList.filter((item) => !newListIds.includes(`${item.sn}`)); const updateList = replaceObjectsByKey(conversationsList, newList, 'sn'); const mergedList = position==='top' ? [...newList, ...withoutNew] : [...updateList, ...newConversations]; const mergedListMapped = groupBy(mergedList, 'top_state'); const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0); return set((state) => ({ topList: mergedListMapped[1] || [], pageList: mergedListMapped[0] || [], conversationsList: mergedList, activeConversations: { ...activeConversations, ...newConversationsMapped }, totalNotify: refreshTotalNotify, // totalNotify: state.totalNotify + newConversations.map((ele) => ele.unread_msg_count).reduce((acc, cur) => acc + (cur || 0), 0), })); }, delConversationitem: (conversation) => { const { conversationsList, activeConversations } = get(); const targetId = conversation.sn; const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); conversationsList.splice(targetIndex, 1); return set({ conversationsList: [...conversationsList], activeConversations: { ...activeConversations, [`${targetId}`]: [] }, currentConversation: {}, }); }, setCurrentConversation: (conversation) => { // 清空未读 const { conversationsList } = get(); const targetId = conversation.sn; const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); const targetItemFromList = conversationsList.find((ele) => String(ele.sn) === String(targetId)); targetIndex !== -1 ? conversationsList.splice(targetIndex, 1, { ...conversationsList[targetIndex], unread_msg_count: 0, }) : null; return set((state) => ({ totalNotify: state.totalNotify - (conversation.unread_msg_count || 0), currentConversation: Object.assign({}, conversation, targetItemFromList), referenceMsg: {}, conversationsList: [...conversationsList], })); }, updateCurrentConversation: (conversation) => set((state) => ({ currentConversation: { ...state.currentConversation, ...conversation } })), updateConversationItem: (conversation) => { const { conversationsList } = get(); const targetId = conversation.sn; const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); targetIndex !== -1 ? conversationsList.splice(targetIndex, 1, { ...conversationsList[targetIndex], ...conversation, }) : null; return set({ conversationsList: [...conversationsList] }); }, }); const messageSlice = (set, get) => ({ totalNotify: 0, msgListLoading: false, activeConversations: {}, refreshTotalNotify: () => set((state) => ({ totalNotify: state.conversationsList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0) })), setMsgLoading: (msgListLoading) => set({ msgListLoading }), receivedMessageList: (conversationid, msgList) => set((state) => ({ // msgListLoading: false, activeConversations: { ...state.activeConversations, [String(conversationid)]: msgList }, })), updateMessageItem: (message) => { // msgUpdate // console.log('UPDATE_SENT_MESSAGE_ITEM-----------------------------------------------------------------', message); // 更新会话中的消息 const { activeConversations, conversationsList, currentConversation } = get(); const targetId = message.conversationid; const targetMsgs = (activeConversations[String(targetId)] || []).map((ele) => { // 更新状态 // * 已读的不再更新状态, 有时候投递结果在已读之后返回 if (ele.id === ele.actionId && ele.actionId === message.actionId) { return { ...ele, id: message.id, status: ele.status === 'read' ? ele.status : message.status, dateString: message.dateString }; } else if (String(ele.id) === String(message.id)) { // console.log('old msg ele', ele); const renderStatus = message?.data?.status ? { status: { ...ele.data.status, loading: 0, download: true } } : {}; const keepReply = ele.reply ? { reply: ele.reply } : {}; const keepTemplate = ele.template ? { template: ele.template, template_origin: ele.template_origin, text: ele.text } : {}; return { ...ele, ...message, id: message.id, status: ele.status === 'read' ? ele.status : message.status, dateString: message.dateString, data: { ...ele.data, ...renderStatus }, ...keepReply, ...keepTemplate }; } return ele; }); // 显示会话中其他客户端发送的消息 const targetMsgsIds = targetMsgs.map((ele) => ele.id); if (!targetMsgsIds.includes(message.id)) { targetMsgs.push(message); } const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); let newConversations = []; if (targetIndex !== -1) { // 'delivered' // 更新列表的时间 conversationsList.splice(targetIndex, 1, { ...conversationsList[targetIndex], last_received_time: message.status === 'received' ? dayjs(message.deliverTime).add(8, 'hours').format(DATETIME_FORMAT) : conversationsList[targetIndex].last_received_time, conversation_expiretime: message?.conversation?.expireTime || conversationsList[targetIndex].conversation_expiretime || '', // 保留使用UTC时间 }); } else if (targetIndex === -1) { // 当前客户端不存在的会话 // todo: 设置为当前(在WhatsApp返回号码不一致时) newConversations = [{ ...conversationRow, ...message, sn: targetId, opi_sn: currentConversation.opi_sn, // todo: coli sn last_received_time: message.date, unread_msg_count: 0, whatsapp_name: message.to, //message?.senderName || message?.sender || '', customer_name: message.to, // message?.senderName || message?.sender || '', conversation_expiretime: message?.conversation?.expireTime || '', // 保留使用UTC时间 whatsapp_phone_number: message.type === 'email' ? null : message.to, show_default: message.to || '', last_message: message, channels: { "email": message.type === 'email' ? message.from : null, "phone_number": message.type === 'email' ? null : message.from, "whatsapp_phone_number": message.type === 'email' ? null : message.from, }, }]; } return set({ activeConversations: { ...activeConversations, [String(targetId)]: targetMsgs }, conversationsList: [...newConversations, ...conversationsList], }); }, sentOrReceivedNewMessage: (targetId, message) => { // msgRender: const { activeConversations, conversationsList, currentConversation, totalNotify } = get(); const targetMsgs = activeConversations[String(targetId)] || []; const targetIndex = conversationsList.findIndex((ele) => Number(ele.sn) === Number(targetId)); const lastReceivedTime = (message.type !== 'system' && message.sender !== 'me') ? dayjs(message.date).add(8, 'hours').format(DATETIME_FORMAT) : null; const newConversation = targetIndex !== -1 ? { ...conversationsList[targetIndex], last_received_time: lastReceivedTime || conversationsList[targetIndex].last_received_time, unread_msg_count: Number(targetId) !== Number(currentConversation.sn) && message.sender !== 'me' ? conversationsList[targetIndex].unread_msg_count + 1 : conversationsList[targetIndex].unread_msg_count, last_message: message, } : { ...conversationRow, ...message, sn: targetId, opi_sn: currentConversation.opi_sn, // todo: coli sn last_received_time: dayjs(message.date).add(8, 'hours').format(DATETIME_FORMAT), unread_msg_count: message.sender === 'me' ? 0 : 1, whatsapp_name: message?.senderName || message?.sender || '', customer_name: message?.senderName || message?.sender || '', whatsapp_phone_number: message.type === 'email' ? null : message.from, show_default: message?.senderName || message?.sender || message.from || '', last_message: message, channels: { "email": message.type === 'email' ? message.from : null, "phone_number": message.type === 'email' ? null : message.from, "whatsapp_phone_number": message.type === 'email' ? null : message.from, }, }; conversationsList.splice(targetIndex, 1); conversationsList.unshift(newConversation); // console.log('find in list, i:', targetIndex); // console.log('find in list, chat updated and Top: \n', JSON.stringify(newConversation, null, 2)); // console.log('list updated : \n', JSON.stringify(conversationsList, null, 2)); const isCurrent = Number(targetId) === Number(currentConversation.sn); const updatedCurrent = isCurrent ? { ...currentConversation, last_received_time: dayjs(message.date).add(8, 'hours').format(DATETIME_FORMAT), conversation_expiretime: dayjs(message.date).add(24, 'hours').format(DATETIME_FORMAT), last_message: message, } : {...currentConversation, last_message: message,}; return set({ currentConversation: updatedCurrent, conversationsList: [...conversationsList], totalNotify: totalNotify + (message.sender === 'me' ? 0 : 1), activeConversations: { ...activeConversations, [String(targetId)]: [...targetMsgs, message] }, }); }, }); export const useConversationStore = create( devtools((set, get) => ({ ...initialConversationState, ...websocketSlice(set, get), ...conversationSlice(set, get), ...templatesSlice(set, get), ...messageSlice(set, get), ...referenceMsgSlice(set, get), ...complexMsgSlice(set, get), ...tagsSlice(set, get), ...filterSlice(set, get), ...globalNotifySlice(set, get), // state actions addError: (error) => set((state) => ({ errors: [...state.errors, error] })), setInitial: (v) => set({ initialState: v }), // side effects fetchInitialData: async ({userId, userIds, whatsAppBusiness, ...loginUser}) => { const { setTemplates, setInitial, setTags } = get(); const templates = await fetchTemplates({ waba: whatsAppBusiness }); setTemplates(templates); const myTags = await fetchTags({ opisn: userId}); setTags(myTags); setInitial(true); }, reset: () => set(initialConversationState), }), { name: 'cStore' }) ); export default useConversationStore;