Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
490
src/screens/message/PrivateChatInfoScreen.tsx
Normal file
490
src/screens/message/PrivateChatInfoScreen.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* PrivateChatInfoScreen 私聊聊天管理页面
|
||||
* 胡萝卜BBS - 私聊设置界面
|
||||
* 参考QQ和微信的实现,提供聊天管理功能
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { authService } from '../../services/authService';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { ApiError } from '../../services/api';
|
||||
import {
|
||||
clearConversationMessages,
|
||||
getConversationCache,
|
||||
} from '../../services/database';
|
||||
import { messageManager } from '../../stores/messageManager';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
|
||||
import { User } from '../../types';
|
||||
import { RootStackParamList, MessageStackParamList } from '../../navigation/types';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<MessageStackParamList>;
|
||||
type PrivateChatInfoRouteProp = RouteProp<MessageStackParamList, 'PrivateChatInfo'>;
|
||||
|
||||
const PrivateChatInfoScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const route = useRoute<PrivateChatInfoRouteProp>();
|
||||
const { conversationId, userId, userName, userAvatar } = route.params;
|
||||
const { currentUser } = useAuthStore();
|
||||
|
||||
// 用户信息状态
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isBlocked, setIsBlocked] = useState(false);
|
||||
|
||||
// 聊天设置状态
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [isPinned, setIsPinned] = useState(false);
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const userData = await userManager.getUserById(userId);
|
||||
setUser(userData || null);
|
||||
const blockStatus = await authService.getBlockStatus(userId);
|
||||
setIsBlocked(blockStatus);
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
// 加载聊天设置
|
||||
const loadChatSettings = useCallback(async () => {
|
||||
try {
|
||||
const currentConv = messageManager.getConversation(conversationId);
|
||||
if (currentConv) {
|
||||
setIsPinned(Boolean(currentConv.is_pinned));
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = await getConversationCache(conversationId);
|
||||
if (cached) {
|
||||
setIsPinned(Boolean((cached as any).is_pinned));
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = await messageService.getConversationById(conversationId);
|
||||
setIsPinned(Boolean(detail.is_pinned));
|
||||
} catch (error) {
|
||||
console.error('加载聊天设置失败:', error);
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadUserInfo();
|
||||
loadChatSettings();
|
||||
}, [loadUserInfo, loadChatSettings]);
|
||||
|
||||
// 切换免打扰(仅本地状态,实际需要API支持)
|
||||
const toggleMute = async () => {
|
||||
try {
|
||||
const newValue = !isMuted;
|
||||
setIsMuted(newValue);
|
||||
// TODO: 调用API更新免打扰状态
|
||||
// await conversationService.updateConversationMute(conversationId, newValue);
|
||||
} catch (error) {
|
||||
setIsMuted(!isMuted);
|
||||
Alert.alert('错误', '设置免打扰失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 切换置顶
|
||||
const togglePin = async () => {
|
||||
const nextValue = !isPinned;
|
||||
setIsPinned(nextValue);
|
||||
try {
|
||||
await messageService.setConversationPinned(conversationId, nextValue);
|
||||
messageManager.updateConversation(conversationId, { is_pinned: nextValue });
|
||||
} catch (error) {
|
||||
setIsPinned(!nextValue);
|
||||
Alert.alert('错误', '设置置顶失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 清空聊天记录
|
||||
const handleClearHistory = () => {
|
||||
Alert.alert(
|
||||
'清空聊天记录',
|
||||
'确定要清空与该用户的所有聊天记录吗?此操作不可恢复。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '清空',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearConversationMessages(conversationId);
|
||||
Alert.alert('成功', '聊天记录已清空');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '清空聊天记录失败');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// 查找聊天记录
|
||||
const handleSearchMessages = () => {
|
||||
// TODO: 实现聊天记录搜索功能
|
||||
Alert.alert('提示', '聊天记录搜索功能开发中');
|
||||
};
|
||||
|
||||
// 查看用户资料
|
||||
const handleViewProfile = () => {
|
||||
// 使用根导航跳转到用户资料页面
|
||||
const rootNavigation = navigation.getParent();
|
||||
rootNavigation?.navigate('UserProfile' as any, { userId });
|
||||
};
|
||||
|
||||
// 举报/投诉
|
||||
const handleReport = () => {
|
||||
Alert.alert(
|
||||
'举报用户',
|
||||
'请选择举报原因',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: '骚扰信息', onPress: () => submitReport('harassment') },
|
||||
{ text: '欺诈行为', onPress: () => submitReport('fraud') },
|
||||
{ text: '不当内容', onPress: () => submitReport('inappropriate') },
|
||||
{ text: '其他原因', onPress: () => submitReport('other') },
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const submitReport = async (reason: string) => {
|
||||
try {
|
||||
// TODO: 实现举报API
|
||||
Alert.alert('举报已提交', '我们会尽快处理您的举报,感谢您的反馈');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '举报提交失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlockUser = () => {
|
||||
if (isBlocked) {
|
||||
Alert.alert(
|
||||
'取消拉黑',
|
||||
'确定取消拉黑该用户吗?',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '确定',
|
||||
onPress: async () => {
|
||||
const ok = await authService.unblockUser(userId);
|
||||
if (!ok) {
|
||||
Alert.alert('错误', '取消拉黑失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
setIsBlocked(false);
|
||||
Alert.alert('成功', '已取消拉黑');
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'拉黑用户',
|
||||
'拉黑后,对方将无法给你发送私聊消息,且你们会互相移除关注关系。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '确认拉黑',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const ok = await authService.blockUser(userId);
|
||||
if (!ok) {
|
||||
Alert.alert('错误', '拉黑失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
setIsBlocked(true);
|
||||
messageManager.removeConversation(conversationId);
|
||||
navigation.navigate('MessageList' as any);
|
||||
Alert.alert('已拉黑', '你已成功拉黑该用户');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '拉黑失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// 删除并退出聊天
|
||||
const handleDeleteAndExit = () => {
|
||||
Alert.alert(
|
||||
'删除聊天',
|
||||
'确定要删除与该用户的聊天吗?聊天记录将被清空。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await messageService.deleteConversationForSelf(conversationId);
|
||||
messageManager.removeConversation(conversationId);
|
||||
// 返回消息列表
|
||||
navigation.navigate('MessageList' as any);
|
||||
} catch (error) {
|
||||
const msg = error instanceof ApiError ? error.message : '删除聊天失败';
|
||||
Alert.alert('错误', msg);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染设置项(带开关)
|
||||
const renderSwitchItem = (
|
||||
icon: string,
|
||||
title: string,
|
||||
value: boolean,
|
||||
onValueChange: () => void
|
||||
) => (
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingIconContainer}>
|
||||
<MaterialCommunityIcons name={icon as any} size={20} color={colors.primary.main} />
|
||||
</View>
|
||||
<Text variant="body" style={styles.settingTitle}>
|
||||
{title}
|
||||
</Text>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: '#E0E0E0', true: colors.primary.light }}
|
||||
thumbColor={value ? colors.primary.main : '#F5F5F5'}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染设置项(带箭头)
|
||||
const renderActionItem = (
|
||||
icon: string,
|
||||
title: string,
|
||||
onPress: () => void,
|
||||
danger: boolean = false
|
||||
) => (
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.settingIconContainer, danger && styles.settingIconDanger]}>
|
||||
<MaterialCommunityIcons
|
||||
name={icon as any}
|
||||
size={20}
|
||||
color={danger ? colors.error.main : colors.primary.main}
|
||||
/>
|
||||
</View>
|
||||
<Text variant="body" style={danger ? [styles.settingTitle, styles.dangerText] : styles.settingTitle}>
|
||||
{title}
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="chevron-right" size={22} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<Loading />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const displayUser = user || {
|
||||
id: userId,
|
||||
nickname: userName,
|
||||
avatar: userAvatar,
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* 用户基本信息卡片 */}
|
||||
<View style={styles.headerCard}>
|
||||
<TouchableOpacity
|
||||
style={styles.userHeader}
|
||||
onPress={handleViewProfile}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Avatar
|
||||
source={displayUser.avatar}
|
||||
size={90}
|
||||
name={displayUser.nickname || ''}
|
||||
/>
|
||||
<View style={styles.userInfo}>
|
||||
<Text variant="h3" style={styles.userName} numberOfLines={1}>
|
||||
{displayUser.nickname || userName || '用户'}
|
||||
</Text>
|
||||
{(displayUser as any).username && (
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
@{(displayUser as any).username}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={24} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 聊天设置 */}
|
||||
<View style={styles.section}>
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
|
||||
聊天设置
|
||||
</Text>
|
||||
<View style={styles.card}>
|
||||
{renderSwitchItem('bell-off-outline', '消息免打扰', isMuted, toggleMute)}
|
||||
<Divider style={styles.divider} />
|
||||
{renderSwitchItem('pin-outline', '置顶聊天', isPinned, togglePin)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 聊天记录管理 */}
|
||||
<View style={styles.section}>
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
|
||||
聊天记录
|
||||
</Text>
|
||||
<View style={styles.card}>
|
||||
{renderActionItem('magnify', '查找聊天记录', handleSearchMessages)}
|
||||
<Divider style={styles.divider} />
|
||||
{renderActionItem('delete-sweep-outline', '清空聊天记录', handleClearHistory, true)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 其他操作 */}
|
||||
<View style={styles.section}>
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
|
||||
其他
|
||||
</Text>
|
||||
<View style={styles.card}>
|
||||
{renderActionItem('shield-alert-outline', '投诉', handleReport, true)}
|
||||
<Divider style={styles.divider} />
|
||||
{renderActionItem(
|
||||
isBlocked ? 'account-check-outline' : 'account-cancel-outline',
|
||||
isBlocked ? '取消拉黑' : '拉黑用户',
|
||||
handleBlockUser,
|
||||
true
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 删除聊天按钮 */}
|
||||
<View style={styles.section}>
|
||||
<Button
|
||||
title="删除并退出聊天"
|
||||
onPress={handleDeleteAndExit}
|
||||
variant="danger"
|
||||
style={styles.deleteButton}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
headerCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.sm,
|
||||
},
|
||||
userHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
section: {
|
||||
marginTop: spacing.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
marginLeft: spacing.sm,
|
||||
fontWeight: '500',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
...shadows.sm,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
settingIconContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
settingIconDanger: {
|
||||
backgroundColor: colors.error.light + '20',
|
||||
},
|
||||
settingTitle: {
|
||||
flex: 1,
|
||||
},
|
||||
dangerText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
divider: {
|
||||
marginLeft: spacing.xl + 36,
|
||||
width: 'auto',
|
||||
marginVertical: spacing.xs,
|
||||
},
|
||||
deleteButton: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
|
||||
export default PrivateChatInfoScreen;
|
||||
Reference in New Issue
Block a user