590 lines
16 KiB
React
590 lines
16 KiB
React
|
import { View, Text, Platform } from "react-native";
|
|||
|
import React, { useState, useCallback, useEffect } from "react";
|
|||
|
import {
|
|||
|
GiftedChat,
|
|||
|
Send,
|
|||
|
InputToolbar,
|
|||
|
Composer,
|
|||
|
Bubble,
|
|||
|
Day,
|
|||
|
LoadEarlier,
|
|||
|
} from "react-native-gifted-chat";
|
|||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|||
|
import { useTailwind } from "tailwind-rn";
|
|||
|
import "dayjs/locale/zh-cn";
|
|||
|
import dayjs from "dayjs";
|
|||
|
import { Image } from "expo-image";
|
|||
|
import { get } from "../../../utils/storeInfo";
|
|||
|
import Toast from "react-native-toast-message";
|
|||
|
import baseRequest from "../../../utils/baseRequest";
|
|||
|
import { generateSignature } from "../../../utils/crypto";
|
|||
|
|
|||
|
const blurhash = "LcKUTa%gOYWBYRt6xuoJo~s8V@fk";
|
|||
|
|
|||
|
/*
|
|||
|
params格式:
|
|||
|
{
|
|||
|
mid: item.mid,
|
|||
|
}
|
|||
|
*/
|
|||
|
|
|||
|
export default function MessageDetail({ navigation, route }) {
|
|||
|
const tailwind = useTailwind();
|
|||
|
const insets = useSafeAreaInsets();
|
|||
|
const params = route.params;
|
|||
|
const [messages, setMessages] = useState([]);
|
|||
|
|
|||
|
//获取本地自身数据
|
|||
|
const [selfData, setSelfData] = useState({});
|
|||
|
useEffect(() => {
|
|||
|
async function getData() {
|
|||
|
try {
|
|||
|
const account = await get("account");
|
|||
|
if (account) {
|
|||
|
setSelfData(account);
|
|||
|
}
|
|||
|
} catch (e) {
|
|||
|
console.error(e);
|
|||
|
}
|
|||
|
}
|
|||
|
getData();
|
|||
|
}, []);
|
|||
|
|
|||
|
//获取主播数据发送自动回复信息并设置页面标题
|
|||
|
useEffect(() => {
|
|||
|
const getData = async () => {
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const signature = await generateSignature({
|
|||
|
mid: params.mid,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const detailResponse = await fetch(
|
|||
|
`${apiUrl}/api/streamer/list_ext_by_mid?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
mid: params.mid,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const detailData = await detailResponse.json();
|
|||
|
if (detailData.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: detailData.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
navigation.setOptions({
|
|||
|
title: detailData.data.streamer_ext.name,
|
|||
|
});
|
|||
|
await sendAutoMessages(
|
|||
|
detailData.data.streamer_ext?.name,
|
|||
|
detailData.data.streamer_ext?.avatar?.images[0]?.urls[0],
|
|||
|
detailData.data.streamer_ext?.auto_response_message
|
|||
|
);
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
};
|
|||
|
if (params?.mid === 1) {
|
|||
|
navigation.setOptions({
|
|||
|
title: "在线客服",
|
|||
|
});
|
|||
|
} else {
|
|||
|
getData();
|
|||
|
}
|
|||
|
}, []);
|
|||
|
|
|||
|
//显示主播自动回复
|
|||
|
const sendAutoMessages = async (name, avatar, content) => {
|
|||
|
const temMessages = [
|
|||
|
{
|
|||
|
_id: 1,
|
|||
|
text: content,
|
|||
|
createdAt: new Date(),
|
|||
|
user: {
|
|||
|
_id: 1,
|
|||
|
name: name,
|
|||
|
avatar: avatar,
|
|||
|
},
|
|||
|
},
|
|||
|
];
|
|||
|
setMessages(temMessages);
|
|||
|
};
|
|||
|
|
|||
|
//读取本地缓存的聊天记录
|
|||
|
// const loadMessages = async () => {
|
|||
|
// const accout = await get("account");
|
|||
|
// const selfMid = accout.mid;
|
|||
|
// const messagesCache = await get(`${selfMid}_to_${params.mid}_messages`);
|
|||
|
// if (messagesCache) {
|
|||
|
// setMessages(messagesCache);
|
|||
|
// }
|
|||
|
// };
|
|||
|
|
|||
|
//查询session
|
|||
|
const [sessionId, setSessionId] = useState();
|
|||
|
useEffect(() => {
|
|||
|
const getSession = async () => {
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const account = await get("account");
|
|||
|
const signature = await generateSignature({
|
|||
|
mid: account.mid,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const detailResponse = await fetch(
|
|||
|
`${apiUrl}/api/contact_customer_service_session/list_by_mid?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
mid: account.mid,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const detailData = await detailResponse.json();
|
|||
|
if (detailData.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: detailData.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
if (detailData.data.session) {
|
|||
|
setSessionId(detailData.data.session.id);
|
|||
|
return;
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
};
|
|||
|
getSession();
|
|||
|
}, []);
|
|||
|
|
|||
|
//创建session
|
|||
|
const createSession = async () => {
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const account = await get("account");
|
|||
|
const signature = await generateSignature({
|
|||
|
sub_mid: account.mid,
|
|||
|
obj_mid: 0,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const createResponse = await fetch(
|
|||
|
`${apiUrl}/api/contact_customer_service_session/create?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
sub_mid: account.mid,
|
|||
|
obj_mid: 0,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const createData = await createResponse.json();
|
|||
|
if (createData.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: createData.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
setSessionId(createData.data.session_id);
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
//请求历史记录
|
|||
|
const [offset, setOffset] = useState(1);
|
|||
|
const [more, setMore] = useState(1);
|
|||
|
const loadEarlierHistory = async () => {
|
|||
|
if (params?.mid !== 1) return;
|
|||
|
if (!more) return;
|
|||
|
if (sessionId === undefined) return;
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const signature = await generateSignature({
|
|||
|
session_id: sessionId,
|
|||
|
offset: offset,
|
|||
|
limit: 12,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const response = await fetch(
|
|||
|
`${apiUrl}/api/contact_customer_service/list_by_session_id?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
session_id: sessionId,
|
|||
|
offset: offset,
|
|||
|
limit: 12,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const data = await response.json();
|
|||
|
if (data.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: data.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
setOffset(data.data.offset);
|
|||
|
setMore(data.data.more);
|
|||
|
const account = await get("account");
|
|||
|
const temMessages = data.data.list.map((item) => {
|
|||
|
if (item.predicate === 0) {
|
|||
|
return {
|
|||
|
_id: item.id,
|
|||
|
createdAt: new Date(item.ct * 1000).toISOString(),
|
|||
|
text: item.message,
|
|||
|
user: {
|
|||
|
_id: account?.mid,
|
|||
|
name: account?.name,
|
|||
|
avatar: account?.avatar?.images[0]?.urls[0],
|
|||
|
},
|
|||
|
};
|
|||
|
} else {
|
|||
|
return {
|
|||
|
_id: item.id,
|
|||
|
createdAt: new Date(item.ct * 1000).toISOString(),
|
|||
|
text: item.message,
|
|||
|
user: {
|
|||
|
_id: 0,
|
|||
|
name: "客服",
|
|||
|
avatar: require("../../../assets/icon.png"),
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
});
|
|||
|
setMessages((prev) => [...prev, ...temMessages]);
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
//请求最新1条历史记录,如果与之前不同则加在第1条
|
|||
|
const updateLatestHistory = async () => {
|
|||
|
if (params?.mid !== 1) return;
|
|||
|
if (sessionId === undefined) return;
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const signature = await generateSignature({
|
|||
|
session_id: sessionId,
|
|||
|
offset: 0,
|
|||
|
limit: 1,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const response = await fetch(
|
|||
|
`${apiUrl}/api/contact_customer_service/list_by_session_id?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
session_id: sessionId,
|
|||
|
offset: 0,
|
|||
|
limit: 1,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const data = await response.json();
|
|||
|
if (data.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: data.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
const account = await get("account");
|
|||
|
const temMessages = data.data.list.map((item) => {
|
|||
|
if (item.predicate === 0) {
|
|||
|
return {
|
|||
|
_id: item.id,
|
|||
|
createdAt: new Date(item.ct * 1000).toISOString(),
|
|||
|
text: item.message,
|
|||
|
user: {
|
|||
|
_id: account.mid,
|
|||
|
name: account.name,
|
|||
|
avatar: account.avatar.images[0].urls[0],
|
|||
|
},
|
|||
|
};
|
|||
|
} else {
|
|||
|
return {
|
|||
|
_id: item.id,
|
|||
|
createdAt: new Date(item.ct * 1000).toISOString(),
|
|||
|
text: item.message,
|
|||
|
user: {
|
|||
|
_id: 1,
|
|||
|
name: "客服",
|
|||
|
avatar: require("../../../assets/icon.png"),
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
});
|
|||
|
setMessages((prev) => {
|
|||
|
if (prev[0]?._id === temMessages[0]?._id) {
|
|||
|
return prev;
|
|||
|
} else {
|
|||
|
return [...temMessages, ...prev];
|
|||
|
}
|
|||
|
});
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 轮询更新历史记录
|
|||
|
useEffect(() => {
|
|||
|
loadEarlierHistory();
|
|||
|
updateLatestHistory();
|
|||
|
// 设置轮询请求,每隔一定时间执行一次
|
|||
|
const intervalId = setInterval(() => {
|
|||
|
updateLatestHistory();
|
|||
|
}, 3000); // 间隔时间为3秒
|
|||
|
|
|||
|
// 在组件卸载时清除定时器
|
|||
|
return () => {
|
|||
|
clearInterval(intervalId);
|
|||
|
};
|
|||
|
}, [sessionId]);
|
|||
|
|
|||
|
//发送私信功能
|
|||
|
const onSend = useCallback(
|
|||
|
async (messages = []) => {
|
|||
|
if (!messages[0].text) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: "不可发送空内容",
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
//如果是第一次发送,需要创建session
|
|||
|
if (!sessionId) await createSession();
|
|||
|
|
|||
|
//查询历史记录的时候后移一位,防止记录重复
|
|||
|
setOffset((prev) => prev + 1);
|
|||
|
//请求接口发送私信
|
|||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
|||
|
try {
|
|||
|
const base = await baseRequest();
|
|||
|
const signature = await generateSignature({
|
|||
|
session_id: sessionId,
|
|||
|
predicate: 0,
|
|||
|
message: messages[0].text,
|
|||
|
...base,
|
|||
|
});
|
|||
|
const response = await fetch(
|
|||
|
`${apiUrl}/api/contact_customer_service/create?signature=${signature}`,
|
|||
|
{
|
|||
|
method: "POST",
|
|||
|
headers: {
|
|||
|
"Content-Type": "application/json",
|
|||
|
},
|
|||
|
body: JSON.stringify({
|
|||
|
session_id: sessionId,
|
|||
|
predicate: 0,
|
|||
|
message: messages[0].text,
|
|||
|
...base,
|
|||
|
}),
|
|||
|
}
|
|||
|
);
|
|||
|
const data = await response.json();
|
|||
|
if (data.ret === -1) {
|
|||
|
Toast.show({
|
|||
|
type: "error",
|
|||
|
text1: data.msg,
|
|||
|
topOffset: 60,
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
updateLatestHistory();
|
|||
|
} catch (error) {
|
|||
|
console.error(error);
|
|||
|
}
|
|||
|
// //每次发送都缓存信息到本地
|
|||
|
// addArr(`${selfData.mid}_to_${params.mid}_messages`, messages);
|
|||
|
},
|
|||
|
[selfData, sessionId]
|
|||
|
);
|
|||
|
|
|||
|
//发送按钮样式
|
|||
|
const renderSend = useCallback((props) => {
|
|||
|
return (
|
|||
|
<Send
|
|||
|
{...props}
|
|||
|
disabled={!props.text}
|
|||
|
containerStyle={tailwind(
|
|||
|
"px-4 bg-[#FF669E] justify-center items-center rounded-lg h-10 ml-2"
|
|||
|
)}
|
|||
|
>
|
|||
|
<Text style={tailwind("text-white font-medium text-sm")}>发送</Text>
|
|||
|
</Send>
|
|||
|
);
|
|||
|
}, []);
|
|||
|
|
|||
|
//日期格式
|
|||
|
const renderDay = useCallback((props) => {
|
|||
|
const now = dayjs();
|
|||
|
const isToday = now.diff(props.currentMessage.createdAt, "day") === 0;
|
|||
|
const isYesterday = now.diff(props.currentMessage.createdAt, "day") === 1;
|
|||
|
if (isToday) {
|
|||
|
return <Day {...props} dateFormat="HH:mm" />;
|
|||
|
} else if (isYesterday) {
|
|||
|
return <Day {...props} dateFormat="昨天 HH:mm" />;
|
|||
|
} else {
|
|||
|
return <Day {...props} dateFormat="YYYY/MM/DD HH:mm" />;
|
|||
|
}
|
|||
|
}, []);
|
|||
|
|
|||
|
//时间格式
|
|||
|
const renderTime = useCallback((props) => {
|
|||
|
return <></>;
|
|||
|
}, []);
|
|||
|
|
|||
|
//输入框样式
|
|||
|
const renderComposer = useCallback((props) => {
|
|||
|
return (
|
|||
|
<Composer
|
|||
|
{...props}
|
|||
|
textInputStyle={tailwind(
|
|||
|
"bg-[#FFFFFF1A] text-white px-4 py-2 rounded-lg text-sm ml-0"
|
|||
|
)}
|
|||
|
textInputProps={{ maxLength: 140 }}
|
|||
|
/>
|
|||
|
);
|
|||
|
}, []);
|
|||
|
|
|||
|
//整个输入栏样式
|
|||
|
const renderInputToolbar = useCallback((props) => {
|
|||
|
if (params?.mid === 1) {
|
|||
|
return (
|
|||
|
<InputToolbar
|
|||
|
renderComposer={() => renderComposer(props)}
|
|||
|
renderSend={() => renderSend(props)}
|
|||
|
containerStyle={tailwind("p-2 bg-[#13121F] border-[#FFFFFF26]")}
|
|||
|
primaryStyle={tailwind("items-center")}
|
|||
|
/>
|
|||
|
);
|
|||
|
} else {
|
|||
|
return <View style={tailwind("flex-1")}></View>;
|
|||
|
}
|
|||
|
}, []);
|
|||
|
|
|||
|
//头像样式
|
|||
|
const renderAvatar = useCallback((props) => {
|
|||
|
return (
|
|||
|
<Image
|
|||
|
style={tailwind("w-10 h-10 rounded-full")}
|
|||
|
source={props.currentMessage.user.avatar}
|
|||
|
placeholder={blurhash}
|
|||
|
contentFit="cover"
|
|||
|
transition={1000}
|
|||
|
cachePolicy="disk"
|
|||
|
/>
|
|||
|
);
|
|||
|
}, []);
|
|||
|
|
|||
|
//气泡样式
|
|||
|
const renderBubble = useCallback((props) => {
|
|||
|
return (
|
|||
|
<Bubble
|
|||
|
{...props}
|
|||
|
wrapperStyle={{
|
|||
|
left: tailwind("bg-white p-1"),
|
|||
|
right: tailwind("p-1"),
|
|||
|
}}
|
|||
|
/>
|
|||
|
);
|
|||
|
}, []);
|
|||
|
|
|||
|
//加载更早信息样式
|
|||
|
const renderLoadEarlier = useCallback(
|
|||
|
(props) => {
|
|||
|
return (
|
|||
|
<LoadEarlier
|
|||
|
{...props}
|
|||
|
label={more === 0 || params.mid !== 1 ? "无更早消息" : "查看更早"}
|
|||
|
wrapperStyle={tailwind("bg-[#FFFFFF1A]")}
|
|||
|
/>
|
|||
|
);
|
|||
|
},
|
|||
|
[more]
|
|||
|
);
|
|||
|
|
|||
|
return (
|
|||
|
<View
|
|||
|
style={{
|
|||
|
paddingBottom: insets.bottom,
|
|||
|
paddingLeft: insets.left,
|
|||
|
paddingRight: insets.right,
|
|||
|
...tailwind("flex-1 bg-[#13121F]"),
|
|||
|
}}
|
|||
|
>
|
|||
|
<View style={tailwind("flex-1")}>
|
|||
|
<GiftedChat
|
|||
|
placeholder={params?.mid === 1 ? "输入新消息" : "爱就大胆说出来!"}
|
|||
|
alwaysShowSend
|
|||
|
locale={"zh-cn"}
|
|||
|
keyboardShouldPersistTaps="never"
|
|||
|
alignTop={false}
|
|||
|
listViewProps={{
|
|||
|
contentContainerStyle: { flexGrow: 1, justifyContent: "flex-end" },
|
|||
|
}}
|
|||
|
onLoadEarlier={() => loadEarlierHistory()}
|
|||
|
showUserAvatar
|
|||
|
showAvatarForEveryMessage
|
|||
|
renderAvatarOnTop
|
|||
|
messagesContainerStyle={tailwind("bg-[#13121F] pb-4")}
|
|||
|
renderAvatar={renderAvatar}
|
|||
|
renderDay={renderDay}
|
|||
|
renderInputToolbar={renderInputToolbar}
|
|||
|
renderBubble={renderBubble}
|
|||
|
loadEarlier
|
|||
|
renderLoadEarlier={renderLoadEarlier}
|
|||
|
renderTime={renderTime}
|
|||
|
messages={messages}
|
|||
|
onSend={(messages) => onSend(messages)}
|
|||
|
user={{
|
|||
|
_id: selfData?.mid,
|
|||
|
name: selfData?.name,
|
|||
|
avatar: selfData?.avatar?.images[0]?.urls[0],
|
|||
|
}}
|
|||
|
/>
|
|||
|
</View>
|
|||
|
</View>
|
|||
|
);
|
|||
|
}
|