Files
frontend/src/screens/message/PrivateChatInfoScreen.tsx

493 lines
14 KiB
TypeScript
Raw Normal View History

/**
* 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 { useRouter, useLocalSearchParams } from 'expo-router';
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 * as hrefs from '../../navigation/hrefs';
const PrivateChatInfoScreen: React.FC = () => {
const router = useRouter();
const raw = useLocalSearchParams<{
conversationId?: string;
userId?: string;
userName?: string;
userAvatar?: string;
}>();
const conversationId = raw.conversationId ?? '';
const userId = raw.userId ?? '';
const userName = raw.userName;
const userAvatar = raw.userAvatar;
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 = () => {
router.push(hrefs.hrefUserProfile(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);
router.replace(hrefs.hrefMessages());
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);
// 返回消息列表
router.replace(hrefs.hrefMessages());
} 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} edges={['bottom']}>
<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;