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={[
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Post, Comment, VoteResultDTO } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
|
||||
@@ -62,15 +63,6 @@ export const PostDetailScreen: React.FC = () => {
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, '2xl': 28, '3xl': 32, '4xl': 40 });
|
||||
const responsiveGap = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 28, '4xl': 32 });
|
||||
|
||||
const {
|
||||
likePost,
|
||||
unlikePost,
|
||||
favoritePost,
|
||||
unfavoritePost,
|
||||
likeComment,
|
||||
unlikeComment
|
||||
} = useUserStore();
|
||||
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
@@ -144,14 +136,15 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 从API获取帖子详情
|
||||
const postData = await postService.getPost(postId);
|
||||
// 使用 ProcessPostUseCase 获取帖子详情
|
||||
const postData = await processPostUseCase.fetchPostById(postId);
|
||||
if (postData) {
|
||||
setPost(postData);
|
||||
// 类型转换:将 core/entities/Post 转换为 PostDTO 以保持兼容性
|
||||
setPost(postData as unknown as Post);
|
||||
// 初始化关注状态
|
||||
if (postData.author) {
|
||||
setIsFollowing(postData.author.is_following || false);
|
||||
setIsFollowingMe(postData.author.is_following_me || false);
|
||||
setIsFollowing((postData.author as any).is_following || false);
|
||||
setIsFollowingMe((postData.author as any).is_following_me || false);
|
||||
}
|
||||
// 只在首次加载时记录浏览量
|
||||
if (recordView && !hasRecordedView.current) {
|
||||
@@ -163,7 +156,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
// 如果是投票帖子,立即加载投票数据
|
||||
if (postData.is_vote) {
|
||||
if ((postData as any).is_vote) {
|
||||
setIsVoteLoading(true);
|
||||
try {
|
||||
const voteData = await voteService.getVoteResult(postId);
|
||||
@@ -184,8 +177,8 @@ export const PostDetailScreen: React.FC = () => {
|
||||
setPost(updatedPost);
|
||||
// 初始化关注状态
|
||||
if (updatedPost.author) {
|
||||
setIsFollowing(updatedPost.author.is_following || false);
|
||||
setIsFollowingMe(updatedPost.author.is_following_me || false);
|
||||
setIsFollowing((updatedPost.author as any).is_following || false);
|
||||
setIsFollowingMe((updatedPost.author as any).is_following_me || false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,10 +354,11 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsLiked) {
|
||||
await unlikePost(post.id);
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
} else {
|
||||
await likePost(post.id);
|
||||
await processPostUseCase.likePost(post.id);
|
||||
}
|
||||
// UseCase 会更新 store,本地状态已经是乐观更新的,无需再次更新
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
// 失败时回滚状态
|
||||
@@ -374,7 +368,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
likes_count: oldLikesCount
|
||||
} : null);
|
||||
}
|
||||
}, [post, likePost, unlikePost]);
|
||||
}, [post]);
|
||||
|
||||
// 收藏帖子
|
||||
const handleFavorite = useCallback(async () => {
|
||||
@@ -393,10 +387,11 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsFavorited) {
|
||||
await unfavoritePost(post.id);
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
} else {
|
||||
await favoritePost(post.id);
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
}
|
||||
// UseCase 会更新 store,本地状态已经是乐观更新的,无需再次更新
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
// 失败时回滚状态
|
||||
@@ -406,7 +401,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
favorites_count: oldFavoritesCount
|
||||
} : null);
|
||||
}
|
||||
}, [post, favoritePost, unfavoritePost]);
|
||||
}, [post]);
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = useCallback(async () => {
|
||||
@@ -527,17 +522,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
onPress: async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const success = await postService.deletePost(post.id);
|
||||
if (success) {
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
|
||||
}
|
||||
await processPostUseCase.deletePost(post.id);
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('删除帖子失败:', error);
|
||||
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
|
||||
@@ -817,9 +808,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsLiked) {
|
||||
await unlikeComment(comment.id);
|
||||
await commentService.unlikeComment(comment.id);
|
||||
} else {
|
||||
await likeComment(comment.id);
|
||||
await commentService.likeComment(comment.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('评论点赞操作失败:', error);
|
||||
|
||||
Reference in New Issue
Block a user