Files
frontend/src/screens/message/MessageListScreen.tsx

1131 lines
36 KiB
TypeScript
Raw Normal View History

/**
* MessageListScreen
* -
*
* 使MessageManager架构
* -
* - useMessageList hook获取数据
* - MessageManager统一处理
* -
*/
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
Pressable,
Modal,
RefreshControl,
ActivityIndicator,
Animated,
Keyboard,
BackHandler,
Platform,
} from 'react-native';
import { FlashList, ListRenderItem } from '@shopify/flash-list';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { useIsFocused } from "expo-router/react-navigation";
import { useBottomTabBarHeight } from "expo-router/js-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';
refactor: restructure core architecture and responsive system This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase. Key changes include: - **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience. - **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`). - **Data Layer Improvements**: - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`. - Cleaned up repository implementations by removing redundant local utility functions. - Refactored `MessageMapper` to use the new centralized JSON utility. - **Type System Cleanup**: - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory. - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities. - **Utility Consolidation**: - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components. - Removed deprecated responsive utility files in favor of the new hook-based system. - **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
2026-05-05 19:07:33 +08:00
import { formatTime as formatTimeUtil } from '../../utils/formatTime';
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';
refactor: restructure core architecture and responsive system This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase. Key changes include: - **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience. - **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`). - **Data Layer Improvements**: - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`. - Cleaned up repository implementations by removing redundant local utility functions. - Refactored `MessageMapper` to use the new centralized JSON utility. - **Type System Cleanup**: - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory. - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities. - **Utility Consolidation**: - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components. - Removed deprecated responsive utility files in favor of the new hook-based system. - **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
2026-05-05 19:07:33 +08:00
import { useResponsive, useBreakpointGTE } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
// 导入 ChatScreen 组件用于桌面端双栏布局
import { ChatScreen } from './ChatScreen';
import { ConversationListRow } from './components/ConversationListRow';
2026-03-16 17:47:10 +08:00
// 导入 NotificationsScreen 用于在内部显示
import { NotificationsScreen } from './NotificationsScreen';
// 导入扫码组件
import { SearchBar, SearchHeader, 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();
2026-03-16 17:47:10 +08:00
// 在大屏幕模式下不使用 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 systemMessageTime = 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<SearchResultItem[]>([]);
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);
2026-03-16 17:47:10 +08:00
// 系统通知显示状态 - 用于在移动端显示通知页面
const [showNotifications, setShowNotifications] = useState(false);
// Stable callbacks to avoid inline arrows passed to child components
const stableCloseNotifications = useCallback(() => setShowNotifications(false), []);
const stableClearSelectedConversation = useCallback(() => setSelectedConversation(null), []);
const stableCloseScanner = useCallback(() => setScannerVisible(false), []);
2026-03-16 17:47:10 +08:00
// 系统消息会话对象(用于选中状态)
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: systemMessageTime,
2026-03-16 17:47:10 +08:00
},
last_message_at: systemMessageTime,
2026-03-16 17:47:10 +08:00
unread_count: systemUnreadCount,
participants: [],
created_at: systemMessageTime,
updated_at: systemMessageTime,
}), [systemUnreadCount, systemMessageTime]);
2026-03-16 17:47:10 +08:00
// 动画值
const [scaleAnims] = useState(() =>
Array.from({ length: 20 }, () => new Animated.Value(1))
);
// 【桌面端双栏布局】选中会话的状态
const [selectedConversation, setSelectedConversation] = useState<ConversationResponse | null>(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(() => {
messageManager.initialize();
}, []);
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 => {
refactor: restructure core architecture and responsive system This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase. Key changes include: - **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience. - **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`). - **Data Layer Improvements**: - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`. - Cleaned up repository implementations by removing redundant local utility functions. - Refactored `MessageMapper` to use the new centralized JSON utility. - **Type System Cleanup**: - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory. - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities. - **Utility Consolidation**: - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components. - Removed deprecated responsive utility files in favor of the new hook-based system. - **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
2026-05-05 19:07:33 +08:00
return formatTimeUtil(dateString);
}, []);
// 跳转到聊天页
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) {
2026-03-16 17:47:10 +08:00
// 系统通知 - 根据屏幕宽度决定如何显示
if (isWideScreen) {
2026-03-16 17:47:10 +08:00
// 宽屏下使用内部状态显示通知页面,同时设置系统消息为选中状态
setSelectedConversation(systemMessageConversation);
setShowNotifications(true);
} else {
2026-03-16 17:47:10 +08:00
// 窄屏下也使用内部状态显示通知页面
setShowNotifications(true);
}
} else if (isWideScreen) {
// 【桌面端双栏布局】宽屏下设置选中会话,在右侧显示聊天
2026-03-16 17:47:10 +08:00
// 同时关闭系统通知页面
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,
groupAvatar: conversation.group.avatar,
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} 条未读通知` : '暂无新通知';
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: systemMessageTime,
},
last_message_at: systemMessageTime,
unread_count: systemUnreadCount,
participants: [],
created_at: systemMessageTime,
updated_at: systemMessageTime,
};
};
// 合并列表数据:系统消息与普通会话一起按时间排序
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;
const aTime = new Date(a.updated_at || 0).getTime();
const bTime = new Date(b.updated_at || 0).getTime();
if (Number.isNaN(aTime) || Number.isNaN(bTime)) return 0;
return bTime - aTime;
});
const listConvRef = useRef(new Map<string, ConversationResponse>());
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<ListRenderItem<ConversationResponse>>(
({ item, index }) => {
const isSelected = selectedConversation?.id === item.id;
return (
<ConversationListRow
item={item}
index={index}
scale={scaleAnims[index] ?? 1}
isSelected={isSelected}
formatTime={formatTime}
systemChannelId={SYSTEM_MESSAGE_CHANNEL_ID}
onPress={() => stableConversationPress(String(item.id), index)}
/>
);
},
[selectedConversation?.id, scaleAnims, formatTime, stableConversationPress]
);
// 渲染空状态
const renderEmpty = () => (
<EmptyState
title="暂无消息"
description="开始聊天吧,与其他用户交流互动"
icon="message-text-outline"
/>
);
// 高亮搜索关键词
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>
<Text>{text.substring(0, index)}</Text>
<Text style={styles.highlightText}>{text.substring(index, index + keyword.length)}</Text>
<Text>{text.substring(index + keyword.length)}</Text>
</Text>
);
};
// 渲染搜索结果项
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 (
<TouchableOpacity
style={styles.searchResultItem}
onPress={() => handleConversationPress(conv, 0)}
>
<Avatar
source={otherUser?.avatar || null}
size={44}
name={otherUser?.nickname || '未知用户'}
/>
<View style={styles.searchResultContent}>
<View style={styles.searchResultHeader}>
<Text style={styles.searchResultName}>
{otherUser?.nickname || otherUser?.username || '未知用户'}
</Text>
{matchedMessages.length > 0 && (
<Text style={styles.searchResultTime}>
{matchedMessages.length}
</Text>
)}
</View>
<View style={styles.matchedMessagesContainer}>
{matchedMessages.map((msg, idx) => (
<View key={msg.id || idx} style={styles.matchedMessageItem}>
<Text style={styles.matchedMessageText} numberOfLines={2}>
{highlightKeyword(extractTextFromSegments(msg.segments), searchText)}
</Text>
<Text style={styles.matchedMessageTime}>
{formatTime(msg.created_at)}
</Text>
</View>
))}
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
</TouchableOpacity>
);
}
if (item.type === 'user' && item.user) {
const user = item.user;
return (
<TouchableOpacity
style={styles.searchResultItem}
onPress={async () => {
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);
}
}}
>
<Avatar
source={user.avatar}
size={44}
name={user.nickname}
/>
<View style={styles.searchResultContent}>
<Text style={styles.searchResultName}>
{highlightKeyword(user.nickname, searchText)}
</Text>
<Text style={styles.searchResultMessage} numberOfLines={1}>
{highlightKeyword(`@${user.username}`, searchText)}
{user.bio ? ` · ${highlightKeyword(user.bio, searchText)}` : ''}
</Text>
</View>
{user.is_following && (
<View style={styles.followingBadge}>
<MaterialCommunityIcons name="check" size={14} color={colors.primary.main} />
</View>
)}
{!user.is_following && (
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
)}
</TouchableOpacity>
);
}
return null;
};
// 渲染搜索空状态
const renderSearchEmpty = () => {
if (isSearching) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary.main} />
<Text style={styles.searchingText}>...</Text>
</View>
);
}
if (searchText.trim() && searchResults.length === 0) {
return (
<EmptyState
title={`未找到相关${activeSearchTab === 'chat' ? '聊天记录' : '用户'}`}
description="试试其他关键词吧"
icon={activeSearchTab === 'chat' ? 'message-search-outline' : 'account-search-outline'}
/>
);
}
if (!searchText.trim()) {
return (
<EmptyState
title="搜索聊天记录或用户"
description="输入关键词开始搜索"
icon="magnify"
/>
);
}
return null;
};
// 渲染搜索模式UI扁平化现代化设计
const renderSearchMode = () => (
<View style={styles.searchModeContainer}>
{/* 搜索顶栏(统一组件,内置硬件返回键拦截) */}
<SearchHeader
value={searchText}
onChangeText={setSearchText}
onSubmit={() => performSearch(searchText)}
onCancel={handleCloseSearch}
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
compact
/>
{/* Tab切换 - 与主页一致的下划线风格 */}
<View style={styles.searchTabContainer}>
<TabBar
tabs={['聊天记录', '用户']}
activeIndex={activeSearchTab === 'chat' ? 0 : 1}
onTabChange={(index) => setActiveSearchTab(index === 0 ? 'chat' : 'user')}
variant="home"
style={{
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
}}
/>
</View>
{/* 搜索结果 */}
<FlashList
data={searchResults}
renderItem={renderSearchResult}
keyExtractor={(item, index) => {
if (item.type === 'conversation') {
return `conv-${item.conversation?.id || index}`;
}
return `user-${item.user?.id || index}`;
}}
contentContainerStyle={{
paddingHorizontal: spacing.md,
paddingTop: spacing.sm,
paddingBottom: listBottomInset,
}}
showsVerticalScrollIndicator={false}
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
scrollEnabled={true}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
drawDistance={250}
ListEmptyComponent={renderSearchEmpty}
/>
</View>
);
// 渲染会话列表内容
const renderConversationList = () => (
<>
<View style={[styles.header, ...(isWideScreen ? [styles.headerWide] : [])]}>
<View style={styles.headerLeft} />
<View style={styles.headerCenter}>
<Text style={[styles.headerTitle, ...(isWideScreen ? [styles.headerTitleWide] : [])]}></Text>
{totalUnread > 0 && (
<View style={styles.totalBadge}>
<Text style={styles.totalBadgeText}>{totalUnread > 99 ? '99+' : totalUnread}</Text>
</View>
)}
</View>
<View style={styles.headerRightContainer}>
<TouchableOpacity style={styles.headerRightBtn} onPress={handleOpenActionMenu}>
<MaterialCommunityIcons name="plus" size={24} color={colors.text.secondary} />
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
style={[styles.searchContainer, ...(isWideScreen ? [styles.searchContainerWide] : [])]}
onPress={handleSearch}
activeOpacity={0.85}
>
<View pointerEvents="none">
<SearchBar
value=""
onChangeText={() => {}}
onSubmit={() => {}}
placeholder="搜索"
/>
</View>
</TouchableOpacity>
{isLoading && conversationList.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlashList
data={listData}
renderItem={renderConversation}
keyExtractor={(item) => String(item.id)}
contentContainerStyle={{
backgroundColor: colors.background.default,
paddingHorizontal: spacing.xs,
paddingBottom: listBottomInset,
}}
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
scrollEnabled={true}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
drawDistance={250}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
ListFooterComponent={
loadingMore ? (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color={colors.primary.main} />
<Text style={styles.loadingMoreText}>...</Text>
</View>
) : null
}
/>
)}
</>
);
const renderActionMenu = () => (
<Modal visible={actionMenuVisible} transparent animationType="fade" onRequestClose={() => setActionMenuVisible(false)}>
<Pressable style={styles.actionMenuBackdrop} onPress={() => setActionMenuVisible(false)}>
<View style={styles.actionMenu}>
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('scanQRCode')}>
<MaterialCommunityIcons name="qrcode-scan" size={21} color={colors.text.primary} />
<Text style={styles.actionMenuText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('join')}>
<MaterialCommunityIcons name="account-plus-outline" size={21} color={colors.text.primary} />
<Text style={styles.actionMenuText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('create')}>
<MaterialCommunityIcons name="account-multiple-plus" size={21} color={colors.text.primary} />
<Text style={styles.actionMenuText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('readAll')} disabled={isMarking}>
<MaterialCommunityIcons name="message-check-outline" size={21} color={colors.text.primary} />
<Text style={styles.actionMenuText}>{isMarking ? '处理中...' : '全部已读'}</Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
);
// 桌面端双栏布局
if (isWideScreen && !isSearchMode) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.desktopContainer}>
{/* 左侧会话列表 */}
<View style={[styles.sidebar, { width: sidebarWidth }]}>
{renderConversationList()}
</View>
{/* 右侧内容区域 - 使用 flex:1 填充剩余空间 */}
<View style={styles.chatArea}>
2026-03-16 17:47:10 +08:00
{showNotifications ? (
// 显示系统通知页面
(<NotificationsScreen onBack={stableCloseNotifications} />)
2026-03-16 17:47:10 +08:00
) : selectedConversation ? (
// 显示选中会话的聊天内容
(<ChatScreen
isEmbedded
embeddedConversationId={String(selectedConversation.id)}
embeddedIsGroupChat={selectedConversation.type === 'group'}
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
embeddedGroupName={selectedConversation.group?.name}
onEmbeddedBack={stableClearSelectedConversation}
/>)
) : (
// 默认占位符
(<View style={styles.chatPlaceholder}>
<View style={styles.chatPlaceholderContent}>
<MaterialCommunityIcons name="message-text-outline" size={64} color={colors.chat.iconMuted} />
<Text style={styles.chatPlaceholderText}></Text>
</View>
</View>)
)}
</View>
</View>
{renderActionMenu()}
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
2026-03-16 17:47:10 +08:00
{showNotifications ? (
// 显示系统通知页面,传入 onBack 回调
(<NotificationsScreen onBack={stableCloseNotifications} />)
2026-03-16 17:47:10 +08:00
) : isSearchMode ? (
renderSearchMode()
2026-03-16 17:47:10 +08:00
) : isWideScreen ? (
<ResponsiveContainer maxWidth={800}>
{renderConversationList()}
</ResponsiveContainer>
) : (
2026-03-16 17:47:10 +08:00
renderConversationList()
)}
{renderActionMenu()}
<QRCodeScanner visible={scannerVisible} onClose={stableCloseScanner} />
</SafeAreaView>
);
};
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.default,
},
searchTabContainer: {
backgroundColor: colors.background.default,
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,
},
});
}