import { create } from 'zustand'; import { RealTimeAPI } from '@/channel/realTimeAPI'; import { olog, isEmpty } from '@/utils/commons'; import { receivedMsgTypeMapped, handleNotification } from '@/channel/whatsappUtils'; import { fetchConversationsList, fetchTemplates, fetchMessages } from '@/actions/ConversationActions'; import { devtools } from 'zustand/middleware'; import { WS_URL } from '@/config'; // 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: [], conversationsList: [], // 对话列表 currentConversation: {}, // 当前对话 activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: [] } referenceMsg: {}, complexMsg: {}, totalNotify: 0, msgListLoading: false, }; 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------------------'); // console.log(data); const { updateMessageItem, sentOrReceivedNewMessage } = 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'].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 } : {}), }); } console.log('handleMessage*******************'); }, }); const referenceMsgSlice = (set) => ({ referenceMsg: {}, setReferenceMsg: (referenceMsg) => set({ referenceMsg }), }); const complexMsgSlice = (set) => ({ complexMsg: {}, setComplexMsg: (complexMsg) => set({ complexMsg }), }); const conversationSlice = (set, get) => ({ conversationsList: [], currentConversation: {}, /** * @deprecated */ setConversationsList: (conversationsList) => { const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {}); return set({ conversationsList, activeConversations: conversationsMapped }); }, addToConversationList: (newList) => { const { activeConversations, conversationsList } = get(); const conversationsIds = Object.keys(activeConversations); 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}`)); return set((state) => ({ conversationsList: [...newList, ...withoutNew], activeConversations: { ...activeConversations, ...newConversationsMapped }, 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)); 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: conversation, 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, 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 (ele.id === 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' ? message.deliverTime : conversationsList[targetIndex].last_received_time, // todo: 需要+8 hours 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 || '', whatsapp_phone_number: message.to, conversation_expiretime: message?.conversation?.expireTime || '', // 保留使用UTC时间 }]; } 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) => String(ele.sn) === String(targetId)); const lastReceivedTime = message.type !== 'system' && message.sender !== 'me' ? message.date : null; const newConversation = targetIndex !== -1 ? { ...conversationsList[targetIndex], 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 : conversationsList[targetIndex].unread_msg_count, } : { ...conversationRow, ...message, sn: targetId, opi_sn: currentConversation.opi_sn, // todo: coli sn last_received_time: message.date, unread_msg_count: message.sender === 'me' ? 0 : 1, whatsapp_name: message?.senderName || message?.sender || '', customer_name: message?.senderName || message?.sender || '', whatsapp_phone_number: message.from, }; conversationsList.splice(targetIndex, 1); conversationsList.unshift(newConversation); return set({ totalNotify: totalNotify + (message.sender === 'me' ? 0 : 1), activeConversations: { ...activeConversations, [String(targetId)]: [...targetMsgs, message] }, conversationsList: [...conversationsList], currentConversation: { ...currentConversation, ...(String(targetId) === String(currentConversation.sn) ? { last_received_time: message.date } : {}), }, }); }, }); 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), // state actions addError: (error) => set((state) => ({ errors: [...state.errors, error] })), setInitial: (v) => set({ initialState: v }), // side effects fetchInitialData: async (userIds) => { const { addToConversationList, setTemplates, setInitial, } = get(); const conversationsList = await fetchConversationsList({ opisn: userIds }); addToConversationList(conversationsList); const templates = await fetchTemplates(); setTemplates(templates); setInitial(true); }, reset: () => set(initialConversationState), })) ); export default useConversationStore;