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:
@@ -114,10 +114,15 @@ export const useChatScreen = () => {
|
||||
const flatListRef = useRef<any>(null);
|
||||
const textInputRef = useRef<any>(null);
|
||||
|
||||
// 防止重复加载的 ref
|
||||
const shouldAutoScrollOnEnterRef = useRef(true);
|
||||
const autoScrollTimersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
const scrollPositionRef = useRef({ contentHeight: 0, scrollY: 0 });
|
||||
// 滚动状态机 refs(Telegram/Element 风格:底部粘附 + 阅读锚点)
|
||||
const scrollPositionRef = useRef({ contentHeight: 0, scrollY: 0, viewportHeight: 0 });
|
||||
const hasInitialAnchorDoneRef = useRef(false);
|
||||
const prevMessageCountRef = useRef(0);
|
||||
const prevLatestSeqRef = useRef(0);
|
||||
const prevMarkedReadSeqRef = useRef(0);
|
||||
const isProgrammaticScrollRef = useRef(false);
|
||||
const suppressAutoFollowRef = useRef(false);
|
||||
const isBrowsingHistoryRef = useRef(false);
|
||||
|
||||
// 回复消息状态
|
||||
const [replyingTo, setReplyingTo] = useState<GroupMessage | null>(null);
|
||||
@@ -196,10 +201,13 @@ export const useChatScreen = () => {
|
||||
return '对方未关注你前:仅可发送1条文字消息,且不能发送图片';
|
||||
}, [followRestricted, myPrivateSentCount]);
|
||||
|
||||
// 【改造】同步加载状态
|
||||
// 加载态语义修正:
|
||||
// isLoadingMessages 在分页加载历史时也会短暂为 true。
|
||||
// 若直接驱动 ChatScreen 的 loading,会导致消息列表被卸载重挂载,触发“回到底部”。
|
||||
// 这里只在“首屏且尚无消息”时展示 loading 占位。
|
||||
useEffect(() => {
|
||||
setLoading(isLoadingMessages);
|
||||
}, [isLoadingMessages]);
|
||||
setLoading(isLoadingMessages && messageManagerMessages.length === 0);
|
||||
}, [isLoadingMessages, messageManagerMessages.length]);
|
||||
|
||||
// 【改造】同步 hasMore 状态
|
||||
useEffect(() => {
|
||||
@@ -263,32 +271,73 @@ export const useChatScreen = () => {
|
||||
};
|
||||
}, [isGroupChat, otherUserId]);
|
||||
|
||||
// 进入新会话时重置首次自动滚动标记
|
||||
// 进入新会话时重置滚动状态
|
||||
useEffect(() => {
|
||||
shouldAutoScrollOnEnterRef.current = true;
|
||||
autoScrollTimersRef.current.forEach(clearTimeout);
|
||||
autoScrollTimersRef.current = [];
|
||||
hasInitialAnchorDoneRef.current = false;
|
||||
prevMessageCountRef.current = 0;
|
||||
prevLatestSeqRef.current = 0;
|
||||
prevMarkedReadSeqRef.current = 0;
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
}, [conversationId]);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
autoScrollTimersRef.current.forEach(clearTimeout);
|
||||
autoScrollTimersRef.current = [];
|
||||
};
|
||||
}, []);
|
||||
const scrollToLatest = useCallback((animated: boolean, force: boolean = false, reason: string = 'unknown') => {
|
||||
if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current)) return;
|
||||
// inverted 列表下,最新消息端对应 offset=0
|
||||
isProgrammaticScrollRef.current = true;
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: 0,
|
||||
animated,
|
||||
});
|
||||
setTimeout(() => {
|
||||
isProgrammaticScrollRef.current = false;
|
||||
}, animated ? 220 : 32);
|
||||
}, [flatListRef, scrollPositionRef, messages.length]);
|
||||
|
||||
// 消息加载完成后滚动到底部
|
||||
const isNearBottom = useCallback(() => {
|
||||
const scrollY = scrollPositionRef.current.scrollY || 0;
|
||||
// inverted 列表下,越接近 0 越靠近最新消息端
|
||||
return scrollY <= 100;
|
||||
}, [scrollPositionRef]);
|
||||
|
||||
// 首屏加载完成后仅锚定一次到最新消息端
|
||||
useEffect(() => {
|
||||
if (!loading && !loadingMore && messages.length > 0 && shouldAutoScrollOnEnterRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
if (shouldAutoScrollOnEnterRef.current) {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
}
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
if (
|
||||
loading ||
|
||||
loadingMore ||
|
||||
messages.length === 0 ||
|
||||
hasInitialAnchorDoneRef.current ||
|
||||
scrollPositionRef.current.viewportHeight <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}, [loading, loadingMore, messages.length]);
|
||||
hasInitialAnchorDoneRef.current = true;
|
||||
const timer = setTimeout(() => {
|
||||
scrollToLatest(false, true, 'initial-anchor');
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}, [loading, loadingMore, messages.length, scrollToLatest]);
|
||||
|
||||
// 新消息跟随策略(Telegram/QQ 风格):
|
||||
// 仅当“最新端消息 seq 增长”且用户在底部附近时才跟随;
|
||||
// 历史加载只会增加旧消息,不会提升 latest seq,因此不会触发回底。
|
||||
useEffect(() => {
|
||||
const currentCount = messages.length;
|
||||
const latestSeq = currentCount > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0;
|
||||
const prevLatestSeq = prevLatestSeqRef.current;
|
||||
prevMessageCountRef.current = currentCount;
|
||||
prevLatestSeqRef.current = latestSeq;
|
||||
|
||||
if (loading || loadingMore) return;
|
||||
if (latestSeq <= prevLatestSeq) return;
|
||||
if (suppressAutoFollowRef.current) return;
|
||||
if (!isNearBottom()) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
scrollToLatest(false, false, 'new-message-follow');
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}, [messages, loading, loadingMore, isNearBottom, scrollToLatest]);
|
||||
|
||||
// 获取当前用户信息
|
||||
useEffect(() => {
|
||||
@@ -384,22 +433,18 @@ export const useChatScreen = () => {
|
||||
};
|
||||
}, [routeUserId, conversationId, currentUserId, isGroupChat]);
|
||||
|
||||
// 【改造】加载更多历史消息
|
||||
// 加载更多历史消息(inverted 下保持阅读锚点)
|
||||
const loadMoreHistory = useCallback(async () => {
|
||||
if (!conversationId || !hasMoreHistory || loadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用自动滚动到底部,防止加载历史消息后滚动位置跳转
|
||||
shouldAutoScrollOnEnterRef.current = false;
|
||||
// 清除所有待执行的自动滚动定时器
|
||||
autoScrollTimersRef.current.forEach(clearTimeout);
|
||||
autoScrollTimersRef.current = [];
|
||||
// 历史加载期间禁止“新消息自动跟随到底”
|
||||
suppressAutoFollowRef.current = true;
|
||||
|
||||
// 保存加载前的滚动位置和内容高度
|
||||
const scrollYBefore = scrollPositionRef.current.scrollY;
|
||||
const contentHeightBefore = scrollPositionRef.current.contentHeight;
|
||||
|
||||
setLoadingMore(true);
|
||||
|
||||
try {
|
||||
@@ -411,22 +456,8 @@ export const useChatScreen = () => {
|
||||
setFirstSeq(minSeq);
|
||||
}
|
||||
|
||||
// 加载完成后,恢复滚动位置
|
||||
// 使用 setTimeout 确保 FlashList 已经更新
|
||||
setTimeout(() => {
|
||||
if (flatListRef.current) {
|
||||
// 计算新的滚动位置,保持相对位置不变
|
||||
const newContentHeight = scrollPositionRef.current.contentHeight;
|
||||
const heightDiff = newContentHeight - contentHeightBefore;
|
||||
const newScrollY = scrollYBefore + heightDiff;
|
||||
|
||||
// 滚动到计算后的位置
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: newScrollY,
|
||||
animated: false,
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
// inverted + maintainVisibleContentPosition 下由列表原生保持位置
|
||||
// 不做手动 scrollToOffset,避免与原生锚点冲突导致回到底部
|
||||
} catch (error) {
|
||||
console.error('加载历史消息失败:', error);
|
||||
} finally {
|
||||
@@ -434,48 +465,44 @@ export const useChatScreen = () => {
|
||||
}
|
||||
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
|
||||
|
||||
// 列表内容尺寸变化后触发首次自动滚动
|
||||
// 列表内容尺寸变化:仅同步内容高度,锚点补偿由 loadMoreHistory 统一处理
|
||||
const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||||
// 如果是加载更多历史消息,不要自动滚动
|
||||
if (loadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldAutoScrollOnEnterRef.current) return;
|
||||
if (loading) return;
|
||||
if (messages.length === 0) return;
|
||||
|
||||
const prevContentHeight = scrollPositionRef.current.contentHeight;
|
||||
scrollPositionRef.current.contentHeight = contentHeight;
|
||||
}, []);
|
||||
|
||||
// 如果内容高度增加(加载了更多消息),不要自动滚动到底部
|
||||
if (prevContentHeight > 0 && contentHeight > prevContentHeight) {
|
||||
return;
|
||||
}
|
||||
const handleReachLatestEdge = useCallback(() => {
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
}, []);
|
||||
|
||||
shouldAutoScrollOnEnterRef.current = false;
|
||||
autoScrollTimersRef.current.forEach(clearTimeout);
|
||||
autoScrollTimersRef.current = [];
|
||||
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||
isBrowsingHistoryRef.current = browsing;
|
||||
}, []);
|
||||
|
||||
[100, 300, 500, 800, 1200].forEach(delay => {
|
||||
const timer = setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
}, delay);
|
||||
autoScrollTimersRef.current.push(timer);
|
||||
});
|
||||
}, [loading, loadingMore, messages.length]);
|
||||
|
||||
// 【改造】自动标记已读 - 当有新消息且是当前会话时
|
||||
// 自动标记已读(QQ/Telegram 风格):
|
||||
// 仅当出现更大的 latest seq,且用户在最新端附近时才上报。
|
||||
// 历史加载/浏览历史不会触发 read。
|
||||
useEffect(() => {
|
||||
if (!conversationId || messages.length === 0) return;
|
||||
if (loading || loadingMore) return;
|
||||
if (suppressAutoFollowRef.current || isBrowsingHistoryRef.current) return;
|
||||
if (!isNearBottom()) return;
|
||||
|
||||
const maxSeq = Math.max(...messages.map(m => m.seq), 0);
|
||||
if (maxSeq > 0) {
|
||||
markAsRead(maxSeq).catch(error => {
|
||||
console.error('[ChatScreen] 自动标记已读失败:', error);
|
||||
});
|
||||
}
|
||||
}, [messages, conversationId, markAsRead]);
|
||||
const latestSeq = Math.max(...messages.map(m => m.seq), 0);
|
||||
if (latestSeq <= 0) return;
|
||||
|
||||
// 没有新增最新消息,不重复上报
|
||||
if (latestSeq <= prevLatestSeqRef.current) return;
|
||||
prevLatestSeqRef.current = latestSeq;
|
||||
|
||||
// 避免对同一 seq 重复标记
|
||||
if (latestSeq <= prevMarkedReadSeqRef.current) return;
|
||||
prevMarkedReadSeqRef.current = latestSeq;
|
||||
|
||||
markAsRead(latestSeq).catch(error => {
|
||||
console.error('[ChatScreen] 自动标记已读失败:', error);
|
||||
});
|
||||
}, [messages, conversationId, markAsRead, loading, loadingMore, isNearBottom]);
|
||||
|
||||
// 使用 ref 存储 groupMembers
|
||||
const groupMembersRef = useRef(groupMembers);
|
||||
@@ -494,16 +521,20 @@ export const useChatScreen = () => {
|
||||
const keyboardWillShow = (e: KeyboardEvent) => {
|
||||
setKeyboardHeight(e.endCoordinates.height);
|
||||
setActivePanel(prev => prev === 'mention' ? 'mention' : 'none');
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
}, 150);
|
||||
if (isNearBottom()) {
|
||||
setTimeout(() => {
|
||||
scrollToLatest(false, false, 'keyboard-show');
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
const keyboardWillHide = () => {
|
||||
setKeyboardHeight(0);
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
}, 100);
|
||||
if (isNearBottom()) {
|
||||
setTimeout(() => {
|
||||
scrollToLatest(false, false, 'keyboard-hide');
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const showSubscription = Keyboard.addListener(
|
||||
@@ -519,7 +550,7 @@ export const useChatScreen = () => {
|
||||
showSubscription.remove();
|
||||
hideSubscription.remove();
|
||||
};
|
||||
}, []);
|
||||
}, [scrollToLatest, isNearBottom]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = useCallback((dateString: string): string => {
|
||||
@@ -773,7 +804,7 @@ export const useChatScreen = () => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: true });
|
||||
scrollToLatest(false, false, 'send-text');
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
@@ -781,7 +812,7 @@ export const useChatScreen = () => {
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}, [inputText, conversationId, isGroupChat, routeGroupId, selectedMentions, mentionAll, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText]);
|
||||
}, [inputText, conversationId, isGroupChat, routeGroupId, selectedMentions, mentionAll, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText, scrollToLatest]);
|
||||
|
||||
// 发送图片消息
|
||||
const handleSendImage = useCallback(async (imageUri: string, mimeType?: string) => {
|
||||
@@ -823,7 +854,7 @@ export const useChatScreen = () => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: true });
|
||||
scrollToLatest(false, false, 'send-image');
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('发送图片失败:', error);
|
||||
@@ -831,7 +862,7 @@ export const useChatScreen = () => {
|
||||
} finally {
|
||||
setSendingImage(false);
|
||||
}
|
||||
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, getSendErrorMessage, canSendPrivateImage]);
|
||||
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, getSendErrorMessage, canSendPrivateImage, scrollToLatest]);
|
||||
|
||||
// 选择图片
|
||||
const handlePickImage = useCallback(async () => {
|
||||
@@ -950,7 +981,7 @@ export const useChatScreen = () => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: true });
|
||||
scrollToLatest(false, false, 'send-sticker');
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('发送自定义表情失败:', error);
|
||||
@@ -958,35 +989,35 @@ export const useChatScreen = () => {
|
||||
} finally {
|
||||
setSendingImage(false);
|
||||
}
|
||||
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, canSendPrivateImage]);
|
||||
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, canSendPrivateImage, scrollToLatest]);
|
||||
|
||||
// 切换表情面板
|
||||
const toggleEmojiPanel = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
setActivePanel(prev => {
|
||||
const newPanel = prev === 'emoji' ? 'none' : 'emoji';
|
||||
if (newPanel !== 'none') {
|
||||
if (newPanel !== 'none' && isNearBottom()) {
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
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') {
|
||||
if (newPanel !== 'none' && isNearBottom()) {
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: false });
|
||||
scrollToLatest(false, false, 'open-more-panel');
|
||||
}, 200);
|
||||
}
|
||||
return newPanel;
|
||||
});
|
||||
}, []);
|
||||
}, [scrollToLatest, isNearBottom]);
|
||||
|
||||
// 关闭面板
|
||||
const closePanel = useCallback(() => {
|
||||
@@ -1245,5 +1276,7 @@ export const useChatScreen = () => {
|
||||
loadMoreHistory,
|
||||
handleClearConversation,
|
||||
handleMessageListContentSizeChange,
|
||||
handleReachLatestEdge,
|
||||
setBrowsingHistory,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user