- 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.
493 lines
14 KiB
TypeScript
493 lines
14 KiB
TypeScript
/**
|
||
* 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;
|