/** * 消息列表页 MessageListScreen * 威友 - 消息列表 * * 【重构后】使用MessageManager架构 * - 纯渲染组件,不直接管理数据 * - 通过useMessageList hook获取数据 * - 所有状态变更通过MessageManager统一处理 * - 支持响应式双栏布局(桌面端) */ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, Pressable, Modal, RefreshControl, ActivityIndicator, Animated, Keyboard, BackHandler, Platform, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { useIsFocused } from '@react-navigation/native'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, fontSizes, shadows, borderRadius, useAppColors, type AppColors, } from '../../theme'; import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto'; import { authService } from '../../services'; import { blurActiveElement } from '../../infrastructure/platform'; import { useUserStore, useAuthStore } from '../../stores'; // 【新架构】使用MessageManager hooks(会话列表数据源自 MessageManager 游标同步) import { messageManager, useMessageListRefresh, useCreateConversation, useUnreadCount, useMarkAsRead, useMessageManagerConversations, useMessageStore, } from '../../stores'; import { Avatar, Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import * as hrefs from '../../navigation/hrefs'; // 导入 ChatScreen 组件用于桌面端双栏布局 import { ChatScreen } from './ChatScreen'; import { ConversationListRow } from './components/ConversationListRow'; // 导入 NotificationsScreen 用于在内部显示 import { NotificationsScreen } from './NotificationsScreen'; // 导入扫码组件 import { SearchBar, TabBar } from '../../components/business'; import { QRCodeScanner } from '../../components/business/QRCodeScanner'; // 系统通知会话特殊ID const SYSTEM_MESSAGE_CHANNEL_ID = '-1'; // 搜索结果类型 type SearchResultType = 'conversation' | 'user'; interface SearchResultItem { type: SearchResultType; conversation?: ConversationResponse; user?: UserDTO; matchedMessages?: MessageResponse[]; } /** * MessageListScreen - 纯渲染组件 * 所有数据通过useMessageList hook获取 * 支持响应式双栏布局 */ export const MessageListScreen: React.FC = () => { const colors = useAppColors(); const styles = useMemo(() => createMessageListStyles(colors), [colors]); const router = useRouter(); 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'); // 会话列表:MessageManager 统一游标拉取与合并,与已读/角标同源 const { conversations: conversationList, isLoading, hasMore, loadMore, refresh, } = useMessageManagerConversations(); // 使用 MessageManager 获取未读数和系统通知数 const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); const lastSystemMessageAt = useMessageStore(state => state.lastSystemMessageAt); 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); useEffect(() => { if (actionMenuVisible) { blurActiveElement(); } }, [actionMenuVisible]); 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: lastSystemMessageAt || new Date().toISOString(), }, last_message_at: lastSystemMessageAt || new Date().toISOString(), unread_count: systemUnreadCount, participants: [], created_at: lastSystemMessageAt || new Date().toISOString(), updated_at: lastSystemMessageAt || new Date().toISOString(), }), [systemUnreadCount, lastSystemMessageAt]); // 动画值 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 遮挡导致最后几项无法完整滑出 // TabBar 悬浮:高度 64 + 浮动间距 12*2 = 88 const listBottomInset = useMemo(() => { if (isWideScreen) return spacing.lg; const FLOATING_TAB_BAR_HEIGHT = 64 + 24; // TabBar高度 + 上下浮动间距 return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md; }, [isWideScreen, insets.bottom]); // 【新架构】页面获得焦点时初始化MessageManager useEffect(() => { if (isFocused) { messageManager.initialize(); } }, [isFocused]); // 【新架构】使用focus刷新hook,从ChatScreen返回时自动刷新未读数 useMessageListRefresh(); // 同步未读数到userStore(用于TabBar角标显示) useEffect(() => { setMessageUnreadCount(totalUnreadCount + systemUnreadCount); }, [totalUnreadCount, systemUnreadCount, setMessageUnreadCount]); // 屏幕失去焦点时重置搜索状态 useEffect(() => { if (!isFocused) { setIsSearchMode(false); setSearchText(''); setSearchResults([]); setActiveSearchTab('chat'); setActionMenuVisible(false); Keyboard.dismiss(); } }, [isFocused]); // 处理 Android 物理返回键 - 当显示通知页面时,返回键关闭通知页面 useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { if (showNotifications) { setShowNotifications(false); return true; // 阻止默认行为 } return false; }); return () => backHandler.remove(); }, [showNotifications]); // 下拉刷新 const onRefresh = useCallback(async () => { setRefreshing(true); await refresh(); setRefreshing(false); }, [refresh]); // 格式化时间(稳定引用,配合会话行 memo) const formatTime = useCallback((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: Platform.OS !== 'web', }), Animated.timing(scaleAnims[index], { toValue: 1, duration: 100, useNativeDriver: Platform.OS !== 'web', }), ]).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) { // 群聊 - 窄屏下导航 router.push( hrefs.hrefChat({ 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)); router.push( hrefs.hrefChat({ conversationId: String(conversation.id), userId: otherUser ? String(otherUser.id) : undefined, isGroupChat: false, }) ); } }); }; const handleConversationPressRef = useRef(handleConversationPress); handleConversationPressRef.current = handleConversationPress; // 创建群聊 const handleCreateGroup = () => { router.push(hrefs.hrefGroupCreate()); }; // 主动加群 const handleJoinGroup = () => { router.push(hrefs.hrefGroupJoin()); }; 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 conversationList) { 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); } }, [conversationList, 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} 条未读通知` : '暂无新通知'; const lastTime = lastSystemMessageAt || new Date().toISOString(); 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: lastTime, }, last_message_at: lastTime, unread_count: systemUnreadCount, participants: [], created_at: lastTime, updated_at: lastTime, }; }; // 合并列表数据:系统消息与普通会话一起按时间排序 const listData: ConversationResponse[] = [ createSystemMessageItem(), ...conversationList, ].sort((a, b) => { const aPinned = a.is_pinned ? 1 : 0; const bPinned = b.is_pinned ? 1 : 0; if (aPinned !== bPinned) return bPinned - aPinned; return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); }); const listConvRef = useRef(new Map()); listConvRef.current = new Map(listData.map((c) => [String(c.id), c])); const stableConversationPress = useCallback((conversationId: string, index: number) => { const c = listConvRef.current.get(conversationId); if (c) { handleConversationPressRef.current(c, index); } }, []); // 总未读数 const totalUnread = totalUnreadCount + systemUnreadCount; // 渲染会话项(行级 memo + 按 id 解析最新会话,避免 Tab 切换时整表无效重绘) const renderConversation = useCallback( ({ item, index }: { item: ConversationResponse; index: number }) => { const isSelected = selectedConversation?.id === item.id; return ( stableConversationPress(String(item.id), index)} /> ); }, [selectedConversation?.id, scaleAnims, formatTime, stableConversationPress] ); // 渲染空状态 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) { router.push( hrefs.hrefChat({ conversationId: String(conversation.id), userId: String(user.id), 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 = () => ( {/* 搜索头部 */} performSearch(searchText)} placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'} autoFocus /> 取消 {/* Tab切换 - 现代化风格 */} setActiveSearchTab(index === 0 ? 'chat' : 'user')} variant="modern" /> {/* 搜索结果 */} { if (item.type === 'conversation') { return `conv-${item.conversation?.id || index}`; } return `user-${item.user?.id || index}`; }} contentContainerStyle={[ styles.searchResultsContent, { paddingBottom: listBottomInset }, ]} showsVerticalScrollIndicator={false} scrollEnabled={true} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" ListEmptyComponent={renderSearchEmpty} /> ); // 渲染会话列表内容 const renderConversationList = () => ( <> 消息 {totalUnread > 0 && ( {totalUnread > 99 ? '99+' : totalUnread} )} {}} onSubmit={() => {}} placeholder="搜索" /> {isLoading && conversationList.length === 0 ? ( ) : ( String(item.id)} contentContainerStyle={[ styles.listContent, ...(isWideScreen ? [styles.listContentWide] : []), { paddingBottom: listBottomInset }, ]} showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} onEndReached={onEndReached} onEndReachedThreshold={0.5} scrollEnabled={true} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" 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)} /> ); }; function createMessageListStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, }, // 桌面端双栏布局 desktopContainer: { flex: 1, flexDirection: 'row', }, sidebar: { backgroundColor: colors.background.default, borderRightWidth: 1, borderRightColor: colors.chat.border, }, chatArea: { flex: 1, backgroundColor: colors.chat.surfaceMuted, }, chatPlaceholder: { flex: 1, backgroundColor: colors.chat.surfaceMuted, alignItems: 'center', justifyContent: 'center', }, chatPlaceholderContent: { alignItems: 'center', justifyContent: 'center', }, chatPlaceholderText: { marginTop: spacing.md, fontSize: 16, color: colors.text.hint, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, backgroundColor: colors.background.default, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.divider + '60', }, headerWide: { paddingHorizontal: spacing.lg, paddingVertical: spacing.lg, }, headerTitleWide: { fontSize: 22, }, searchContainerWide: { paddingHorizontal: spacing.lg, }, listContentWide: { paddingHorizontal: spacing.xs, }, headerLeft: { width: 44, }, headerCenter: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, }, headerTitle: { fontSize: 19, fontWeight: '700', color: colors.text.primary, }, totalBadge: { minWidth: 18, height: 18, borderRadius: 9, backgroundColor: colors.error.main, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 5, }, totalBadgeText: { color: colors.primary.contrast, fontSize: 11, fontWeight: '600', lineHeight: 18, textAlignVertical: 'center', includeFontPadding: false, }, 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: colors.background.paper, 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: colors.text.primary, }, searchContainer: { paddingHorizontal: spacing.md, paddingBottom: spacing.sm, backgroundColor: colors.background.default, }, listContent: { flexGrow: 1, backgroundColor: colors.background.default, paddingHorizontal: spacing.xs, }, 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: colors.text.hint, }, searchModeContainer: { flex: 1, backgroundColor: colors.background.paper, }, searchHeader: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: `${colors.divider}70`, paddingHorizontal: spacing.md, paddingBottom: spacing.sm, }, searchShell: { flex: 1, }, cancelButton: { paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, marginLeft: spacing.sm, }, cancelText: { fontSize: fontSizes.md, fontWeight: '600', color: colors.primary.main, }, searchTabContainer: { backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: `${colors.divider}50`, }, searchResultsContent: { flexGrow: 1, paddingHorizontal: spacing.md, paddingTop: spacing.sm, }, searchResultItem: { flexDirection: 'row', alignItems: 'center', padding: spacing.md, backgroundColor: colors.background.default, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: `${colors.divider}50`, }, searchResultContent: { flex: 1, marginLeft: spacing.md, }, searchResultHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 2, }, searchResultName: { fontSize: 15, fontWeight: '600', color: colors.text.primary, }, searchResultTime: { fontSize: 12, color: colors.text.hint, }, searchResultMessage: { fontSize: 13, color: colors.text.secondary, }, searchingText: { marginTop: spacing.sm, fontSize: 14, color: colors.text.hint, }, 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: colors.divider, }, matchedMessageText: { flex: 1, fontSize: 13, color: colors.text.secondary, lineHeight: 18, }, matchedMessageTime: { fontSize: 11, color: colors.text.hint, marginLeft: spacing.sm, minWidth: 45, }, }); }