feat(message): implement message search functionality
Some checks failed
Frontend CI / ota-android (push) Successful in 1m19s
Frontend CI / ota-ios (push) Successful in 1m50s
Frontend CI / build-and-push-web (push) Successful in 3m28s
Frontend CI / build-android-apk (push) Failing after 8m56s

Add ability to search messages within a specific conversation. This includes:
- New `MessageSearchScreen` for displaying search results.
- `MessageRepository.searchByConversation` to query messages by keyword and conversation ID.
- Integration of search entry points in `GroupInfoScreen` and `PrivateChatInfoScreen`.
- Support for scrolling to a specific message sequence (`scrollToSeq`) when navigating from search results.
- Enhanced `HighlightText` component to support custom highlight styles.

feat(message): implement message search functionality
This commit is contained in:
2026-05-14 02:26:32 +08:00
parent ea2cf7bcff
commit d8d2b03f94
10 changed files with 462 additions and 10 deletions

View File

@@ -97,6 +97,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
groupId?: string | string[];
groupName?: string | string[];
groupAvatar?: string | string[];
scrollToSeq?: string | string[];
}>();
// 路由参数(动态段 + query群 ID 勿用 Number(),避免超过 2^53-1 时精度丢失
// 嵌入式模式下优先使用 props否则从路由参数获取
@@ -112,6 +113,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
};
}
const isGroupFlag = firstRouteParam(rawParams.isGroupChat);
const scrollToSeqParam = firstRouteParam(rawParams.scrollToSeq);
return {
conversationId: firstRouteParam(rawParams.conversationId) ?? null,
userId: firstRouteParam(rawParams.userId) ?? null,
@@ -119,6 +121,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
groupId: firstRouteParam(rawParams.groupId),
groupName: firstRouteParam(rawParams.groupName),
groupAvatar: firstRouteParam(rawParams.groupAvatar),
scrollToSeq: scrollToSeqParam ? Number(scrollToSeqParam) : null,
};
}, [
props?.embeddedConversationId,
@@ -131,9 +134,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
rawParams.groupId,
rawParams.groupName,
rawParams.groupAvatar,
rawParams.scrollToSeq,
]);
const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName, groupAvatar: routeGroupAvatar } = routeParams;
const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName, groupAvatar: routeGroupAvatar, scrollToSeq: routeScrollToSeq } = routeParams;
// 当前会话ID可能在新建私聊会话后动态更新
const [conversationId, setConversationId] = useState<string | null>(routeConversationId);
@@ -200,6 +204,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
const isBrowsingHistoryRef = useRef(false);
const hasShownMessageListRef = useRef(false);
const historyLoadingLockUntilRef = useRef(0);
const scrollToSeqRef = useRef<number | null>(null);
// 回复消息状态
const [replyingTo, setReplyingTo] = useState<GroupMessage | null>(null);
@@ -377,6 +382,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
isBrowsingHistoryRef.current = false;
hasShownMessageListRef.current = false;
historyLoadingLockUntilRef.current = 0;
scrollToSeqRef.current = routeScrollToSeq ?? null;
}, [conversationId]);
useEffect(() => {
@@ -415,7 +421,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
return scrollY <= 100;
}, [scrollPositionRef]);
// 首屏加载完成后仅锚定一次到最新消息端
// 首屏加载完成后仅锚定一次到最新消息端(有 scrollToSeq 时跳过)
useEffect(() => {
if (
loading ||
@@ -427,6 +433,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
return;
}
hasInitialAnchorDoneRef.current = true;
if (scrollToSeqRef.current != null) return; // 由下方 scrollToSeq effect 处理
const timer = setTimeout(() => {
scrollToLatest(false, true, 'initial-anchor');
}, 0);
@@ -606,6 +613,36 @@ export const useChatScreen = (props?: ChatScreenProps) => {
}
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
// 从搜索结果跳转:滚动到目标 seq
useEffect(() => {
const targetSeq = scrollToSeqRef.current;
if (targetSeq == null || !hasInitialAnchorDoneRef.current) return;
if (loading || loadingMore) return;
const found = messages.find(m => m.seq === targetSeq);
if (found) {
scrollToSeqRef.current = null;
isBrowsingHistoryRef.current = true;
suppressAutoFollowRef.current = true;
const ascendingIndex = messages.findIndex(m => m.seq === targetSeq);
const displayIndex = messages.length - 1 - ascendingIndex;
setTimeout(() => {
flatListRef.current?.scrollToIndex({
index: displayIndex,
animated: false,
viewPosition: 0.5,
});
}, 50);
return;
}
if (hasMoreHistory) {
loadMoreHistory();
} else {
scrollToSeqRef.current = null;
}
}, [loading, loadingMore, messages, hasMoreHistory, loadMoreHistory]);
// 列表内容尺寸变化:仅同步内容高度,锚点补偿由 loadMoreHistory 统一处理
const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
scrollPositionRef.current.contentHeight = contentHeight;