/** * 通知页 NotificationsScreen * 胡萝卜BBS - 系统消息列表 * 使用新的系统消息API * 支持响应式布局 */ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, RefreshControl, ActivityIndicator, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { colors, spacing } from '../../theme'; import { SystemMessageResponse } from '../../types/dto'; import { messageService } from '../../services/messageService'; import { commentService } from '../../services/commentService'; import { SystemMessageItem } from '../../components/business'; import { Text, EmptyState, ResponsiveContainer } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { RootStackParamList } from '../../navigation/types'; import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores'; const MESSAGE_TYPES = [ { key: 'all', title: '全部' }, { key: 'like_post', title: '点赞' }, { key: 'comment', title: '评论' }, { key: 'follow', title: '关注' }, { key: 'group', title: '群通知' }, { key: 'system', title: '系统' }, { key: 'announcement', title: '公告' }, ]; const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']); const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']); export const NotificationsScreen: React.FC = () => { const isFocused = useIsFocused(); const fetchMessageUnreadCount = useUserStore(state => state.fetchMessageUnreadCount); const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); const navigation = useNavigation>(); // 响应式布局 const { isDesktop, isTablet } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); const [messages, setMessages] = useState([]); const [activeType, setActiveType] = useState('all'); const [refreshing, setRefreshing] = useState(false); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [unreadCount, setUnreadCount] = useState(0); // 同一 flag 只要有人审批过,就将待处理消息同步展示为已处理状态 const displayMessages = useMemo(() => { const reviewedByFlag = new Map(); messages.forEach((msg) => { if (msg.system_type !== 'group_join_apply') return; const flag = msg.extra_data?.flag; const status = msg.extra_data?.request_status; if (!flag) return; if (status === 'accepted' || status === 'rejected') { reviewedByFlag.set(flag, msg); } }); if (reviewedByFlag.size === 0) { return messages; } return messages.map((msg) => { if (msg.system_type !== 'group_join_apply') return msg; const flag = msg.extra_data?.flag; if (!flag) return msg; const reviewedMsg = reviewedByFlag.get(flag); if (!reviewedMsg) return msg; const reviewedStatus = reviewedMsg.extra_data?.request_status; if (reviewedStatus !== 'accepted' && reviewedStatus !== 'rejected') return msg; return { ...msg, extra_data: { ...msg.extra_data, request_status: reviewedStatus, actor_name: reviewedMsg.extra_data?.actor_name || reviewedMsg.extra_data?.operator_name || msg.extra_data?.actor_name, avatar_url: reviewedMsg.extra_data?.avatar_url || reviewedMsg.extra_data?.operator_avatar || msg.extra_data?.avatar_url, }, }; }); }, [messages]); // 获取系统消息数据 const fetchMessages = useCallback(async () => { try { setLoading(true); const response = await messageService.getSystemMessages(50, 1); // 添加防御性检查,确保 messages 数组存在 setMessages(response.messages || []); setHasMore(response.has_more ?? false); } catch (error) { console.error('获取系统消息失败:', error); } finally { setLoading(false); } }, []); // 获取未读数 const fetchUnreadCount = useCallback(async () => { try { const response = await messageService.getSystemUnreadCount(); setUnreadCount(response.unread_count); } catch (error) { console.error('获取未读数失败:', error); } }, []); // 一键已读 const handleMarkAllRead = useCallback(async () => { try { await messageService.markAllSystemMessagesRead(); setMessages(prev => prev.map(m => ({ ...m, is_read: true }))); setUnreadCount(0); setSystemUnreadCount(0); // 同步更新全局 TabBar 红点 fetchMessageUnreadCount(); // 刷新消息列表 fetchMessages(); } catch (error) { console.error('一键已读失败:', error); } }, [fetchMessages, fetchMessageUnreadCount, setSystemUnreadCount]); // 页面加载和获得焦点时刷新,并自动标记所有消息为已读 useEffect(() => { if (isFocused) { fetchMessages(); fetchUnreadCount(); // 进入界面自动标记所有消息为已读 handleMarkAllRead(); } }, [isFocused, fetchMessages, fetchUnreadCount, handleMarkAllRead]); // 屏幕失去焦点时返回消息列表 useEffect(() => { if (!isFocused) { // 使用 setTimeout 确保在导航状态稳定后再执行 const timer = setTimeout(() => { navigation.goBack(); }, 0); return () => clearTimeout(timer); } }, [isFocused, navigation]); // 筛选消息 const filteredMessages = activeType === 'all' ? displayMessages : displayMessages.filter((m) => { if (activeType === 'like_post') { return LIKE_SYSTEM_TYPES.has(m.system_type); } if (activeType === 'group') { return GROUP_SYSTEM_TYPES.has(m.system_type); } return m.system_type === activeType; }); // 下拉刷新 const onRefresh = useCallback(async () => { setRefreshing(true); await Promise.all([fetchMessages(), fetchUnreadCount()]); setRefreshing(false); }, [fetchMessages, fetchUnreadCount]); // 加载更多 const loadMore = useCallback(async () => { if (loadingMore || !hasMore || messages.length === 0) return; try { setLoadingMore(true); // 使用时间戳或seq作为游标分页(后端使用page分页) const nextPage = Math.floor((messages.length / 20)) + 1; const response = await messageService.getSystemMessages(20, nextPage); // 添加防御性检查 const newMessages = response.messages || []; setMessages(prev => [...prev, ...newMessages]); setHasMore(response.has_more ?? false); } catch (error) { console.error('加载更多失败:', error); } finally { setLoadingMore(false); } }, [loadingMore, hasMore, messages]); // 标记单条消息已读并处理导航 const extractPostIdFromActionUrl = (actionUrl?: string): string | null => { if (!actionUrl) return null; const postPathMatch = actionUrl.match(/\/posts\/([^/?#]+)/); if (postPathMatch?.[1]) { return decodeURIComponent(postPathMatch[1]); } const query = actionUrl.split('?')[1]; if (!query) return null; const params = new URLSearchParams(query); return params.get('post') || params.get('post_id'); }; const resolvePostId = async (message: SystemMessageResponse): Promise => { const { extra_data, system_type } = message; // 后端已统一将 target_id 设为帖子ID(like_post/comment/mention/favorite_post/like_reply/like_comment) if ( extra_data?.target_id && ['like_post', 'like_comment', 'like_reply', 'comment', 'mention', 'favorite_post'].includes(system_type) ) { return extra_data.target_id; } // reply 通知:target_id 是 replyID,从 action_url 解析帖子ID const postIdFromUrl = extractPostIdFromActionUrl(extra_data?.action_url); if (postIdFromUrl) { return postIdFromUrl; } // 兜底:通过评论详情反查(仅旧数据兜底) if (extra_data?.target_id && system_type === 'reply') { const comment = await commentService.getComment(extra_data.target_id); if (comment?.post_id) { return comment.post_id; } } return null; }; const handleMessagePress = async (message: SystemMessageResponse) => { try { const messageId = String(message.id); const wasUnread = message.is_read !== true; await messageService.markSystemMessageRead(messageId); setMessages(prev => prev.map(m => (String(m.id) === messageId ? { ...m, is_read: true } : m)) ); if (wasUnread) { setUnreadCount(prev => Math.max(0, prev - 1)); decrementSystemUnreadCount(1); } // 更新本地未读数以及全局 TabBar 红点 fetchUnreadCount(); fetchMessageUnreadCount(); // 根据消息类型处理导航 const { system_type, extra_data } = message; if ( ['like_post', 'comment', 'mention', 'reply', 'like_comment', 'like_reply', 'favorite_post'].includes(system_type) ) { const postId = await resolvePostId(message); if (postId) { navigation.navigate('PostDetail', { postId }); } } else if (system_type === 'follow') { // 关注 - 跳转到用户主页 if (extra_data?.actor_id_str) { navigation.navigate('UserProfile', { userId: extra_data.actor_id_str }); } } else if (system_type === 'group_join_apply') { navigation.navigate('GroupRequestDetail', { message }); } else if (system_type === 'group_invite') { navigation.navigate('GroupInviteDetail', { message }); } // 其他类型暂不处理跳转 } catch (error) { console.error('标记已读失败:', error); } }; // 处理头像点击 - 跳转到用户主页 const handleAvatarPress = (message: SystemMessageResponse) => { const { extra_data } = message; // 优先使用 actor_id_str (UUID格式),兼容 actor_id (数字格式) const actorId = extra_data?.actor_id_str || (extra_data?.actor_id ? String(extra_data.actor_id) : null); if (actorId) { navigation.navigate('UserProfile', { userId: actorId }); } }; // 渲染消息项 const renderMessage = ({ item }: { item: SystemMessageResponse }) => ( handleMessagePress(item)} onAvatarPress={() => handleAvatarPress(item)} /> ); // 渲染底部加载指示器 const renderFooter = () => { if (!loadingMore) return null; return ( 加载中... ); }; // 渲染空状态 const renderEmpty = () => ( ); return ( {isWideScreen ? ( {/* 分类筛选 */} {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length : displayMessages.filter((m) => { if (type.key === 'like_post') { return LIKE_SYSTEM_TYPES.has(m.system_type); } if (type.key === 'group') { return GROUP_SYSTEM_TYPES.has(m.system_type); } return m.system_type === type.key; }).length; return ( setActiveType(type.key)} > {type.title} {count > 0 && ( {count} )} ); })} {/* 消息列表 */} {loading ? ( ) : ( item.id.toString()} contentContainerStyle={[styles.listContent, isWideScreen && styles.listContentWide]} showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} ListFooterComponent={renderFooter} onEndReached={loadMore} onEndReachedThreshold={0.3} refreshControl={ } /> )} ) : ( <> {/* 分类筛选 */} {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length : displayMessages.filter((m) => { if (type.key === 'like_post') { return LIKE_SYSTEM_TYPES.has(m.system_type); } if (type.key === 'group') { return GROUP_SYSTEM_TYPES.has(m.system_type); } return m.system_type === type.key; }).length; return ( setActiveType(type.key)} > {type.title} {count > 0 && ( {count} )} ); })} {/* 消息列表 */} {loading ? ( ) : ( item.id.toString()} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} ListFooterComponent={renderFooter} onEndReached={loadMore} onEndReachedThreshold={0.3} refreshControl={ } /> )} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, filterContainer: { flexDirection: 'row', paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: colors.divider, }, filterContainerWide: { paddingHorizontal: spacing.xl, paddingVertical: spacing.md, justifyContent: 'center', }, filterTag: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, marginRight: spacing.sm, borderRadius: 16, backgroundColor: colors.background.default, }, filterTagWide: { paddingHorizontal: spacing.lg, paddingVertical: spacing.md, marginRight: spacing.md, }, filterTagActive: { backgroundColor: colors.primary.light + '30', }, filterCount: { marginLeft: spacing.xs, }, listContent: { flexGrow: 1, }, listContentWide: { paddingHorizontal: spacing.xl, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: spacing.xl * 2, }, loadingMore: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', paddingVertical: spacing.lg, }, loadingMoreText: { marginLeft: spacing.sm, }, });