import { create } from 'zustand'; import { RealTimeAPI } from '@/channel/realTimeAPI'; import { olog, isEmpty, groupBy, sortArrayByOrder, pick, sortKeys, omit, sortObjectsByKeysMap, merge } from '@/utils/commons'; import { logWebsocket, clean7DaysWebsocketLog } from '@/utils/indexedDB' 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'; import EmailSlice from './EmailSlice'; const replaceObjectsByKey = (arr1, arr2, key) => { const map2 = new Map(arr2.map(ele => [ele[key], ele])); return arr1.map(item => map2.has(item[key]) ? map2.get(item[key]) : item); } const sortConversationList = (list) => { const mergedListMapped = groupBy(list, 'top_state'); const topValOrder = Object.keys(mergedListMapped).filter(ss => ss !== '1').sort((a, b) => b - a); const pagelist = topValOrder.reduce((r, topVal) => r.concat(mergedListMapped[String(topVal)]), []); return { topList: mergedListMapped['1'] || [], pageList: pagelist, } }; // 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: '', top_state: 0, session_type: 0, }; 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, detailPopupOpen: false, wai: {}, }; const globalNotifySlice = (set) => ({ globalNotify: [], setGlobalNotify: (notify) => set(() => ({ globalNotify: notify })), addGlobalNotify: (notify) => set((state) => ({ globalNotify: [notify, ...state.globalNotify] })), removeGlobalNotify: (id) => set((state) => ({ globalNotify: state.globalNotify.filter(item => item.id !== id) })), clearGlobalNotify: () => set(() => ({ globalNotify: [] })), }) const waiSlice = (set) => ({ wai: {}, setWai: (wai) => set({ wai }), }); // 顾问的自定义标签 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 = { pagesize: '', 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('websocket Message IN ⬇', JSON.stringify(data, null, 2)); logWebsocket(data, 'I'); // olog('websocket Messages ----', data); // console.log(data); const { updateMessageItem, sentOrReceivedNewMessage, addGlobalNotify, setWai, addToConversationList, updateMailboxCount } = 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'; } 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 (['email.updated', 'email.inbound.received',].includes(resultType)) { updateMailboxCount({ opi_sn: msgObj.opi_sn }) // if (!isEmpty(msgRender)) { // const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj); // addGlobalNotify(msgNotify); // } return false; } if ([ 'whatsapp.message.updated', 'message', 'error', 'email.updated', 'wai.message.updated', ].includes(resultType) && !isEmpty(msgUpdate)) { 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); } // WhatsApp creds update if ([ 'wai.creds.update' ].includes(resultType)) { const _data = receivedMsgTypeMapped[resultType].getMsg(result); setWai(_data) if (['offline', 'close'].includes(_data.status)) { const msgNotify = receivedMsgTypeMapped[resultType].contentToNotify(msgObj); addGlobalNotify(msgNotify); } // setTimeout(() => { // setWai({}); // 60s 后清空 // }, 60_000); } // 会话表 更新 if (['session.new', 'session.updated'].includes(resultType) && result.webhooksource !== 'email' ) { const sessionList = receivedMsgTypeMapped[resultType].getMsg(result); addToConversationList(sessionList || [], 'top') } // 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, currentConversation } = get(); // 让当前会话显示在页面上 let _tmpCurrentMsgs = []; if (currentConversation.sn) { // _tmpCurrentMsgs = activeConversations[currentConversation.sn]; } const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); const indexCurrent = currentConversation.sn ? conversationsList.findIndex(ele => Number(ele.sn) === Number(currentConversation.sn)) : -1; if (indexCurrent !== -1) { const [currentElement] = conversationsList.splice(indexCurrent, 1); conversationsList.unshift(currentElement); // Add to top // const hasCurrent = Object.keys(conversationsMapped).findIndex(sn => Number(sn) === Number(currentConversation.sn)) !== -1; // conversationsMapped[currentConversation.sn] = _tmpCurrentMsgs; // conversationsList.unshift(hasCurrent ? ) // hasCurrent ? 0 : conversationsList.unshift(currentConversation); } const { topList, pageList } = sortConversationList(conversationsList); return set({ topList, // conversationsList: conversationsTopStateMapped[0], pageList, 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, currentConversation } = 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 mergedListMsgs = { ...newConversationsMapped, ...activeConversations, }; const needUpdateCurrent = -1 !== newList.findIndex(row => Number(row.sn) === Number(currentConversation.sn)); const updateCurrent = needUpdateCurrent ? { currentConversation: newList.find(row => Number(row.sn) === Number(currentConversation.sn)) } : {}; // 让当前会话显示在页面上 const indexCurrent = currentConversation.sn ? mergedList.findIndex(ele => Number(ele.sn) === Number(currentConversation.sn)) : -1; if (indexCurrent !== -1 ) { const [currentElement] = mergedList.splice(indexCurrent, 1); mergedList.unshift(currentElement); // Add to top // hasCurrent ? 0 : mergedList.unshift(currentConversation); } const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0); const { topList, pageList } = sortConversationList(mergedList) return set((state) => ({ ...updateCurrent, topList, pageList, conversationsList: mergedList, activeConversations: mergedListMsgs, 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); const { topList, pageList } = sortConversationList(conversationsList) return set({ topList, pageList, 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; const mergedListMapped = groupBy(conversationsList, 'top_state'); return set((state) => ({ totalNotify: state.totalNotify - (conversation.unread_msg_count || 0), currentConversation: Object.assign({}, conversation, targetItemFromList), referenceMsg: {}, // topList: mergedListMapped['1'] || [], // pageList: mergedListMapped['0'] || [], // conversationsList: [...conversationsList], })); }, updateCurrentConversation: (conversation) => { const { updateConversationItem, currentConversation } = get(); updateConversationItem({...currentConversation, ...conversation}) return 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; const { topList, pageList } = sortConversationList(conversationsList) return set({ topList, pageList, 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, setFilter } = get(); const targetId = message.conversationid; const targetMsgs = (activeConversations[String(targetId)] || []).map((ele) => { // 更新状态 // * 已读的不再更新状态, 有时候投递结果在已读之后返回 // if (ele.id === ele.actionId && ele.actionId === message.actionId) { if (ele.actionId === message.actionId && !isEmpty(ele.actionId) && !isEmpty(message.actionId)) { // console.log('actionID', message.actionId, ele.actionId) // WABA: 同步返回, 根据actionId 更新消息的id; const toUpdateFields = pick(message, ['msgOrigin', 'id', 'status', 'dateString', 'replyButton', 'coli_id', 'coli_sn']); return { ...ele, ...toUpdateFields, status: ele.status === 'read' ? ele.status : message.status, }; } else if (String(ele.id) === String(message.id)) { // console.log('id', message.id, ele.id) // WABA: 异步的后续状态更新, id已更新为wamid // console.log('coming msg', message.type, message); // console.log('old msg ele', ele.type, 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); } setFilter({ loadNextPage: true }); return set({ activeConversations: { ...activeConversations, [String(targetId)]: targetMsgs }, }); }, sentOrReceivedNewMessage: (targetId, message) => { // msgRender: // console.log('sentOrReceivedNewMessage', targetId, message) const { activeConversations, setFilter } = get(); const targetMsgs = activeConversations[String(targetId)] || []; setFilter({ loadNextPage: true }); return set({ 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), ...EmailSlice(set, get), ...waiSlice(set, get), // state actions addError: (error) => set((state) => ({ errors: [...state.errors, error] })), setInitial: (v) => set({ initialState: v }), // side effects fetchInitialData: async ({userId, whatsAppBusiness, ...loginUser}) => { const { addToConversationList, setTemplates, setInitial, setTags, initMailbox } = get(); initMailbox({ userId, dei_sn: loginUser.accountList[0].OPI_DEI_SN, opi_sn: loginUser.accountList[0].OPI_SN, userIdStr: loginUser.userIdStr }) const conversationsList = await fetchConversationsList({ opisn: userId }); addToConversationList(conversationsList); 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;