590 lines
16 KiB
JavaScript
590 lines
16 KiB
JavaScript
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>
|
||
);
|
||
}
|