Files
frontend/src/screens/message/PrivateChatInfoScreen.tsx
lafay 2ddb9cadd8
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m27s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h16m45s
refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- Updated App entry point to utilize Expo Router, simplifying the navigation setup.
- Removed legacy navigation components and services to streamline the codebase.
- Adjusted package.json to reflect the new entry point and updated dependencies for compatibility.
- Enhanced overall application structure by consolidating navigation logic and improving maintainability.
2026-03-24 14:21:31 +08:00

493 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;