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

@@ -0,0 +1,5 @@
import { MessageSearchScreen } from '../../../src/screens/message';
export default function MessageSearchRoute() {
return <MessageSearchScreen />;
}

View File

@@ -5,9 +5,10 @@ interface HighlightTextProps {
text: string; text: string;
keyword: string; keyword: string;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
highlightStyle?: StyleProp<TextStyle>;
} }
const HighlightText: React.FC<HighlightTextProps> = ({ text, keyword, style }) => { const HighlightText: React.FC<HighlightTextProps> = ({ text, keyword, style, highlightStyle }) => {
const parts = useMemo(() => { const parts = useMemo(() => {
if (!text || !keyword) return [{ text, highlight: false }]; if (!text || !keyword) return [{ text, highlight: false }];
@@ -37,12 +38,10 @@ const HighlightText: React.FC<HighlightTextProps> = ({ text, keyword, style }) =
}, [text, keyword]); }, [text, keyword]);
return ( return (
<Text> <Text style={style}>
{parts.map((part, i) => {parts.map((part, i) =>
part.highlight ? ( part.highlight ? (
<Text key={i} style={style}> <Text key={i} style={highlightStyle}>{part.text}</Text>
{part.text}
</Text>
) : ( ) : (
<Text key={i}>{part.text}</Text> <Text key={i}>{part.text}</Text>
) )

View File

@@ -1,6 +1,7 @@
import { localDataSource } from '../LocalDataSource'; import { localDataSource } from '../LocalDataSource';
import { CachedMessage } from '../types'; import { CachedMessage } from '../types';
import type { ILocalDataSource } from '@/data/datasources/interfaces'; import type { ILocalDataSource } from '@/data/datasources/interfaces';
import { extractTextFromSegments } from '@/types/dto';
export interface IMessageRepository { export interface IMessageRepository {
saveMessage(message: CachedMessage): Promise<void>; saveMessage(message: CachedMessage): Promise<void>;
@@ -17,6 +18,7 @@ export interface IMessageRepository {
updateStatus(messageId: string, status: string, clearContent?: boolean): Promise<void>; updateStatus(messageId: string, status: string, clearContent?: boolean): Promise<void>;
clearConversation(conversationId: string): Promise<void>; clearConversation(conversationId: string): Promise<void>;
search(keyword: string): Promise<CachedMessage[]>; search(keyword: string): Promise<CachedMessage[]>;
searchByConversation(conversationId: string, keyword: string, limit?: number, offset?: number): Promise<CachedMessage[]>;
getStats(): Promise<{ totalMessages: number; totalConversations: number }>; getStats(): Promise<{ totalMessages: number; totalConversations: number }>;
getLastMessageByConversation(conversationId: string): Promise<CachedMessage | null>; getLastMessageByConversation(conversationId: string): Promise<CachedMessage | null>;
} }
@@ -143,6 +145,35 @@ export class MessageRepository implements IMessageRepository {
return rows.map(r => ({ ...r, isRead: r.isRead === 1, segments: r.segments ? JSON.parse(r.segments) : undefined })); return rows.map(r => ({ ...r, isRead: r.isRead === 1, segments: r.segments ? JSON.parse(r.segments) : undefined }));
} }
async searchByConversation(
conversationId: string,
keyword: string,
limit: number = 50,
offset: number = 0
): Promise<CachedMessage[]> {
const likeParam = `%${keyword}%`;
const rows = await this.dataSource.query<any>(
`SELECT * FROM messages
WHERE conversationId = ?
AND status != 'deleted'
AND (segments LIKE ? OR content LIKE ?)
ORDER BY createdAt DESC
LIMIT ? OFFSET ?`,
[conversationId, likeParam, likeParam, limit, offset]
);
const lowerKeyword = keyword.toLowerCase();
return rows
.map(r => ({
...r,
isRead: r.isRead === 1,
segments: r.segments ? JSON.parse(r.segments) : undefined,
}))
.filter(msg => {
const text = extractTextFromSegments(msg.segments).toLowerCase();
return text.includes(lowerKeyword);
});
}
async getStats(): Promise<{ totalMessages: number; totalConversations: number }> { async getStats(): Promise<{ totalMessages: number; totalConversations: number }> {
const msgCount = await this.dataSource.getFirst<{ count: number }>(`SELECT COUNT(*) as count FROM messages`); const msgCount = await this.dataSource.getFirst<{ count: number }>(`SELECT COUNT(*) as count FROM messages`);
const convCount = await this.dataSource.getFirst<{ count: number }>(`SELECT COUNT(*) as count FROM conversations`); const convCount = await this.dataSource.getFirst<{ count: number }>(`SELECT COUNT(*) as count FROM conversations`);

View File

@@ -87,14 +87,16 @@ export function hrefChat(params: {
groupId?: string; groupId?: string;
groupName?: string; groupName?: string;
groupAvatar?: string; groupAvatar?: string;
scrollToSeq?: number;
}): string { }): string {
const { conversationId, userId, isGroupChat, groupId, groupName, groupAvatar } = params; const { conversationId, userId, isGroupChat, groupId, groupName, groupAvatar, scrollToSeq } = params;
const q = new URLSearchParams(); const q = new URLSearchParams();
if (userId) q.set('userId', userId); if (userId) q.set('userId', userId);
if (isGroupChat) q.set('isGroupChat', '1'); if (isGroupChat) q.set('isGroupChat', '1');
if (groupId != null && groupId !== '') q.set('groupId', String(groupId)); if (groupId != null && groupId !== '') q.set('groupId', String(groupId));
if (groupName) q.set('groupName', groupName); if (groupName) q.set('groupName', groupName);
if (groupAvatar) q.set('groupAvatar', groupAvatar); if (groupAvatar) q.set('groupAvatar', groupAvatar);
if (scrollToSeq != null) q.set('scrollToSeq', String(scrollToSeq));
const qs = q.toString(); const qs = q.toString();
return `/chat/${encodeURIComponent(conversationId)}${qs ? `?${qs}` : ''}`; return `/chat/${encodeURIComponent(conversationId)}${qs ? `?${qs}` : ''}`;
} }
@@ -131,6 +133,19 @@ export function hrefGroupInviteDetail(message: SystemMessageResponse): string {
return `/group/invite?messageId=${encodeURIComponent(message.id)}`; return `/group/invite?messageId=${encodeURIComponent(message.id)}`;
} }
export function hrefMessageSearch(params: {
conversationId: string;
conversationName?: string;
isGroupChat?: boolean;
}): string {
const q = new URLSearchParams({
conversationId: params.conversationId,
});
if (params.conversationName) q.set('conversationName', params.conversationName);
if (params.isGroupChat) q.set('isGroupChat', '1');
return `/chat/message-search?${q.toString()}`;
}
export function hrefPrivateChatInfo(params: { export function hrefPrivateChatInfo(params: {
conversationId: string; conversationId: string;
userId: string; userId: string;

View File

@@ -837,6 +837,19 @@ const GroupInfoScreen: React.FC = () => {
thumbColor={isNotificationMuted ? colors.primary.main : colors.background.paper} thumbColor={isNotificationMuted ? colors.primary.main : colors.background.paper}
/> />
</View> </View>
<Divider style={styles.settingDivider} />
{renderSettingItem(
'magnify',
'查找聊天记录',
undefined,
conversationId
? () => router.push(hrefs.hrefMessageSearch({
conversationId,
conversationName: group?.name || '群聊',
isGroupChat: true,
}))
: undefined,
)}
</View> </View>
</View> </View>

View File

@@ -0,0 +1,347 @@
/**
* MessageSearchScreen 聊天记录搜索页
* 在单个会话中搜索消息记录,基于本地 SQLite 缓存
*/
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import {
spacing,
fontSizes,
borderRadius,
useAppColors,
type AppColors,
} from '../../theme';
import { CachedMessage } from '../../database/types';
import { extractTextFromSegments, UserDTO } from '../../types/dto';
import { messageRepository } from '../../database';
import { userManager } from '../../stores/user';
import { useAuthStore } from '../../stores';
import { Avatar, Text, EmptyState, AppBackButton } from '../../components/common';
import { SearchBar } from '../../components/business';
import HighlightText from '../../components/common/HighlightText';
import { formatTime } from '../../utils/formatTime';
import * as hrefs from '../../navigation/hrefs';
const PAGE_SIZE = 50;
interface SenderInfo {
nickname: string;
avatar?: string;
}
export const MessageSearchScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createMessageSearchStyles(colors), [colors]);
const router = useRouter();
const currentUserId = useAuthStore(state => state.currentUser?.id);
const raw = useLocalSearchParams<{
conversationId?: string;
conversationName?: string;
isGroupChat?: string;
}>();
const conversationId = raw.conversationId ?? '';
const conversationName = raw.conversationName ?? '聊天';
const isGroupChat = raw.isGroupChat === '1';
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState<CachedMessage[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [searched, setSearched] = useState(false);
const senderCache = useRef<Map<string, SenderInfo>>(new Map());
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const doSearch = useCallback(async (text: string, offset = 0) => {
if (!text.trim() || !conversationId) return;
setLoading(true);
try {
const msgs = await messageRepository.searchByConversation(
conversationId,
text.trim(),
PAGE_SIZE,
offset,
);
if (offset === 0) {
setResults(msgs);
} else {
setResults(prev => [...prev, ...msgs]);
}
setHasMore(msgs.length >= PAGE_SIZE);
setSearched(true);
// prefetch sender info (skip system sender 10000)
const senderIds = new Set(msgs.map(m => m.senderId).filter(id => id !== '10000'));
for (const sid of senderIds) {
if (!senderCache.current.has(sid)) {
try {
const u = await userManager.getUserById(sid);
if (u) {
senderCache.current.set(sid, {
nickname: u.nickname || u.username || sid,
avatar: u.avatar,
});
}
} catch {
senderCache.current.set(sid, { nickname: sid });
}
}
}
// system sender cache entry
senderCache.current.set('10000', { nickname: '系统通知' });
} catch (e) {
console.error('搜索消息失败:', e);
} finally {
setLoading(false);
}
}, [conversationId]);
const handleTextChanged = useCallback((text: string) => {
setKeyword(text);
if (debounceRef.current) clearTimeout(debounceRef.current);
if (!text.trim()) {
setResults([]);
setSearched(false);
setHasMore(false);
return;
}
debounceRef.current = setTimeout(() => {
doSearch(text, 0);
}, 300);
}, [doSearch]);
const handleSubmit = useCallback(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (keyword.trim()) {
doSearch(keyword, 0);
}
}, [keyword, doSearch]);
const handleEndReached = useCallback(() => {
if (!loading && hasMore && keyword.trim()) {
doSearch(keyword, results.length);
}
}, [loading, hasMore, keyword, results.length, doSearch]);
const handlePressResult = useCallback((msg: CachedMessage) => {
router.push(
hrefs.hrefChat({
conversationId,
isGroupChat,
scrollToSeq: msg.seq,
}) as any,
);
}, [router, conversationId, isGroupChat]);
useEffect(() => {
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, []);
const getSender = useCallback((senderId: string): SenderInfo => {
if (senderCache.current.has(senderId)) {
return senderCache.current.get(senderId)!;
}
return { nickname: senderId };
}, []);
const renderItem = useCallback(({ item }: { item: CachedMessage }) => {
const sender = getSender(item.senderId);
const isMe = item.senderId === currentUserId;
const displayText = extractTextFromSegments(item.segments) || item.content || '';
return (
<TouchableOpacity
style={styles.resultItem}
onPress={() => handlePressResult(item)}
activeOpacity={0.6}
>
<Avatar
source={sender.avatar ? { uri: sender.avatar } : undefined}
name={sender.nickname}
size={36}
/>
<View style={styles.resultContent}>
<View style={styles.resultHeader}>
<Text style={styles.senderName} numberOfLines={1}>
{isMe ? '我' : sender.nickname}
</Text>
<Text style={styles.resultTime}>
{formatTime(item.createdAt)}
</Text>
</View>
<HighlightText
text={displayText}
keyword={keyword}
style={styles.resultText}
highlightStyle={styles.highlightText}
/>
</View>
</TouchableOpacity>
);
}, [styles, keyword, currentUserId, getSender, handlePressResult]);
const ListEmptyComponent = useMemo(() => {
if (loading) {
return (
<View style={styles.emptyContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
);
}
if (!searched) {
return (
<View style={styles.emptyContainer}>
<EmptyState
icon="magnify"
title="搜索聊天记录"
description="输入关键词搜索聊天记录"
/>
</View>
);
}
return (
<View style={styles.emptyContainer}>
<EmptyState
icon="message-off-outline"
title="未找到相关消息"
description="换个关键词试试"
/>
</View>
);
}, [loading, searched, styles, colors]);
const ListFooterComponent = useMemo(() => {
if (loading && results.length > 0) {
return (
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary.main} />
</View>
);
}
return null;
}, [loading, results.length, styles, colors]);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<View style={styles.header}>
<AppBackButton onPress={() => router.back()} />
<Text style={styles.headerTitle} numberOfLines={1}>
{conversationName}
</Text>
<View style={styles.headerSpacer} />
</View>
<View style={styles.searchWrap}>
<SearchBar
value={keyword}
onChangeText={handleTextChanged}
onSubmit={handleSubmit}
placeholder="搜索聊天记录"
autoFocus
/>
</View>
<FlashList
data={results}
renderItem={renderItem}
keyExtractor={item => item.id}
onEndReached={handleEndReached}
onEndReachedThreshold={0.3}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
keyboardShouldPersistTaps="handled"
/>
</SafeAreaView>
);
};
function createMessageSearchStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
},
headerTitle: {
flex: 1,
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
textAlign: 'center',
marginHorizontal: spacing.sm,
},
headerSpacer: {
width: 40,
},
searchWrap: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
},
resultItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
resultContent: {
flex: 1,
marginLeft: spacing.md,
},
resultHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 2,
},
senderName: {
fontSize: fontSizes.sm,
fontWeight: '500',
color: colors.text.primary,
flex: 1,
marginRight: spacing.sm,
},
resultTime: {
fontSize: fontSizes.xs,
color: colors.text.hint,
},
resultText: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
lineHeight: fontSizes.sm * 1.4,
},
highlightText: {
color: colors.primary.main,
fontWeight: '600',
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingTop: spacing.xl * 3,
},
footerLoader: {
paddingVertical: spacing.md,
alignItems: 'center',
},
});
}

View File

@@ -145,8 +145,11 @@ const PrivateChatInfoScreen: React.FC = () => {
// 查找聊天记录 // 查找聊天记录
const handleSearchMessages = () => { const handleSearchMessages = () => {
// TODO: 实现聊天记录搜索功能 router.push(hrefs.hrefMessageSearch({
Alert.alert('提示', '聊天记录搜索功能开发中'); conversationId,
conversationName: userName || user?.nickname || '私聊',
isGroupChat: false,
}));
}; };
// 查看用户资料 // 查看用户资料

View File

@@ -33,6 +33,7 @@ export interface ChatRouteParams {
isGroupChat?: boolean; isGroupChat?: boolean;
groupId?: string; groupId?: string;
groupName?: string; groupName?: string;
scrollToSeq?: number;
} }
// 发送者信息 // 发送者信息

View File

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

View File

@@ -12,3 +12,4 @@ export { default as PrivateChatInfoScreen } from './PrivateChatInfoScreen';
export { default as JoinGroupScreen } from './JoinGroupScreen'; export { default as JoinGroupScreen } from './JoinGroupScreen';
export { default as GroupRequestDetailScreen } from './GroupRequestDetailScreen'; export { default as GroupRequestDetailScreen } from './GroupRequestDetailScreen';
export { default as GroupInviteDetailScreen } from './GroupInviteDetailScreen'; export { default as GroupInviteDetailScreen } from './GroupInviteDetailScreen';
export { MessageSearchScreen } from './MessageSearchScreen';