Files
frontend/src/screens/message/PrivateChatInfoScreen.tsx
lafay 5815f1a5f7
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m49s
Frontend CI / ota-android (push) Successful in 10m29s
Frontend CI / build-android-apk (push) Successful in 40m32s
refactor(home): redesign home screen with sort bar and add recommendation feed
- Replace PagerView-based tabs with modern sort bar navigation
- Add "推荐" (recommended) feed option with "hot" sort type
- Update tab icons and labels to match new sort paradigm
- Optimize PostCard, MessageBubble conditional styles

perf(chat): migrate FlatList to FlashList for emoji/sticker panels

- Improve virtualized rendering performance for emoji and sticker grids
- Fix jump-to-latest scroll behavior for inverted FlashList

perf(profile): extract user header and tab bar into separate memoized renders

- Prevent unnecessary re-renders when switching tabs

feat(api): add channel_id filter support for post queries

- Include channel_id in cursor pagination requests
- Update CursorPaginationRequest type definition

style(chat-info): redesign group and private chat info screens with flat layout

- Remove card borders and shadows for cleaner appearance
- Adjust avatar sizes and spacing for consistency
2026-04-26 12:04:27 +08:00

521 lines
16 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 私聊聊天管理页面
* 威友 - 私聊设置界面
* 参考QQ和微信的实现提供聊天管理功能
*/
import React, { useState, useEffect, useCallback, useMemo } 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 { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores';
import { authService } from '@/services/auth';
import { messageService } from '@/services/message';
import { ApiError } from '@/services/core';
import { messageRepository, conversationRepository } from '@/database';
import { messageManager } from '../../stores/message';
import { useMessageStore } from '../../stores/message/store';
import { userManager } from '../../stores/user';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { User } from '../../types';
import * as hrefs from '../../navigation/hrefs';
import { AppBackButton } from '../../components/common';
const PrivateChatInfoScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createPrivateChatInfoStyles(colors), [colors]);
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 = useMessageStore(state => state.notificationMutedMap.get(conversationId) || 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 conversationRepository.getCache(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]);
// 切换免打扰
const toggleMute = async () => {
try {
const newValue = !isMuted;
await messageManager.toggleNotificationMuted(conversationId, newValue);
} catch (error) {
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 messageRepository.clearConversation(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 : colors.background.default}
/>
</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={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<Loading />
</SafeAreaView>
);
}
const displayUser = user || {
id: userId,
nickname: userName,
avatar: userAvatar,
};
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<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={80}
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>
);
};
function createPrivateChatInfoStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: 0,
},
pageTitle: {
fontWeight: '600',
fontSize: fontSizes.xl,
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
},
headerCard: {
backgroundColor: colors.background.paper,
paddingVertical: spacing.lg,
marginBottom: spacing.md,
},
userHeader: {
flexDirection: 'row',
alignItems: 'center',
},
userInfo: {
flex: 1,
marginLeft: spacing.lg,
},
userName: {
fontWeight: '700',
fontSize: fontSizes['2xl'],
marginBottom: spacing.xs,
},
section: {
marginTop: spacing.lg,
},
sectionTitle: {
marginBottom: spacing.sm,
marginLeft: spacing.sm,
fontWeight: '500',
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
card: {
backgroundColor: colors.background.paper,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: 0,
},
settingIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '15',
},
settingTitle: {
flex: 1,
},
dangerText: {
color: colors.error.main,
fontWeight: '600',
},
divider: {
marginLeft: 52,
width: 'auto',
marginVertical: 0,
},
deleteButton: {
marginTop: spacing.sm,
borderRadius: borderRadius.lg,
height: 52,
},
});
}
export default PrivateChatInfoScreen;