refactor(LocalDataSource, PostRepository, ChatScreen): enhance database handling and message loading
- 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:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user