You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
558 lines
15 KiB
558 lines
15 KiB
import React, { useState, useEffect, useRef } from "react"; |
|
import customRF from '../utils/customRF'; |
|
import { |
|
View, |
|
Text, |
|
StyleSheet, |
|
TextInput, |
|
TouchableOpacity, |
|
FlatList, |
|
Platform, |
|
ImageBackground, |
|
StatusBar, |
|
SafeAreaView, |
|
Modal, |
|
Animated |
|
} from "react-native"; |
|
import { useNavigation } from "@react-navigation/native"; |
|
import { NativeStackNavigationProp } from "@react-navigation/native-stack"; |
|
import { chatService } from "../services/api/chat"; |
|
import useUserStore from "../store/user"; |
|
import AsyncStorage from "@react-native-async-storage/async-storage"; |
|
import { t } from "../i18n"; |
|
|
|
interface Message { |
|
id?: string; |
|
mimetype: string; |
|
userWs: string; |
|
app_id: string; |
|
country: number; |
|
body: string; |
|
text: string; |
|
type: string; |
|
isMe?: boolean; |
|
timestamp?: Date; |
|
} |
|
|
|
type TabType = "customer" | "product" | "notification"; |
|
|
|
type RootStackParamList = { |
|
Login: undefined; |
|
// other screens... |
|
}; |
|
|
|
export const ChatScreen = () => { |
|
const [messages, setMessages] = useState<Message[]>([]); |
|
const [inputText, setInputText] = useState(""); |
|
const [activeTab, setActiveTab] = useState<TabType>("customer"); |
|
const [country, setCountry] = useState<string>(""); // Store the country code |
|
const { user } = useUserStore(); |
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>(); |
|
|
|
// Add FlatList ref for auto-scrolling |
|
const flatListRef = useRef<FlatList>(null); |
|
|
|
// Auto-scroll to bottom when messages change |
|
useEffect(() => { |
|
if (messages.length > 0 && user.user_id) { |
|
setTimeout(() => { |
|
flatListRef.current?.scrollToEnd({ animated: true }); |
|
}, 100); |
|
} |
|
}, [messages]); |
|
|
|
// Get country from AsyncStorage |
|
useEffect(() => { |
|
const getCountry = async () => { |
|
try { |
|
const selectedCountry = await AsyncStorage.getItem('@selected_country'); |
|
if (selectedCountry) { |
|
const countryData = JSON.parse(selectedCountry); |
|
setCountry(countryData.name_en || ""); |
|
} |
|
} catch (error) { |
|
console.error('Error getting country data:', error); |
|
} |
|
}; |
|
|
|
// Only get country if user is logged in |
|
if (user.user_id) { |
|
getCountry(); |
|
} |
|
}, [user.user_id]); |
|
|
|
// 添加导航到登录页面的函数 |
|
const goToLogin = () => { |
|
navigation.navigate("Login"); |
|
}; |
|
|
|
const sendMessage = () => { |
|
// 如果用户未登录,直接返回 |
|
if (!user.user_id) { |
|
return; |
|
} |
|
|
|
if (inputText.trim() === "") return; |
|
|
|
const newMessage: Message = { |
|
mimetype: "text/plain", |
|
userWs: "unknown", |
|
app_id: user.user_id ? user.user_id.toString() : "", |
|
country: user.country_code, |
|
body: "", |
|
text: inputText, |
|
type: "text", |
|
isMe: true, |
|
timestamp: new Date(), |
|
id: Date.now().toString(), // Add unique id for keyExtractor |
|
}; |
|
|
|
// Extract only the properties that chatService.sendMessage expects |
|
const chatServiceMessage = { |
|
type: newMessage.type, |
|
mimetype: newMessage.mimetype, |
|
userWs: newMessage.userWs, |
|
app_id: newMessage.app_id, |
|
country: newMessage.country.toString(), |
|
body: newMessage.body, |
|
text: newMessage.text |
|
}; |
|
|
|
// Add user message to the chat UI |
|
setMessages([...messages, newMessage]); |
|
setInputText(""); |
|
|
|
// Add simulated response with loading indicator |
|
const simulatedId = `simulated-${Date.now()}`; |
|
const simulatedResponse: Message = { |
|
mimetype: "text/plain", |
|
userWs: "system", |
|
app_id: "system", |
|
country: user.country_code, |
|
body: "", |
|
text: `${t('chat.typing_message')}...`, |
|
type: "chat", |
|
isMe: false, |
|
timestamp: new Date(), |
|
id: simulatedId, |
|
}; |
|
|
|
// Add simulated message after a short delay to make it feel more natural |
|
setTimeout(() => { |
|
setMessages(prevMessages => [...prevMessages, simulatedResponse]); |
|
}, 800); |
|
|
|
const data = { |
|
newMessage:chatServiceMessage, |
|
} |
|
|
|
// Send actual message to API |
|
chatService.sendMessage(data) |
|
.then(response => { |
|
// When real response arrives, replace simulated message |
|
setMessages(prevMessages => { |
|
// Filter out the simulated message and add real response |
|
const filtered = prevMessages.filter(msg => msg.id !== simulatedId); |
|
|
|
// Create the real response message object |
|
const realResponse: Message = { |
|
mimetype: "text/plain", |
|
userWs: "system", |
|
app_id: "system", |
|
country: user.country_code, |
|
body: "", |
|
text: response?.reply || t('chat.default_response'), |
|
type: "chat", |
|
isMe: false, |
|
timestamp: new Date(), |
|
id: `real-${Date.now()}`, |
|
}; |
|
|
|
return [...filtered, realResponse]; |
|
}); |
|
}) |
|
.catch(error => { |
|
// In case of error, replace simulated message with error message |
|
console.error('Chat API error:', error); |
|
setMessages(prevMessages => { |
|
const filtered = prevMessages.filter(msg => msg.id !== simulatedId); |
|
|
|
const errorResponse: Message = { |
|
mimetype: "text/plain", |
|
userWs: "system", |
|
app_id: "system", |
|
country: user.country_code, |
|
body: "", |
|
text: t('chat.error_response'), |
|
type: "chat", |
|
isMe: false, |
|
timestamp: new Date(), |
|
id: `error-${Date.now()}`, |
|
}; |
|
|
|
return [...filtered, errorResponse]; |
|
}); |
|
}); |
|
}; |
|
|
|
// Generate a unique key for each message |
|
const keyExtractor = (item: Message, index: number): string => { |
|
return item.id || index.toString(); |
|
}; |
|
|
|
const renderMessage = ({ item }: { item: Message }) => ( |
|
<View |
|
style={[ |
|
styles.messageContainer, |
|
item.isMe ? styles.myMessage : styles.theirMessage, |
|
]} |
|
> |
|
<Text style={styles.messageText}>{item.text}</Text> |
|
<Text style={styles.timestamp}> |
|
{item.timestamp?.toLocaleTimeString([], { |
|
hour: "2-digit", |
|
minute: "2-digit", |
|
})} |
|
</Text> |
|
</View> |
|
); |
|
|
|
const renderTabContent = () => { |
|
switch (activeTab) { |
|
case "customer": |
|
return ( |
|
<View style={styles.tabContent}> |
|
<FlatList |
|
ref={flatListRef} |
|
data={messages} |
|
renderItem={renderMessage} |
|
keyExtractor={keyExtractor} |
|
contentContainerStyle={styles.messageList} |
|
showsVerticalScrollIndicator={false} |
|
scrollEnabled={!!user.user_id} |
|
/> |
|
</View> |
|
); |
|
case "product": |
|
return ( |
|
<View style={styles.tabContent}> |
|
<FlatList |
|
ref={flatListRef} |
|
data={messages} |
|
renderItem={renderMessage} |
|
keyExtractor={keyExtractor} |
|
contentContainerStyle={styles.messageList} |
|
showsVerticalScrollIndicator={false} |
|
scrollEnabled={!!user.user_id} |
|
/> |
|
</View> |
|
); |
|
case "notification": |
|
return ( |
|
<View style={styles.tabContent}> |
|
<FlatList |
|
ref={flatListRef} |
|
data={messages} |
|
renderItem={renderMessage} |
|
keyExtractor={keyExtractor} |
|
contentContainerStyle={styles.messageList} |
|
showsVerticalScrollIndicator={false} |
|
scrollEnabled={!!user.user_id} |
|
/> |
|
</View> |
|
); |
|
} |
|
}; |
|
|
|
return ( |
|
<SafeAreaView style={styles.safeArea}> |
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" /> |
|
<View style={styles.safeAreaContent}> |
|
<View style={styles.container}> |
|
<ImageBackground |
|
source={require('../../assets/img/DefaultWallpaper.png')} |
|
style={styles.backgroundImage} |
|
resizeMode="cover" |
|
> |
|
<View style={styles.tabBar}> |
|
<TouchableOpacity |
|
style={[styles.tab, activeTab === "customer" && styles.activeTab]} |
|
onPress={() => user.user_id && setActiveTab("customer")} |
|
disabled={!user.user_id} |
|
> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "customer" && styles.activeTabText, |
|
]} |
|
> |
|
{t('chat.customer_service')} |
|
</Text> |
|
</TouchableOpacity> |
|
<TouchableOpacity |
|
style={[styles.tab, activeTab === "product" && styles.activeTab]} |
|
onPress={() => user.user_id && setActiveTab("product")} |
|
disabled={!user.user_id} |
|
> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "product" && styles.activeTabText, |
|
]} |
|
> |
|
{t('chat.product_support')} |
|
</Text> |
|
</TouchableOpacity> |
|
<TouchableOpacity |
|
style={[styles.tab, activeTab === "notification" && styles.activeTab]} |
|
onPress={() => user.user_id && setActiveTab("notification")} |
|
disabled={!user.user_id} |
|
> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "notification" && styles.activeTabText, |
|
]} |
|
> |
|
{t('chat.notifications')} |
|
</Text> |
|
</TouchableOpacity> |
|
</View> |
|
{renderTabContent()} |
|
<View style={styles.inputContainer}> |
|
<TextInput |
|
style={[styles.input, !user.user_id && styles.disabledInput]} |
|
value={inputText} |
|
onChangeText={user.user_id ? setInputText : undefined} |
|
placeholder={t('chat.input_message')} |
|
multiline |
|
editable={!!user.user_id} |
|
/> |
|
<TouchableOpacity |
|
style={[styles.sendButton, !user.user_id && styles.disabledButton]} |
|
onPress={user.user_id ? sendMessage : undefined} |
|
disabled={!user.user_id} |
|
> |
|
<Text style={styles.sendButtonText}>{t('chat.send')}</Text> |
|
</TouchableOpacity> |
|
</View> |
|
</ImageBackground> |
|
|
|
{/* 未登录遮罩 */} |
|
{!user.user_id && ( |
|
<View style={styles.loginOverlay}> |
|
<View style={styles.blurContainer}> |
|
<View style={styles.loginPromptContainer}> |
|
<View style={styles.loginIcon}> |
|
<Text style={styles.loginIconText}>💬</Text> |
|
</View> |
|
<Text style={styles.loginPromptTitle}> |
|
{t("chat.login_required_title", "请先登录")} |
|
</Text> |
|
<Text style={styles.loginPromptSubtitle}> |
|
{t("chat.login_required_subtitle", "登录后即可使用聊天功能")} |
|
</Text> |
|
<TouchableOpacity |
|
style={styles.loginButton} |
|
onPress={goToLogin} |
|
> |
|
<Text style={styles.loginButtonText}> |
|
{t("chat.login_now", "立即登录")} |
|
</Text> |
|
</TouchableOpacity> |
|
</View> |
|
</View> |
|
</View> |
|
)} |
|
</View> |
|
</View> |
|
</SafeAreaView> |
|
); |
|
}; |
|
|
|
const styles = StyleSheet.create({ |
|
safeArea: { |
|
flex: 1, |
|
backgroundColor: '#fff', |
|
}, |
|
safeAreaContent: { |
|
flex: 1, |
|
paddingTop: Platform.OS === 'android' ? 0 : 0, |
|
}, |
|
container: { |
|
flex: 1, |
|
backgroundColor: '#fff', |
|
}, |
|
backgroundImage: { |
|
flex: 1, |
|
width: '100%', |
|
height: '100%', |
|
}, |
|
tabBar: { |
|
flexDirection: "row", |
|
backgroundColor: "#007a6c", |
|
borderBottomWidth: 1, |
|
borderBottomColor: "#eef0f1", |
|
shadowColor: "#000", |
|
shadowOffset: { |
|
width: 0, |
|
height: 2, |
|
}, |
|
shadowOpacity: 0.25, |
|
shadowRadius: 3.84, |
|
elevation: 5, |
|
}, |
|
tab: { |
|
flex: 1, |
|
paddingVertical: 15, |
|
alignItems: "center", |
|
}, |
|
activeTab: { |
|
borderBottomWidth: 2, |
|
borderBottomColor: "#eef0f1", |
|
}, |
|
tabText: { |
|
fontSize: customRF(14), |
|
color: "#fff", |
|
}, |
|
activeTabText: { |
|
color: "#fff", |
|
fontWeight: "600", |
|
}, |
|
tabContent: { |
|
flex: 1, |
|
}, |
|
messageList: { |
|
padding: 10, |
|
}, |
|
messageContainer: { |
|
maxWidth: "80%", |
|
padding: 10, |
|
borderRadius: 10, |
|
marginVertical: 5, |
|
}, |
|
myMessage: { |
|
alignSelf: "flex-end", |
|
backgroundColor: "#dcf8c6", |
|
}, |
|
theirMessage: { |
|
alignSelf: "flex-start", |
|
backgroundColor: "white", |
|
}, |
|
messageText: { |
|
fontSize: 16, |
|
}, |
|
timestamp: { |
|
fontSize: 12, |
|
color: "#666", |
|
alignSelf: "flex-end", |
|
marginTop: 5, |
|
}, |
|
inputContainer: { |
|
flexDirection: "row", |
|
padding: 10, |
|
backgroundColor: "white", |
|
borderTopWidth: 1, |
|
borderTopColor: "#ddd", |
|
}, |
|
input: { |
|
flex: 1, |
|
backgroundColor: "#f0f0f0", |
|
borderRadius: 20, |
|
paddingHorizontal: 15, |
|
paddingVertical: 8, |
|
marginRight: 10, |
|
maxHeight: 100, |
|
}, |
|
sendButton: { |
|
backgroundColor: "#128C7E", |
|
borderRadius: 20, |
|
paddingHorizontal: 20, |
|
justifyContent: "center", |
|
}, |
|
sendButtonText: { |
|
color: "white", |
|
fontSize: 16, |
|
}, |
|
loginOverlay: { |
|
position: 'absolute', |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
backgroundColor: 'rgba(255, 255, 255, 0.9)', |
|
backdropFilter: 'blur(10px)', // iOS 毛玻璃效果 |
|
justifyContent: 'center', |
|
alignItems: 'center', |
|
zIndex: 1000, |
|
}, |
|
blurContainer: { |
|
width: '100%', |
|
height: '100%', |
|
justifyContent: 'center', |
|
alignItems: 'center', |
|
backgroundColor: Platform.OS === 'android' ? 'rgba(255, 255, 255, 0.95)' : 'transparent', |
|
}, |
|
loginPromptContainer: { |
|
backgroundColor: 'white', |
|
borderRadius: 20, |
|
padding: 40, |
|
alignItems: 'center', |
|
shadowColor: '#000', |
|
shadowOffset: { |
|
width: 0, |
|
height: 2, |
|
}, |
|
shadowOpacity: 0.25, |
|
shadowRadius: 3.84, |
|
elevation: 5, |
|
maxWidth: '80%', |
|
}, |
|
loginIcon: { |
|
width: 80, |
|
height: 80, |
|
marginBottom: 20, |
|
justifyContent: 'center', |
|
alignItems: 'center', |
|
backgroundColor: 'rgba(0, 122, 108, 0.1)', |
|
borderRadius: 40, |
|
}, |
|
loginIconText: { |
|
fontSize: 40, |
|
fontWeight: 'bold', |
|
color: '#007a6c', |
|
}, |
|
loginPromptTitle: { |
|
fontSize: 24, |
|
fontWeight: '700', |
|
color: '#333', |
|
marginBottom: 10, |
|
textAlign: 'center', |
|
}, |
|
loginPromptSubtitle: { |
|
fontSize: 16, |
|
color: '#666', |
|
marginBottom: 30, |
|
textAlign: 'center', |
|
lineHeight: 22, |
|
}, |
|
loginButton: { |
|
backgroundColor: '#007a6c', |
|
paddingHorizontal: 40, |
|
paddingVertical: 15, |
|
borderRadius: 25, |
|
minWidth: 160, |
|
}, |
|
loginButtonText: { |
|
color: 'white', |
|
fontSize: 18, |
|
fontWeight: '700', |
|
textAlign: 'center', |
|
}, |
|
disabledInput: { |
|
opacity: 0.6, |
|
}, |
|
disabledButton: { |
|
opacity: 0.6, |
|
}, |
|
});
|
|
|