refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
This commit is contained in:
@@ -32,8 +32,8 @@ import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CursorPaginationRequest } from '../../types/dto';
|
||||
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { SearchScreen } from './SearchScreen';
|
||||
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||
import { navigationService } from '../../infrastructure/navigation/navigationService';
|
||||
@@ -55,7 +55,7 @@ type PostType = 'recommend' | 'follow' | 'hot' | 'latest';
|
||||
export const HomeScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
|
||||
const { posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 使用响应式 hook
|
||||
@@ -89,6 +89,15 @@ export const HomeScreen: React.FC = () => {
|
||||
const postIdsRef = useRef<Set<string>>(new Set());
|
||||
const lastSwipeAtRef = useRef(0);
|
||||
|
||||
// 构建一个以 postId 为 key 的 map,用于快速查找
|
||||
const postsMap = useMemo(() => {
|
||||
const map = new Map<string, Post>();
|
||||
storePosts.forEach(post => {
|
||||
map.set(post.id, post);
|
||||
});
|
||||
return map;
|
||||
}, [storePosts]);
|
||||
|
||||
// 获取当前 tab 对应的帖子类型
|
||||
const getPostType = useCallback((): PostType => {
|
||||
switch (activeIndex) {
|
||||
@@ -100,42 +109,70 @@ export const HomeScreen: React.FC = () => {
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
// 使用游标分页获取帖子列表
|
||||
// 使用差异更新 Hook 获取帖子列表
|
||||
const listKey = useMemo(() => `home_${getPostType()}`, [getPostType]);
|
||||
|
||||
const {
|
||||
items: posts,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
posts,
|
||||
loading: isLoading,
|
||||
refreshing: isRefreshing,
|
||||
hasMore,
|
||||
error,
|
||||
loadMore,
|
||||
refresh,
|
||||
} = useCursorPagination<Post, { post_type: PostType }>(
|
||||
async ({ cursor, pageSize, extraParams }) => {
|
||||
const params: CursorPaginationRequest = {
|
||||
cursor,
|
||||
page_size: pageSize,
|
||||
post_type: extraParams?.post_type,
|
||||
};
|
||||
const response = await postService.getPostsCursor(params);
|
||||
return response;
|
||||
},
|
||||
{ pageSize: DEFAULT_PAGE_SIZE },
|
||||
{ post_type: getPostType() }
|
||||
reset,
|
||||
} = useDifferentialPosts<Post>(
|
||||
[],
|
||||
{
|
||||
listKey,
|
||||
enableDiff: true,
|
||||
enableBatching: true,
|
||||
autoSubscribe: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Tab 切换时刷新数据
|
||||
// 加载更多方法
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !isLoading) {
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
console.error('加载更多失败:', err);
|
||||
}
|
||||
}
|
||||
}, [listKey, hasMore, isLoading]);
|
||||
|
||||
// 刷新方法 - 先获取正确类型的帖子,再刷新
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
// 先获取正确类型的帖子
|
||||
await processPostUseCase.fetchPosts(
|
||||
{ page: 1, pageSize: DEFAULT_PAGE_SIZE, post_type: getPostType() },
|
||||
listKey
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('刷新失败:', err);
|
||||
}
|
||||
}, [listKey, getPostType]);
|
||||
|
||||
// Tab 切换时刷新数据并重置列表
|
||||
useEffect(() => {
|
||||
reset();
|
||||
refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeIndex]);
|
||||
|
||||
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
|
||||
useEffect(() => {
|
||||
if (posts.length === 0) return;
|
||||
|
||||
// 更新 postIdsRef
|
||||
const currentPostIds = new Set(posts.map(p => p.id));
|
||||
postIdsRef.current = currentPostIds;
|
||||
}, [posts, storePosts]);
|
||||
// 将获取到的原始帖子与 store 中的状态合并
|
||||
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
||||
const displayPosts = useMemo(() => {
|
||||
return posts.map(post => {
|
||||
const storePost = postsMap.get(post.id);
|
||||
if (storePost) {
|
||||
// 如果 store 中有这个帖子,使用 store 的最新状态
|
||||
return storePost;
|
||||
}
|
||||
// 如果 store 中没有这个帖子,使用原始数据
|
||||
return post;
|
||||
});
|
||||
}, [posts, postsMap]);
|
||||
|
||||
// 根据屏幕尺寸确定网格列数
|
||||
const gridColumns = useMemo(() => {
|
||||
@@ -242,20 +279,28 @@ export const HomeScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
// 点赞帖子
|
||||
const handleLike = (post: Post) => {
|
||||
if (post.is_liked) {
|
||||
unlikePost(post.id);
|
||||
} else {
|
||||
likePost(post.id);
|
||||
const handleLike = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏帖子
|
||||
const handleBookmark = (post: Post) => {
|
||||
if (post.is_favorited) {
|
||||
unfavoritePost(post.id);
|
||||
} else {
|
||||
favoritePost(post.id);
|
||||
const handleBookmark = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -272,7 +317,7 @@ export const HomeScreen: React.FC = () => {
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
};
|
||||
|
||||
// 删除帖子 - 由于 posts 来自 Hook,需要刷新列表
|
||||
// 删除帖子 - 删除后刷新列表
|
||||
const handleDeletePost = async (postId: string) => {
|
||||
try {
|
||||
const success = await postService.deletePost(postId);
|
||||
@@ -377,7 +422,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const columnHeights: number[] = Array(gridColumns).fill(0);
|
||||
|
||||
// 防御性检查:确保 posts 存在且是数组
|
||||
if (!posts || !Array.isArray(posts) || posts.length === 0) {
|
||||
if (!displayPosts || !Array.isArray(displayPosts) || displayPosts.length === 0) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -385,7 +430,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const totalGap = (gridColumns - 1) * responsiveGap;
|
||||
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
|
||||
|
||||
posts.forEach((post) => {
|
||||
displayPosts.forEach((post) => {
|
||||
const postHeight = estimatePostHeight(post, columnWidth);
|
||||
|
||||
// 找到当前高度最小的列
|
||||
@@ -395,7 +440,7 @@ export const HomeScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
return columns;
|
||||
}, [posts, gridColumns, width, responsiveGap, responsivePadding]);
|
||||
}, [displayPosts, gridColumns, width, responsiveGap, responsivePadding]);
|
||||
|
||||
// 渲染单列帖子
|
||||
const renderWaterfallColumn = (column: Post[], columnIndex: number) => (
|
||||
@@ -461,7 +506,7 @@ export const HomeScreen: React.FC = () => {
|
||||
gap={{ xs: 8, sm: 12, md: 16, lg: 20, xl: 24 }}
|
||||
containerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 }}
|
||||
>
|
||||
{posts.map(post => {
|
||||
{displayPosts.map(post => {
|
||||
const authorId = post.author?.id || '';
|
||||
const isPostAuthor = currentUser?.id === authorId;
|
||||
return (
|
||||
@@ -507,7 +552,7 @@ export const HomeScreen: React.FC = () => {
|
||||
// 移动端和宽屏都使用单列 FlatList,宽屏下居中显示
|
||||
return (
|
||||
<FlatList
|
||||
data={posts}
|
||||
data={displayPosts}
|
||||
renderItem={renderPostList}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={[
|
||||
|
||||
Reference in New Issue
Block a user