You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/stores/ConversationStore.js

425 lines
17 KiB
JavaScript

import { create } from 'zustand';
import { RealTimeAPI } from '@/channel/realTimeAPI';
import { olog, isEmpty } from '@/utils/commons';
import { receivedMsgTypeMapped, handleNotification } from '@/channel/whatsappUtils';
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 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: [], // 对话列表
currentConversation: {}, // 当前对话
activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: <messageItem>[] }
referenceMsg: {},
complexMsg: {},
totalNotify: 0,
msgListLoading: false,
};
// 顾问的自定义标签
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: [],
status: [],
labels: [],
};
const filterSlice = (set) => ({
filter: structuredClone(filterObj),
setFilter: (filter) => set({ 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 } })),
setFilterStatus: (status) => set((state) => ({ filter: {...state.filter, status } })),
setFilterLabels: (labels) => set((state) => ({ filter: {...state.filter, labels } })),
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------------------');
// 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) => ({
conversationsListLoading: false,
conversationsList: [],
currentConversation: {},
closedConversationsList: [],
setConversationsListLoading: (conversationsListLoading) => set({ conversationsListLoading }),
/**
* @deprecated
*/
setConversationsList: (conversationsList) => {
const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set({ conversationsList, activeConversations: conversationsMapped });
},
setClosedConversationList: (closedConversationsList) => {
const { activeConversations, } = get();
const listMapped = closedConversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set({ closedConversationsList, activeConversations: { ...activeConversations, ...listMapped } });
},
addToConversationList: (newList) => {
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 mergedList = [...newList, ...withoutNew];
const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0);
return set((state) => ({
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 (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' ? 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 || '',
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) => 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,
}
: {
...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.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),
}
: {...currentConversation};
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),
// state actions
addError: (error) => set((state) => ({ errors: [...state.errors, error] })),
setInitial: (v) => set({ initialState: v }),
// side effects
fetchInitialData: async (userIds) => {
const { addToConversationList, setTemplates, setInitial, setClosedConversationList, setTags } = get();
const conversationsList = await fetchConversationsList({ opisn: userIds });
addToConversationList(conversationsList);
const templates = await fetchTemplates();
setTemplates(templates);
const closedList = await fetchConversationsSearch({ opisn: userIds, session_enable: 0 });
setClosedConversationList(closedList);
const myTags = await fetchTags();
setTags(myTags);
setInitial(true);
},
reset: () => set(initialConversationState),
}))
);
export default useConversationStore;