refactor(ProcessPostUseCase, useCursorPagination, useDifferentialPosts): enhance post merging and loading states
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m57s
Frontend CI / ota-android (push) Successful in 10m48s
Frontend CI / build-android-apk (push) Successful in 52m58s

- Introduced new methods for merging posts and comments efficiently, improving performance during data updates.
- Updated loading state management in hooks to include initial loading and loading more states for better user feedback.
- Refactored HomeScreen and PostDetailScreen to utilize the new loading states and merging logic, enhancing user experience during data fetching.
- Improved pagination handling in useCursorPagination and useDifferentialPosts to ensure consistent data management across components.
This commit is contained in:
lafay
2026-03-24 01:19:09 +08:00
parent aaf53d1c3b
commit 5c9cb6acea
11 changed files with 475 additions and 101 deletions

View File

@@ -119,6 +119,8 @@ export const HomeScreen: React.FC = () => {
const {
posts,
loading: isLoading,
isInitialLoading,
isLoadingMore,
refreshing: isRefreshing,
hasMore,
error,
@@ -135,17 +137,23 @@ export const HomeScreen: React.FC = () => {
// 加载更多方法
const loadMore = useCallback(async () => {
if (hasMore && !isLoading && !isLoadingMoreRef.current) {
if (hasMore && !isLoadingMore && !isLoadingMoreRef.current) {
isLoadingMoreRef.current = true;
const startedAt = Date.now();
try {
await processPostUseCase.loadMorePosts(listKey);
console.log('[PostPerf] home loadMore', {
listKey,
costMs: Date.now() - startedAt,
totalItems: posts.length,
});
} catch (err) {
console.error('加载更多失败:', err);
} finally {
isLoadingMoreRef.current = false;
}
}
}, [listKey, hasMore, isLoading]);
}, [posts.length, listKey, hasMore, isLoadingMore]);
// 网格模式滚动处理 - 检测是否滚动到底部
const handleGridScroll = useCallback((event: any) => {
@@ -366,7 +374,9 @@ export const HomeScreen: React.FC = () => {
};
// 渲染帖子卡片(列表模式)
const renderPostList = ({ item }: { item: Post }) => {
const keyExtractor = useCallback((item: Post) => item.id, []);
const renderPostList = useCallback(({ item }: { item: Post }) => {
const authorId = item.author?.id || '';
const isPostAuthor = currentUser?.id === authorId;
return (
@@ -394,7 +404,7 @@ export const HomeScreen: React.FC = () => {
/>
</View>
);
};
}, [currentUser?.id, handleBookmark, handleDeletePost, handleImagePress, handleLike, handlePostPress, handleShare, handleUserPress, isMobile, listItemWidth, responsiveGap]);
// 估算帖子在瀑布流中的高度(用于均匀分配)
const estimatePostHeight = (post: Post, columnWidth: number): number => {
@@ -558,8 +568,8 @@ export const HomeScreen: React.FC = () => {
};
// 渲染空状态
const renderEmpty = () => {
if (isLoading) return null;
const renderEmpty = useCallback(() => {
if (isInitialLoading) return null;
return (
<EmptyState
@@ -567,7 +577,7 @@ export const HomeScreen: React.FC = () => {
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'}
/>
);
};
}, [activeIndex, isInitialLoading]);
// 渲染列表内容
const renderListContent = () => {
@@ -581,7 +591,7 @@ export const HomeScreen: React.FC = () => {
<FlatList
data={displayPosts}
renderItem={renderPostList}
keyExtractor={item => item.id}
keyExtractor={keyExtractor}
contentContainerStyle={[
styles.listContent,
{
@@ -602,7 +612,12 @@ export const HomeScreen: React.FC = () => {
onEndReached={loadMore}
onEndReachedThreshold={0.3}
ListEmptyComponent={renderEmpty}
ListFooterComponent={isLoading ? <Loading size="sm" /> : null}
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
initialNumToRender={8}
maxToRenderPerBatch={8}
updateCellsBatchingPeriod={60}
windowSize={7}
removeClippedSubviews
/>
);
};
@@ -661,7 +676,9 @@ export const HomeScreen: React.FC = () => {
// 移动端:使用 GestureDetector 支持手势切换 Tab
<GestureDetector gesture={swipeGesture}>
<View style={styles.contentContainer}>
{viewMode === 'list' ? (
{isInitialLoading ? (
<Loading fullScreen />
) : viewMode === 'list' ? (
renderListContent()
) : (
// 网格模式:使用响应式网格
@@ -672,7 +689,9 @@ export const HomeScreen: React.FC = () => {
) : (
// 桌面端/平板端:不使用 GestureDetector避免拦截侧边栏点击
<View style={styles.contentContainer}>
{viewMode === 'list' ? (
{isInitialLoading ? (
<Loading fullScreen />
) : viewMode === 'list' ? (
renderListContent()
) : (
// 网格模式:使用响应式网格

View File

@@ -67,13 +67,15 @@ export const PostDetailScreen: React.FC = () => {
const [post, setPost] = useState<Post | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [isPostInitialLoading, setIsPostInitialLoading] = useState(true);
const [isPostRefreshing, setIsPostRefreshing] = useState(false);
// 使用游标分页 Hook 管理评论列表
const {
list: paginatedComments,
isLoading: isCommentsLoading,
isInitialLoading: isCommentsInitialLoading,
isLoadingMore: isCommentsLoadingMore,
isRefreshing: isCommentsRefreshing,
hasMore: hasMoreComments,
loadMore: loadMoreComments,
@@ -88,11 +90,71 @@ export const PostDetailScreen: React.FC = () => {
},
{ pageSize: 20 }
);
const getCommentId = useCallback((comment: Comment) => String(comment.id), []);
const mergeCommentsById = useCallback((previousInput: Comment[] | null | undefined, incomingInput: Comment[] | null | undefined): Comment[] => {
const previous = Array.isArray(previousInput) ? previousInput : [];
const incoming = Array.isArray(incomingInput) ? incomingInput : [];
if (incoming.length === 0) return previous;
if (previous.length === 0) return incoming;
const previousSet = new Set(previous.map(getCommentId));
const incomingFirstId = getCommentId(incoming[0]);
if (!previousSet.has(incomingFirstId)) {
return [...previous, ...incoming];
}
const merged = [...previous];
const indexById = new Map<string, number>();
for (let i = 0; i < merged.length; i += 1) {
indexById.set(getCommentId(merged[i]), i);
}
for (const comment of incoming) {
const id = getCommentId(comment);
const existingIndex = indexById.get(id);
if (existingIndex === undefined) {
indexById.set(id, merged.length);
merged.push(comment);
} else {
merged[existingIndex] = comment;
}
}
return merged;
}, [getCommentId]);
const mergeCommentsRefreshWindow = useCallback((previousInput: Comment[] | null | undefined, latestInput: Comment[] | null | undefined): Comment[] => {
const previous = Array.isArray(previousInput) ? previousInput : [];
const latest = Array.isArray(latestInput) ? latestInput : [];
if (latest.length === 0) return previous;
if (previous.length === 0) return latest;
const latestSet = new Set(latest.map(getCommentId));
const tail = previous.filter((item) => !latestSet.has(getCommentId(item)));
return [...latest, ...tail];
}, [getCommentId]);
// 同步分页评论到本地状态
// 切换帖子时,先清空上一帖评论,避免跨帖残留。
useEffect(() => {
setComments(paginatedComments);
}, [paginatedComments]);
setComments([]);
}, [postId]);
// 同步分页评论到本地状态(增量合并,避免全量替换)
useEffect(() => {
setComments((prev) => {
if (isCommentsRefreshing) {
return mergeCommentsRefreshWindow(prev, paginatedComments);
}
return mergeCommentsById(prev, paginatedComments);
});
}, [isCommentsRefreshing, mergeCommentsById, mergeCommentsRefreshWindow, paginatedComments]);
useEffect(() => {
if (commentsError) {
console.warn('[PostPerf] comments request error', { postId, error: commentsError });
}
}, [commentsError, postId]);
const [commentText, setCommentText] = useState('');
const [showImageModal, setShowImageModal] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
@@ -131,7 +193,7 @@ export const PostDetailScreen: React.FC = () => {
// 加载帖子详情(可选择是否记录浏览量)
const loadPostDetail = useCallback(async (recordView: boolean = false) => {
if (!postId) {
setLoading(false);
setIsPostInitialLoading(false);
return;
}
@@ -188,7 +250,7 @@ export const PostDetailScreen: React.FC = () => {
} catch (error) {
console.error('加载帖子详情失败:', error);
} finally {
setLoading(false);
setIsPostInitialLoading(false);
}
}, [postId]);
@@ -206,13 +268,13 @@ export const PostDetailScreen: React.FC = () => {
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
useEffect(() => {
if (shouldScrollToComments && !loading && (comments?.length ?? 0) > 0) {
if (shouldScrollToComments && !isPostInitialLoading && (comments?.length ?? 0) > 0) {
// 延迟确保列表已完成渲染和布局
setTimeout(() => {
flatListRef.current?.scrollToIndex({ index: 0, animated: true, viewPosition: 0 });
}, 500);
}
}, [shouldScrollToComments, loading, comments?.length]);
}, [shouldScrollToComments, isPostInitialLoading, comments?.length]);
// 动态设置导航头部
useEffect(() => {
@@ -280,11 +342,26 @@ export const PostDetailScreen: React.FC = () => {
// 下拉刷新 - 同时刷新帖子和评论
const onRefresh = useCallback(async () => {
setRefreshing(true);
const startedAt = Date.now();
setIsPostRefreshing(true);
await loadPostDetail();
await refreshComments();
setRefreshing(false);
}, [loadPostDetail, refreshComments]);
console.log('[PostPerf] detail refresh', {
postId,
costMs: Date.now() - startedAt,
comments: comments.length,
});
setIsPostRefreshing(false);
}, [comments.length, loadPostDetail, postId, refreshComments]);
const handleLoadMoreComments = useCallback(async () => {
const startedAt = Date.now();
await loadMoreComments();
console.log('[PostPerf] detail loadMoreComments', {
postId,
costMs: Date.now() - startedAt,
totalItems: comments.length,
});
}, [comments.length, loadMoreComments, postId]);
const formatDateTime = (dateString?: string | null): string => {
if (!dateString) return '';
@@ -1259,7 +1336,9 @@ export const PostDetailScreen: React.FC = () => {
};
// 渲染评论 - 带楼层号
const renderComment = ({ item, index }: { item: Comment; index: number }) => {
const commentKeyExtractor = useCallback((item: Comment) => item.id, []);
const renderComment = useCallback(({ item, index }: { item: Comment; index: number }) => {
const authorId = item.author?.id || '';
// 收集当前评论的所有回复,用于根据 target_id 查找被回复用户
const allReplies = item.replies || [];
@@ -1283,10 +1362,10 @@ export const PostDetailScreen: React.FC = () => {
currentUserId={currentUser?.id}
/>
);
};
}, [currentUser?.id, handleDeleteComment, handleImagePress, handleLikeComment, handleLoadMoreReplies, post?.user_id]);
// 渲染空评论 - 现代化设计
const renderEmptyComments = () => (
const renderEmptyComments = useCallback(() => (
<View style={styles.emptyCommentsContainer}>
<View style={styles.emptyCommentsIconWrapper}>
<MaterialCommunityIcons
@@ -1302,7 +1381,7 @@ export const PostDetailScreen: React.FC = () => {
~
</Text>
</View>
);
), [isDesktop]);
// 渲染评论输入框
const renderCommentInput = () => (
@@ -1398,7 +1477,7 @@ export const PostDetailScreen: React.FC = () => {
ref={flatListRef}
data={comments}
renderItem={renderComment}
keyExtractor={item => item.id}
keyExtractor={commentKeyExtractor}
ListEmptyComponent={renderEmptyComments}
contentContainerStyle={{ paddingBottom: responsiveGap }}
showsVerticalScrollIndicator={false}
@@ -1409,23 +1488,32 @@ export const PostDetailScreen: React.FC = () => {
}}
refreshControl={
<RefreshControl
refreshing={refreshing || isCommentsRefreshing}
refreshing={isPostRefreshing || isCommentsRefreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMoreComments}
onEndReached={handleLoadMoreComments}
onEndReachedThreshold={0.3}
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={60}
windowSize={7}
removeClippedSubviews
ListFooterComponent={
isCommentsLoading ? (
isCommentsInitialLoading ? (
<View style={styles.commentsLoadingFooter}>
<Loading size="sm" />
</View>
) : isCommentsLoadingMore ? (
<View style={styles.commentsLoadingFooter}>
<Loading size="sm" />
</View>
) : hasMoreComments ? (
<TouchableOpacity
style={styles.loadMoreButton}
onPress={loadMoreComments}
onPress={handleLoadMoreComments}
>
<Text variant="caption" color={colors.primary.main}>
@@ -1456,7 +1544,7 @@ export const PostDetailScreen: React.FC = () => {
</View>
);
if (loading) {
if (isPostInitialLoading) {
return <Loading fullScreen />;
}
@@ -1489,7 +1577,7 @@ export const PostDetailScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing || isCommentsRefreshing}
refreshing={isPostRefreshing || isCommentsRefreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
@@ -1530,7 +1618,7 @@ export const PostDetailScreen: React.FC = () => {
ref={flatListRef}
data={comments}
renderItem={renderComment}
keyExtractor={item => item.id}
keyExtractor={commentKeyExtractor}
ListHeaderComponent={renderPostHeader}
ListEmptyComponent={renderEmptyComments}
contentContainerStyle={{ paddingBottom: responsiveGap }}
@@ -1542,23 +1630,32 @@ export const PostDetailScreen: React.FC = () => {
}}
refreshControl={
<RefreshControl
refreshing={refreshing || isCommentsRefreshing}
refreshing={isPostRefreshing || isCommentsRefreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMoreComments}
onEndReached={handleLoadMoreComments}
onEndReachedThreshold={0.3}
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={60}
windowSize={7}
removeClippedSubviews
ListFooterComponent={
isCommentsLoading ? (
isCommentsInitialLoading ? (
<View style={styles.commentsLoadingFooter}>
<Loading size="sm" />
</View>
) : isCommentsLoadingMore ? (
<View style={styles.commentsLoadingFooter}>
<Loading size="sm" />
</View>
) : hasMoreComments ? (
<TouchableOpacity
style={styles.loadMoreButton}
onPress={loadMoreComments}
onPress={handleLoadMoreComments}
>
<Text variant="caption" color={colors.primary.main}>