refactor(LocalDataSource, PostRepository, ChatScreen): enhance database handling and message loading
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 2m41s
Frontend CI / ota-android (push) Successful in 12m45s
Frontend CI / build-android-apk (push) Has been cancelled

- Introduced a promise-based initialization mechanism in LocalDataSource to prevent concurrent initialization issues.
- Updated PostRepository to utilize an enqueueWrite method for database operations, ensuring thread-safe writes.
- Refactored ChatScreen to improve message loading logic, including preloading history and managing scroll behavior more effectively.
- Enhanced message rendering logic to maintain correct indices in inverted lists and optimize user experience during scrolling.
This commit is contained in:
lafay
2026-03-23 23:06:19 +08:00
parent 7305254e11
commit c98f1917f7
9 changed files with 396 additions and 185 deletions

View File

@@ -22,12 +22,11 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import {
View,
TouchableWithoutFeedback,
FlatList,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { FlashList } from "@shopify/flash-list";
import { useNavigation } from '@react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -78,6 +77,15 @@ export const ChatScreen: React.FC = () => {
// 输入框区域高度(用于定位浮动 mention 面板)
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
const isPreloadingRef = useRef(false);
const lastScrollYRef = useRef(0);
const isUserDraggingRef = useRef(false);
const historyAnchorRef = useRef<{ active: boolean; beforeY: number; beforeHeight: number }>({
active: false,
beforeY: 0,
beforeHeight: 0,
});
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const containerStyle = useMemo(() => ([
styles.container,
@@ -194,8 +202,17 @@ export const ChatScreen: React.FC = () => {
navigateToChatSettings,
loadMoreHistory,
handleMessageListContentSizeChange,
handleReachLatestEdge,
} = useChatScreen();
useEffect(() => {
return () => {
if (preloadCooldownTimerRef.current) {
clearTimeout(preloadCooldownTimerRef.current);
}
};
}, []);
// 监听返回事件,刷新会话列表
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', () => {
@@ -206,38 +223,92 @@ export const ChatScreen: React.FC = () => {
}, [navigation]);
// 渲染消息气泡
const renderMessage = ({ item, index }: { item: any; index: number }) => (
<MessageBubble
message={item}
index={index}
currentUserId={currentUserId}
currentUser={currentUser}
otherUser={otherUser}
isGroupChat={isGroupChat}
groupMembers={groupMembers}
otherUserLastReadSeq={otherUserLastReadSeq}
selectedMessageId={selectedMessageId}
messageMap={messageMap}
onLongPress={handleLongPressMessage}
onAvatarPress={handleAvatarPress}
onAvatarLongPress={handleAvatarLongPress}
formatTime={formatTime}
shouldShowTime={shouldShowTime}
onImagePress={handleImagePress}
onReply={handleReplyMessage}
/>
);
const messageIndexMap = useMemo(() => {
const map = new Map<string, number>();
messages.forEach((msg, idx) => {
map.set(String(msg.id), idx);
});
return map;
}, [messages]);
const renderMessage = ({ item, index }: { item: any; index: number }) => {
// inverted 下 renderItem 的 index 与时间顺序不一致,需回查原始序索引
const logicalIndex = messageIndexMap.get(String(item.id)) ?? index;
return (
<MessageBubble
message={item}
index={logicalIndex}
currentUserId={currentUserId}
currentUser={currentUser}
otherUser={otherUser}
isGroupChat={isGroupChat}
groupMembers={groupMembers}
otherUserLastReadSeq={otherUserLastReadSeq}
selectedMessageId={selectedMessageId}
messageMap={messageMap}
onLongPress={handleLongPressMessage}
onAvatarPress={handleAvatarPress}
onAvatarLongPress={handleAvatarLongPress}
formatTime={formatTime}
shouldShowTime={shouldShowTime}
onImagePress={handleImagePress}
onReply={handleReplyMessage}
/>
);
};
// 获取正在输入提示
const typingHint = getTypingHint();
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
const handleMessageListScroll = useCallback((event: any) => {
const { contentSize, contentOffset } = event.nativeEvent;
const { contentSize, contentOffset, layoutMeasurement } = event.nativeEvent;
scrollPositionRef.current = {
contentHeight: contentSize.height,
scrollY: contentOffset.y,
viewportHeight: layoutMeasurement.height,
};
}, [scrollPositionRef]);
// 仅用户手势主动回到底部时才解除历史阅读锁(避免加载阶段误解锁)
const isScrollingTowardLatest = contentOffset.y < lastScrollYRef.current;
if (
isUserDraggingRef.current &&
!loadingMore &&
isScrollingTowardLatest &&
contentOffset.y <= 40
) {
handleReachLatestEdge();
}
// 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;
if (
distanceToHistoryEdge <= PRELOAD_HISTORY_THRESHOLD &&
isScrollingTowardHistory &&
hasMoreHistory &&
!loadingMore &&
!loading &&
!isPreloadingRef.current
) {
isPreloadingRef.current = true;
loadMoreHistory().finally(() => {
if (preloadCooldownTimerRef.current) {
clearTimeout(preloadCooldownTimerRef.current);
}
preloadCooldownTimerRef.current = setTimeout(() => {
isPreloadingRef.current = false;
}, 350);
});
}
}, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge]);
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
handleMessageListContentSizeChange(contentWidth, contentHeight);
}, [handleMessageListContentSizeChange]);
return (
<KeyboardAvoidingView
@@ -262,34 +333,65 @@ export const ChatScreen: React.FC = () => {
/>
{/* 消息列表 */}
<TouchableWithoutFeedback onPress={handleDismiss}>
<View style={styles.messageListContainer}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
<View
style={styles.messageListContainer}
onLayout={e => {
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
}}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlatList
ref={flatListRef}
inverted
data={displayMessages}
renderItem={renderMessage}
keyExtractor={(item: any) => String(item.id)}
contentContainerStyle={listContentStyle as any}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
onScroll={handleMessageListScroll}
onScrollBeginDrag={() => {
isUserDraggingRef.current = true;
handleDismiss();
}}
onScrollEndDrag={() => {
isUserDraggingRef.current = false;
}}
onMomentumScrollEnd={() => {
isUserDraggingRef.current = false;
}}
scrollEventThrottle={16}
onContentSizeChange={handleContentSizeChange}
/>
)}
{loadingMore && (
<View
pointerEvents="none"
style={{
position: 'absolute',
top: 8,
left: 0,
right: 0,
alignItems: 'center',
}}
>
<View
style={{
backgroundColor: 'rgba(255,255,255,0.9)',
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,
}}
>
<ActivityIndicator size="small" color={colors.primary.main} />
</View>
) : (
<FlashList
ref={flatListRef}
data={messages}
renderItem={renderMessage}
keyExtractor={(item: any) => String(item.id)}
contentContainerStyle={listContentStyle as any}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
refreshing={loadingMore}
onRefresh={hasMoreHistory ? loadMoreHistory : undefined}
// @ts-ignore FlashList 类型定义问题estimatedItemSize 是必需属性但类型定义缺失
estimatedItemSize={80}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: undefined,
} as any}
onScroll={handleMessageListScroll}
/>
)}
</View>
</TouchableWithoutFeedback>
</View>
)}
</View>
{/* 底部区域:输入框 + 面板 */}
<View style={{ marginBottom: keyboardHeight > 0 ? keyboardHeight : 0 }}>