/** * 消息列表页 MessageListScreen * 胡萝卜BBS - 消息列表 * * 【重构后】使用MessageManager架构 * - 纯渲染组件,不直接管理数据 * - 通过useMessageList hook获取数据 * - 所有状态变更通过MessageManager统一处理 * - 支持响应式双栏布局(桌面端) */ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, Pressable, Modal, RefreshControl, ActivityIndicator, Animated, TextInput, Keyboard, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation, useIsFocused } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme'; import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments, extractTextFromSegmentsAsync, MessageSegment } from '../../types/dto'; import { authService, messageService } from '../../services'; import { useUserStore, useAuthStore } from '../../stores'; // 【新架构】使用MessageManager hooks import { messageManager, useMessageListRefresh, useCreateConversation, useUnreadCount, useMarkAsRead } from '../../stores'; import { Avatar, Text, EmptyState, ResponsiveContainer } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import { RootStackParamList, MessageStackParamList } from '../../navigation/types'; import { getUserCache } from '../../services/database'; // 导入 EmbeddedChat 组件用于桌面端双栏布局 import { EmbeddedChat } from './components/EmbeddedChat'; // 导入 NotificationsScreen 用于在内部显示 import { NotificationsScreen } from './NotificationsScreen'; // 导入扫码组件 import { QRCodeScanner } from '../../components/business/QRCodeScanner'; type NavigationProp = NativeStackNavigationProp; type MessageNavProp = NativeStackNavigationProp; // 系统通知会话特殊ID const SYSTEM_MESSAGE_CHANNEL_ID = '-1'; // 搜索结果类型 type SearchResultType = 'conversation' | 'user'; interface SearchResultItem { type: SearchResultType; conversation?: ConversationResponse; user?: UserDTO; matchedMessages?: MessageResponse[]; } // 动画值 const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); const MAX_CONVERSATION_NAME_LENGTH = 10; const truncateDisplayName = (name: string, maxLength: number = MAX_CONVERSATION_NAME_LENGTH): string => { if (!name) return ''; if (name.length <= maxLength) return name; return `${name.slice(0, maxLength)}...`; }; /** * 异步消息预览组件 */ const AsyncMessagePreview: React.FC<{ segments?: MessageSegment[]; status?: string; isGroupChat?: boolean; senderName?: string; }> = ({ segments, status, isGroupChat, senderName }) => { const [displayText, setDisplayText] = useState(''); const isMountedRef = useRef(true); useEffect(() => { isMountedRef.current = true; const loadPreview = async () => { if (status === 'recalled') { if (isMountedRef.current) { setDisplayText('消息已撤回'); } return; } const initialText = extractTextFromSegments(segments); if (isMountedRef.current) { setDisplayText(initialText); } const hasAtWithoutNickname = segments?.some( s => s.type === 'at' && s.data.user_id !== 'all' && !s.data.nickname ); if (hasAtWithoutNickname) { try { const asyncText = await extractTextFromSegmentsAsync( segments, getUserCache, authService.getUserById.bind(authService) ); if (isMountedRef.current && asyncText !== initialText) { setDisplayText(asyncText); } } catch (error) { console.warn('[AsyncMessagePreview] 获取用户名失败:', error); } } }; loadPreview(); return () => { isMountedRef.current = false; }; }, [segments, status]); if (!displayText) return null; if (isGroupChat && senderName) { return <>{senderName}: {displayText}; } return <>{displayText}; }; /** * MessageListScreen - 纯渲染组件 * 所有数据通过useMessageList hook获取 * 支持响应式双栏布局 */ export const MessageListScreen: React.FC = () => { const navigation = useNavigation(); const isFocused = useIsFocused(); const insets = useSafeAreaInsets(); // 在大屏幕模式下不使用 Bottom Tab Navigator,所以不需要 tabBarHeight const { isMobile } = useResponsive(); let tabBarHeight = 0; try { tabBarHeight = useBottomTabBarHeight(); } catch (e) { // 大屏幕模式下不在 Bottom Tab Navigator 中,会抛出错误,忽略即可 tabBarHeight = 0; } const currentUserId = useAuthStore(state => state.currentUser?.id); const setMessageUnreadCount = useUserStore(state => state.setMessageUnreadCount); // 响应式布局 const { isDesktop, isTablet, width } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); // 【游标分页】使用 useCursorPagination hook 获取会话列表 const { items: conversations, isLoading, isRefreshing, hasMore, loadMore, refresh, error: paginationError, } = useCursorPagination( async ({ cursor, pageSize }) => { return await messageService.getConversationsCursor({ cursor, page_size: pageSize, }); }, { pageSize: 20 } ); // 使用 MessageManager 获取未读数和系统通知数 const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); const { markAllAsRead, isMarking } = useMarkAsRead(null); // 【新架构】使用MessageManager的hook创建会话 const { createConversation } = useCreateConversation(); // 本地刷新状态(仅用于下拉刷新的UI显示) const [refreshing, setRefreshing] = useState(false); const [loadingMore, setLoadingMore] = useState(false); // 上拉加载更多 const onEndReached = useCallback(async () => { if (isLoading || loadingMore || !hasMore) return; setLoadingMore(true); await loadMore(); setLoadingMore(false); }, [isLoading, loadingMore, hasMore, loadMore]); // 搜索相关状态 const [isSearchMode, setIsSearchMode] = useState(false); const [searchText, setSearchText] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [activeSearchTab, setActiveSearchTab] = useState<'chat' | 'user'>('chat'); const [actionMenuVisible, setActionMenuVisible] = useState(false); const [scannerVisible, setScannerVisible] = useState(false); // 系统通知显示状态 - 用于在移动端显示通知页面 const [showNotifications, setShowNotifications] = useState(false); // 系统消息会话对象(用于选中状态) const systemMessageConversation: ConversationResponse = useMemo(() => ({ id: SYSTEM_MESSAGE_CHANNEL_ID, type: 'private', last_seq: 0, last_message: { id: '0', conversation_id: SYSTEM_MESSAGE_CHANNEL_ID, sender_id: 'system', seq: 0, segments: [{ type: 'text', data: { text: '系统通知' } }], status: 'normal', created_at: new Date().toISOString(), }, last_message_at: new Date().toISOString(), unread_count: systemUnreadCount, participants: [], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }), [systemUnreadCount]); // 动画值 const [scaleAnims] = useState(() => Array.from({ length: 20 }, () => new Animated.Value(1)) ); // 【桌面端双栏布局】选中会话的状态 const [selectedConversation, setSelectedConversation] = useState(null); // 计算侧边栏宽度(桌面端) const sidebarWidth = useMemo(() => { if (width >= 1440) return 380; if (width >= 1280) return 340; if (width >= 1024) return 320; return 280; }, [width]); // 给底部列表预留安全空间,避免被 TabBar 遮挡导致最后几项无法完整滑出 const listBottomInset = useMemo(() => { if (isWideScreen) return spacing.lg; return tabBarHeight + insets.bottom + spacing.md; }, [isWideScreen, tabBarHeight, insets.bottom]); // 【新架构】页面获得焦点时初始化MessageManager useEffect(() => { if (isFocused) { messageManager.initialize(); } }, [isFocused]); // 【新架构】使用focus刷新hook,从ChatScreen返回时自动刷新未读数 useMessageListRefresh(isFocused); // 同步未读数到userStore(用于TabBar角标显示) useEffect(() => { setMessageUnreadCount(totalUnreadCount + systemUnreadCount); }, [totalUnreadCount, systemUnreadCount, setMessageUnreadCount]); // 屏幕失去焦点时重置搜索状态 useEffect(() => { if (!isFocused) { setIsSearchMode(false); setSearchText(''); setSearchResults([]); setActiveSearchTab('chat'); setActionMenuVisible(false); Keyboard.dismiss(); } }, [isFocused]); // 下拉刷新 const onRefresh = useCallback(async () => { setRefreshing(true); await refresh(); setRefreshing(false); }, [refresh]); // 格式化时间 const formatTime = (dateString: string): string => { try { const date = new Date(dateString); const now = new Date(); const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); if (diffInHours < 24 && date.getDate() === now.getDate()) { return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); if (date.getDate() === yesterday.getDate()) { return '昨天'; } if (diffInHours < 24 * 7) { const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; return days[date.getDay()]; } return `${date.getMonth() + 1}/${date.getDate()}`; } catch { return ''; } }; // 跳转到聊天页 const handleConversationPress = (conversation: ConversationResponse, index: number) => { Animated.sequence([ Animated.timing(scaleAnims[index], { toValue: 0.98, duration: 100, useNativeDriver: true, }), Animated.timing(scaleAnims[index], { toValue: 1, duration: 100, useNativeDriver: true, }), ]).start(() => { if (conversation.id === SYSTEM_MESSAGE_CHANNEL_ID) { // 系统通知 - 根据屏幕宽度决定如何显示 if (isWideScreen) { // 宽屏下使用内部状态显示通知页面,同时设置系统消息为选中状态 setSelectedConversation(systemMessageConversation); setShowNotifications(true); } else { // 窄屏下也使用内部状态显示通知页面 setShowNotifications(true); } } else if (isWideScreen) { // 【桌面端双栏布局】宽屏下设置选中会话,在右侧显示聊天 // 同时关闭系统通知页面 setShowNotifications(false); setSelectedConversation(conversation); } else if (conversation.type === 'group' && conversation.group) { // 群聊 - 窄屏下导航 (navigation as any).navigate('Chat', { conversationId: String(conversation.id), groupId: String(conversation.group.id), groupName: conversation.group.name, isGroupChat: true, }); } else { // 私聊 - 窄屏下导航 const currentUserId = useAuthStore.getState().currentUser?.id; const otherUser = conversation.participants?.find(p => String(p.id) !== String(currentUserId)); (navigation as any).navigate('Chat', { conversationId: String(conversation.id), userId: otherUser ? String(otherUser.id) : undefined, userName: otherUser?.nickname || otherUser?.username, isGroupChat: false, }); } }); }; // 创建群聊 const handleCreateGroup = () => { (navigation as any).navigate('CreateGroup'); }; // 主动加群 const handleJoinGroup = () => { (navigation as any).navigate('JoinGroup'); }; const handleOpenActionMenu = () => { setActionMenuVisible(true); }; const runMenuAction = (action: 'join' | 'create' | 'readAll' | 'scanQRCode') => { setActionMenuVisible(false); if (action === 'join') { handleJoinGroup(); return; } if (action === 'create') { handleCreateGroup(); return; } if (action === 'scanQRCode') { setScannerVisible(true); return; } handleMarkAllRead(); }; // 标记所有消息为已读 const handleMarkAllRead = async () => { try { await markAllAsRead(); setMessageUnreadCount(0); } catch (error) { console.error('标记已读失败:', error); } }; // 进入搜索模式 const handleSearch = () => { setIsSearchMode(true); setSearchText(''); setSearchResults([]); }; // 退出搜索模式 const handleCloseSearch = () => { setIsSearchMode(false); setSearchText(''); setSearchResults([]); Keyboard.dismiss(); }; // 执行搜索 const performSearch = useCallback(async (keyword: string) => { if (!keyword.trim()) { setSearchResults([]); return; } const trimmedKeyword = keyword.trim().toLowerCase(); setIsSearching(true); try { if (activeSearchTab === 'chat') { const results: SearchResultItem[] = []; for (const conv of conversations) { await messageManager.fetchMessages(String(conv.id)); const messages = messageManager.getMessages(String(conv.id)); const matchedMessages = messages.filter(msg => { const text = extractTextFromSegments(msg.segments); return text.toLowerCase().includes(trimmedKeyword); }); if (matchedMessages.length > 0) { results.push({ type: 'conversation', conversation: conv, matchedMessages: matchedMessages, }); } } setSearchResults(results); } else { const usersData = await authService.searchUsers(trimmedKeyword, 1, 20); const results: SearchResultItem[] = (usersData.list || []).map(user => ({ type: 'user', user, })); setSearchResults(results); } } catch (error) { console.error('搜索失败:', error); } finally { setIsSearching(false); } }, [conversations, activeSearchTab]); // 搜索文本变化时触发搜索 useEffect(() => { const timer = setTimeout(() => { if (searchText.trim()) { performSearch(searchText); } else { setSearchResults([]); } }, 300); return () => clearTimeout(timer); }, [searchText, performSearch]); // 切换搜索Tab时重新搜索 useEffect(() => { if (searchText.trim()) { performSearch(searchText); } }, [activeSearchTab, searchText, performSearch]); // 创建系统通知会话项 const createSystemMessageItem = (): ConversationResponse => { const noticeContent = systemUnreadCount > 0 ? `您有 ${systemUnreadCount} 条未读通知` : '暂无新通知'; return { id: SYSTEM_MESSAGE_CHANNEL_ID, type: 'private', last_seq: 0, last_message: { id: '0', conversation_id: SYSTEM_MESSAGE_CHANNEL_ID, sender_id: 'system', seq: 0, segments: [{ type: 'text', data: { text: noticeContent } }], status: 'normal', created_at: new Date().toISOString(), }, last_message_at: new Date().toISOString(), unread_count: systemUnreadCount, participants: [], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; }; // 合并列表数据 const listData: ConversationResponse[] = [ createSystemMessageItem(), ...conversations, ]; // 总未读数 const totalUnread = totalUnreadCount + systemUnreadCount; // 渲染会话项 const renderConversation = ({ item, index }: { item: ConversationResponse; index: number }) => { const isSystemChannel = item.id === SYSTEM_MESSAGE_CHANNEL_ID; const isGroupChat = !!(item.type === 'group' && item.group); // 获取显示名称和头像 let displayNameRaw: string; let displayName: string; let displayAvatar: string | null = null; if (isSystemChannel) { displayNameRaw = '系统通知'; } else if (isGroupChat && item.group) { displayNameRaw = item.group.name || '群聊'; displayAvatar = item.group.avatar && item.group.avatar.trim() !== '' ? item.group.avatar : null; } else { displayNameRaw = item.participants?.[0]?.nickname || '未知用户'; displayAvatar = item.participants?.[0]?.avatar || null; } displayName = truncateDisplayName(displayNameRaw); const getSenderName = (): string | undefined => { if (isGroupChat && item.last_message?.sender) { return item.last_message.sender.nickname || item.last_message.sender.username; } return undefined; }; // 是否选中(用于桌面端双栏布局高亮) const isSelected = selectedConversation?.id === item.id; return ( handleConversationPress(item, index)} activeOpacity={0.7} > {isSystemChannel ? ( ) : isGroupChat ? ( {displayAvatar ? ( ) : ( )} ) : ( )} {isSystemChannel && ( 官方 )} {isGroupChat && ( )} {displayName} {item.is_pinned && !isSystemChannel && ( )} {isGroupChat && item.member_count !== undefined && ( ({item.member_count}) )} {formatTime(item.last_message_at || item.updated_at)} 0 ? [styles.messageText, styles.unreadMessageText] : styles.messageText} numberOfLines={1} > {!item.last_message ? ( '暂无消息' ) : ( )} {item.unread_count > 0 && ( {item.unread_count > 99 ? '99+' : item.unread_count} )} {__DEV__ && ( [DEBUG: {item.unread_count || 0}] )} ); }; // 渲染空状态 const renderEmpty = () => ( ); // 高亮搜索关键词 const highlightKeyword = (text: string, keyword: string): React.ReactNode => { if (!text || !keyword) return text; const lowerText = text.toLowerCase(); const lowerKeyword = keyword.toLowerCase(); const index = lowerText.indexOf(lowerKeyword); if (index === -1) return text; return ( {text.substring(0, index)} {text.substring(index, index + keyword.length)} {text.substring(index + keyword.length)} ); }; // 渲染搜索结果项 const renderSearchResult = ({ item }: { item: SearchResultItem }) => { if (item.type === 'conversation' && item.conversation) { const conv = item.conversation; const otherUser = conv.participants?.find( p => String(p.id) !== String(currentUserId) ); const matchedMessages = item.matchedMessages || []; return ( handleConversationPress(conv, 0)} > {otherUser?.nickname || otherUser?.username || '未知用户'} {matchedMessages.length > 0 && ( {matchedMessages.length} 条相关消息 )} {matchedMessages.map((msg, idx) => ( {highlightKeyword(extractTextFromSegments(msg.segments), searchText)} {formatTime(msg.created_at)} ))} ); } if (item.type === 'user' && item.user) { const user = item.user; return ( { try { const conversation = await createConversation(String(user.id)); if (conversation) { (navigation as any).navigate('Chat', { conversationId: String(conversation.id), userId: String(user.id), userName: user.nickname || user.username, isGroupChat: false, }); } } catch (error) { console.error('创建会话失败:', error); } }} > {highlightKeyword(user.nickname, searchText)} {highlightKeyword(`@${user.username}`, searchText)} {user.bio ? ` · ${highlightKeyword(user.bio, searchText)}` : ''} {user.is_following && ( )} {!user.is_following && ( )} ); } return null; }; // 渲染搜索空状态 const renderSearchEmpty = () => { if (isSearching) { return ( 搜索中... ); } if (searchText.trim() && searchResults.length === 0) { return ( ); } if (!searchText.trim()) { return ( ); } return null; }; // 渲染搜索模式UI const renderSearchMode = () => ( {searchText.length > 0 && ( setSearchText('')}> )} setActiveSearchTab('chat')} > 聊天记录 setActiveSearchTab('user')} > 用户 { if (item.type === 'conversation') { return `conv-${item.conversation?.id || index}`; } return `user-${item.user?.id || index}`; }} contentContainerStyle={[styles.searchResultsContent, { paddingBottom: listBottomInset }]} showsVerticalScrollIndicator={false} ListEmptyComponent={renderSearchEmpty} /> ); // 渲染会话列表内容 const renderConversationList = () => ( <> 消息 {totalUnread > 0 && ( {totalUnread > 99 ? '99+' : totalUnread} )} 搜索 {isLoading && conversations.length === 0 ? ( ) : ( String(item.id)} contentContainerStyle={[ styles.listContent, ...(isWideScreen ? [styles.listContentWide] : []), { paddingBottom: listBottomInset }, ]} showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} onEndReached={onEndReached} onEndReachedThreshold={0.5} refreshControl={ } ListFooterComponent={ loadingMore ? ( 加载中... ) : null } /> )} ); const renderActionMenu = () => ( setActionMenuVisible(false)}> setActionMenuVisible(false)}> runMenuAction('scanQRCode')}> 扫码登录 runMenuAction('join')}> 加入群聊 runMenuAction('create')}> 创建群聊 runMenuAction('readAll')} disabled={isMarking}> {isMarking ? '处理中...' : '全部已读'} ); // 桌面端双栏布局 if (isWideScreen && !isSearchMode) { return ( {/* 左侧会话列表 */} {renderConversationList()} {/* 右侧内容区域 - 使用 flex:1 填充剩余空间 */} {showNotifications ? ( // 显示系统通知页面 setShowNotifications(false)} /> ) : selectedConversation ? ( // 显示选中会话的聊天内容 setSelectedConversation(null)} /> ) : ( // 默认占位符 选择一个会话开始聊天 )} {renderActionMenu()} ); } return ( {showNotifications ? ( // 显示系统通知页面,传入 onBack 回调 setShowNotifications(false)} /> ) : isSearchMode ? ( renderSearchMode() ) : isWideScreen ? ( {renderConversationList()} ) : ( renderConversationList() )} {renderActionMenu()} setScannerVisible(false)} /> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#FAFAFA', }, // 桌面端双栏布局 desktopContainer: { flex: 1, flexDirection: 'row', }, sidebar: { backgroundColor: '#FAFAFA', borderRightWidth: 1, borderRightColor: '#E8E8E8', }, chatArea: { flex: 1, backgroundColor: '#F5F7FA', }, chatPlaceholder: { flex: 1, backgroundColor: '#F5F7FA', alignItems: 'center', justifyContent: 'center', }, chatPlaceholderContent: { alignItems: 'center', justifyContent: 'center', }, chatPlaceholderText: { marginTop: spacing.md, fontSize: 16, color: '#999', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, backgroundColor: '#FAFAFA', ...shadows.sm, }, headerWide: { paddingHorizontal: spacing.lg, paddingVertical: spacing.lg, }, headerTitleWide: { fontSize: 22, }, searchContainerWide: { paddingHorizontal: spacing.lg, }, listContentWide: { paddingHorizontal: spacing.lg, }, headerLeft: { width: 44, }, headerCenter: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, }, headerTitle: { fontSize: 19, fontWeight: '700', color: '#333', }, totalBadge: { minWidth: 18, height: 18, borderRadius: 9, backgroundColor: '#FF4757', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 5, }, totalBadgeText: { color: '#FFF', fontSize: 11, fontWeight: '600', }, headerRight: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', }, headerRightContainer: { flexDirection: 'row', alignItems: 'center', }, headerRightBtn: { width: 36, height: 44, alignItems: 'center', justifyContent: 'center', }, actionMenuBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.08)', }, actionMenu: { position: 'absolute', top: 88, right: spacing.md, width: 188, backgroundColor: '#FFF', borderRadius: 12, paddingVertical: spacing.sm, ...shadows.md, }, actionMenuItem: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md, paddingVertical: spacing.md, }, actionMenuText: { marginLeft: spacing.sm, fontSize: 16, color: '#333', }, searchContainer: { paddingHorizontal: spacing.md, paddingBottom: spacing.sm, backgroundColor: '#FAFAFA', }, searchBox: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F0F0F0', borderRadius: 10, paddingHorizontal: spacing.sm, paddingVertical: 10, }, searchPlaceholder: { fontSize: 14, color: '#999', marginLeft: spacing.xs, }, listContent: { flexGrow: 1, backgroundColor: '#FAFAFA', }, conversationItem: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md, paddingVertical: 14, backgroundColor: '#FFF', marginHorizontal: spacing.md, marginTop: spacing.sm, borderRadius: 12, ...shadows.sm, }, conversationItemSelected: { backgroundColor: colors.primary.light + '20', borderColor: colors.primary.main, borderWidth: 1, }, avatarContainer: { position: 'relative', }, systemAvatar: { width: 50, height: 50, borderRadius: 12, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primary.main, }, conversationContent: { flex: 1, marginLeft: spacing.md, }, conversationHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, }, nameRow: { flexDirection: 'row', alignItems: 'center', }, officialBadge: { backgroundColor: colors.primary.main, borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, marginRight: spacing.xs, }, officialBadgeText: { color: '#FFF', fontSize: 10, fontWeight: '600', }, userName: { fontWeight: '600', color: '#333', fontSize: 16, }, groupIcon: { marginRight: 4, }, memberCount: { fontSize: 12, color: '#999', marginLeft: 2, }, pinnedIcon: { marginLeft: 6, }, groupAvatar: { width: 50, height: 50, }, groupAvatarPlaceholder: { width: 50, height: 50, borderRadius: 12, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, timeText: { color: '#999', fontSize: 12, }, messageRow: { flexDirection: 'row', alignItems: 'center', }, messageText: { flex: 1, color: '#888', fontSize: 14, }, unreadMessageText: { color: '#333', fontWeight: '500', }, unreadBadge: { minWidth: 20, height: 20, borderRadius: 10, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginLeft: spacing.sm, paddingHorizontal: 6, }, unreadBadgeText: { color: '#FFF', fontSize: 12, fontWeight: '600', }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: spacing.xl * 2, }, loadingMoreContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.md, }, loadingMoreText: { marginLeft: spacing.sm, fontSize: 14, color: '#999', }, searchModeContainer: { flex: 1, backgroundColor: '#FAFAFA', }, searchInputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F0F0F0', borderRadius: 10, paddingHorizontal: spacing.md, marginHorizontal: spacing.md, marginTop: spacing.md, height: 40, }, searchInput: { flex: 1, fontSize: 15, color: '#333', marginLeft: spacing.md, }, searchTabs: { flexDirection: 'row', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, gap: spacing.sm, }, searchTab: { flex: 1, paddingVertical: spacing.sm, alignItems: 'center', backgroundColor: '#F0F0F0', borderRadius: 8, }, searchTabActive: { backgroundColor: colors.primary.main, }, searchTabText: { fontSize: 14, color: '#666', fontWeight: '500', }, searchTabTextActive: { color: '#FFF', }, searchResultsContent: { flexGrow: 1, paddingHorizontal: spacing.md, }, searchResultItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, backgroundColor: '#FFF', borderRadius: 10, marginBottom: spacing.sm, paddingHorizontal: spacing.md, }, searchResultContent: { flex: 1, marginLeft: spacing.md, }, searchResultHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 2, }, searchResultName: { fontSize: 15, fontWeight: '600', color: '#333', }, searchResultTime: { fontSize: 12, color: '#999', }, searchResultMessage: { fontSize: 13, color: '#888', }, searchingText: { marginTop: spacing.sm, fontSize: 14, color: '#999', }, followingBadge: { width: 24, height: 24, borderRadius: 12, backgroundColor: colors.primary.light + '30', alignItems: 'center', justifyContent: 'center', }, highlightText: { color: colors.primary.main, fontWeight: '700', backgroundColor: colors.primary.light + '30', }, matchedMessagesContainer: { marginTop: spacing.sm, }, matchedMessageItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', paddingVertical: spacing.xs, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#F0F0F0', }, matchedMessageText: { flex: 1, fontSize: 13, color: '#555', lineHeight: 18, }, matchedMessageTime: { fontSize: 11, color: '#AAA', marginLeft: spacing.sm, minWidth: 45, }, });