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

@@ -77,6 +77,7 @@ export const ChatScreen: React.FC = () => {
// 输入框区域高度(用于定位浮动 mention 面板)
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
const [showEdgeLoadingIndicator, setShowEdgeLoadingIndicator] = useState(false);
const isPreloadingRef = useRef(false);
const lastScrollYRef = useRef(0);
const isUserDraggingRef = useRef(false);
@@ -203,8 +204,15 @@ export const ChatScreen: React.FC = () => {
loadMoreHistory,
handleMessageListContentSizeChange,
handleReachLatestEdge,
setBrowsingHistory,
} = useChatScreen();
useEffect(() => {
if (!loadingMore && showEdgeLoadingIndicator) {
setShowEdgeLoadingIndicator(false);
}
}, [loadingMore, showEdgeLoadingIndicator]);
useEffect(() => {
return () => {
if (preloadCooldownTimerRef.current) {
@@ -231,7 +239,7 @@ export const ChatScreen: React.FC = () => {
return map;
}, [messages]);
const renderMessage = ({ item, index }: { item: any; index: number }) => {
const renderMessage = useCallback(({ item, index }: { item: any; index: number }) => {
// inverted 下 renderItem 的 index 与时间顺序不一致,需回查原始序索引
const logicalIndex = messageIndexMap.get(String(item.id)) ?? index;
return (
@@ -255,7 +263,26 @@ export const ChatScreen: React.FC = () => {
onReply={handleReplyMessage}
/>
);
};
}, [
messageIndexMap,
currentUserId,
currentUser,
otherUser,
isGroupChat,
groupMembers,
otherUserLastReadSeq,
selectedMessageId,
messageMap,
handleLongPressMessage,
handleAvatarPress,
handleAvatarLongPress,
formatTime,
shouldShowTime,
handleImagePress,
handleReplyMessage,
]);
const keyExtractor = useCallback((item: any) => String(item.id), []);
// 获取正在输入提示
const typingHint = getTypingHint();
@@ -269,6 +296,11 @@ export const ChatScreen: React.FC = () => {
viewportHeight: layoutMeasurement.height,
};
// 离开最新消息端后进入“浏览历史”模式,屏蔽自动跟随到底
if (contentOffset.y > 120) {
setBrowsingHistory(true);
}
// 仅用户手势主动回到底部时才解除历史阅读锁(避免加载阶段误解锁)
const isScrollingTowardLatest = contentOffset.y < lastScrollYRef.current;
if (
@@ -282,13 +314,19 @@ export const ChatScreen: React.FC = () => {
// inverted 下历史端在“列表尾部”,对应 offset 增大且距离尾部变小
const distanceToHistoryEdge = contentSize.height - (contentOffset.y + layoutMeasurement.height);
const isScrollingTowardHistory = contentOffset.y > lastScrollYRef.current;
lastScrollYRef.current = contentOffset.y;
const PRELOAD_HISTORY_THRESHOLD = 120;
// 预加载阈值:按屏幕高度动态提前(至少 420确保边界前开始加载
const PRELOAD_HISTORY_THRESHOLD = Math.max(420, layoutMeasurement.height * 1.2);
// 仅在真正接近边界时才显示旋转提示
const EDGE_LOADING_INDICATOR_THRESHOLD = 60;
const shouldShowEdgeIndicator = loadingMore && distanceToHistoryEdge <= EDGE_LOADING_INDICATOR_THRESHOLD;
if (shouldShowEdgeIndicator !== showEdgeLoadingIndicator) {
setShowEdgeLoadingIndicator(shouldShowEdgeIndicator);
}
if (
distanceToHistoryEdge <= PRELOAD_HISTORY_THRESHOLD &&
isScrollingTowardHistory &&
hasMoreHistory &&
!loadingMore &&
!loading &&
@@ -304,7 +342,7 @@ export const ChatScreen: React.FC = () => {
}, 350);
});
}
}, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge]);
}, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge, showEdgeLoadingIndicator, setBrowsingHistory]);
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
handleMessageListContentSizeChange(contentWidth, contentHeight);
@@ -338,6 +376,7 @@ export const ChatScreen: React.FC = () => {
onLayout={e => {
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
}}
onTouchEnd={handleDismiss}
>
{loading ? (
<View style={styles.loadingContainer}>
@@ -349,10 +388,15 @@ export const ChatScreen: React.FC = () => {
inverted
data={displayMessages}
renderItem={renderMessage}
keyExtractor={(item: any) => String(item.id)}
keyExtractor={keyExtractor}
contentContainerStyle={listContentStyle as any}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
initialNumToRender={14}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
windowSize={9}
removeClippedSubviews={Platform.OS !== 'web'}
onScroll={handleMessageListScroll}
onScrollBeginDrag={() => {
isUserDraggingRef.current = true;
@@ -368,7 +412,7 @@ export const ChatScreen: React.FC = () => {
onContentSizeChange={handleContentSizeChange}
/>
)}
{loadingMore && (
{showEdgeLoadingIndicator && (
<View
pointerEvents="none"
style={{

View File

@@ -120,6 +120,7 @@ export const useChatScreen = () => {
const prevMessageCountRef = useRef(0);
const prevLatestSeqRef = useRef(0);
const prevMarkedReadSeqRef = useRef(0);
const enterMarkedKeyRef = useRef<string>('');
const isProgrammaticScrollRef = useRef(false);
const suppressAutoFollowRef = useRef(false);
const isBrowsingHistoryRef = useRef(false);
@@ -277,6 +278,7 @@ export const useChatScreen = () => {
prevMessageCountRef.current = 0;
prevLatestSeqRef.current = 0;
prevMarkedReadSeqRef.current = 0;
enterMarkedKeyRef.current = '';
suppressAutoFollowRef.current = false;
isBrowsingHistoryRef.current = false;
}, [conversationId]);
@@ -479,6 +481,29 @@ export const useChatScreen = () => {
isBrowsingHistoryRef.current = browsing;
}, []);
// 进入聊天详情后立即清未读(不依赖滚动位置)
useEffect(() => {
if (!conversationId) return;
if (!conversation) return;
const unreadCount = Number((conversation as any).unread_count || 0);
if (unreadCount <= 0) return;
const latestFromConversation = Number((conversation as any).last_seq || 0);
const latestFromMessages = messages.length > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0;
const targetSeq = Math.max(latestFromConversation, latestFromMessages);
if (targetSeq <= 0) return;
const markKey = `${conversationId}:${targetSeq}`;
if (enterMarkedKeyRef.current === markKey) return;
enterMarkedKeyRef.current = markKey;
prevMarkedReadSeqRef.current = Math.max(prevMarkedReadSeqRef.current, targetSeq);
markAsRead(targetSeq).catch(error => {
console.error('[ChatScreen] 进入会话清未读失败:', error);
});
}, [conversationId, conversation, messages, markAsRead]);
// 自动标记已读QQ/Telegram 风格):
// 仅当出现更大的 latest seq且用户在最新端附近时才上报。
// 历史加载/浏览历史不会触发 read。
@@ -521,20 +546,10 @@ export const useChatScreen = () => {
const keyboardWillShow = (e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height);
setActivePanel(prev => prev === 'mention' ? 'mention' : 'none');
if (isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'keyboard-show');
}, 150);
}
};
const keyboardWillHide = () => {
setKeyboardHeight(0);
if (isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'keyboard-hide');
}, 100);
}
};
const showSubscription = Keyboard.addListener(
@@ -550,7 +565,7 @@ export const useChatScreen = () => {
showSubscription.remove();
hideSubscription.remove();
};
}, [scrollToLatest, isNearBottom]);
}, []);
// 格式化时间
const formatTime = useCallback((dateString: string): string => {
@@ -996,28 +1011,18 @@ export const useChatScreen = () => {
Keyboard.dismiss();
setActivePanel(prev => {
const newPanel = prev === 'emoji' ? 'none' : 'emoji';
if (newPanel !== 'none' && isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'open-emoji-panel');
}, 200);
}
return newPanel;
});
}, [scrollToLatest, isNearBottom]);
}, []);
// 切换更多功能面板
const toggleMorePanel = useCallback(() => {
Keyboard.dismiss();
setActivePanel(prev => {
const newPanel = prev === 'more' ? 'none' : 'more';
if (newPanel !== 'none' && isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'open-more-panel');
}, 200);
}
return newPanel;
});
}, [scrollToLatest, isNearBottom]);
}, []);
// 关闭面板
const closePanel = useCallback(() => {