feat(search): add keyword highlighting to PostCard and improve QRCodeScanner
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 4m39s
Frontend CI / ota-android (push) Successful in 11m22s
Frontend CI / build-android-apk (push) Failing after 14m58s

Add HighlightText component for search result highlighting in PostCard.
Refactor QRCodeScanner to use async permission methods with better error
handling. Improve SearchScreen with stable function refs and pass
highlightKeyword to PostCard. Modernize MessageListScreen search UI with
SearchBar and TabBar components. Add emoji virtualization to EmojiPanel
and auto-focus input after inserting emoji.

BREAKING CHANGE: EmojiPanel onFocusInput prop added for focus management
This commit is contained in:
lafay
2026-04-21 12:31:51 +08:00
parent b16759a147
commit 30c493c88f
9 changed files with 243 additions and 134 deletions

View File

@@ -20,7 +20,6 @@ import {
RefreshControl,
ActivityIndicator,
Animated,
TextInput,
Keyboard,
BackHandler,
Platform,
@@ -60,6 +59,7 @@ import { ConversationListRow } from './components/ConversationListRow';
// 导入 NotificationsScreen 用于在内部显示
import { NotificationsScreen } from './NotificationsScreen';
// 导入扫码组件
import { SearchBar, TabBar } from '../../components/business';
import { QRCodeScanner } from '../../components/business/QRCodeScanner';
// 系统通知会话特殊ID
@@ -673,49 +673,40 @@ export const MessageListScreen: React.FC = () => {
return null;
};
// 渲染搜索模式UI
// 渲染搜索模式UI(扁平化现代化设计)
const renderSearchMode = () => (
<View style={styles.searchModeContainer}>
<View style={styles.searchInputContainer}>
<AppBackButton onPress={handleCloseSearch} iconColor={colors.text.secondary} />
<TextInput
style={styles.searchInput}
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
placeholderTextColor={colors.text.hint}
value={searchText}
onChangeText={setSearchText}
autoFocus
returnKeyType="search"
// 确保光标可见
cursorColor={colors.primary.main}
selectionColor={`${colors.primary.main}40`}
{/* 搜索头部 */}
<View style={[styles.searchHeader, { paddingTop: Math.max(spacing.sm, insets.top + spacing.xs) }]}>
<View style={styles.searchShell}>
<SearchBar
value={searchText}
onChangeText={setSearchText}
onSubmit={() => performSearch(searchText)}
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
autoFocus
/>
</View>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleCloseSearch}
activeOpacity={0.7}
>
<Text style={styles.cancelText}></Text>
</TouchableOpacity>
</View>
{/* Tab切换 - 现代化风格 */}
<View style={styles.searchTabContainer}>
<TabBar
tabs={['聊天记录', '用户']}
activeIndex={activeSearchTab === 'chat' ? 0 : 1}
onTabChange={(index) => setActiveSearchTab(index === 0 ? 'chat' : 'user')}
variant="modern"
/>
{searchText.length > 0 && (
<TouchableOpacity onPress={() => setSearchText('')}>
<MaterialCommunityIcons name="close-circle" size={18} color={colors.text.hint} />
</TouchableOpacity>
)}
</View>
<View style={styles.searchTabs}>
<TouchableOpacity
style={[styles.searchTab, activeSearchTab === 'chat' && styles.searchTabActive]}
onPress={() => setActiveSearchTab('chat')}
>
<Text style={activeSearchTab === 'chat' ? styles.searchTabTextActive : styles.searchTabText}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.searchTab, activeSearchTab === 'user' && styles.searchTabActive]}
onPress={() => setActiveSearchTab('user')}
>
<Text style={activeSearchTab === 'user' ? styles.searchTabTextActive : styles.searchTabText}>
</Text>
</TouchableOpacity>
</View>
{/* 搜索结果 */}
<FlatList
data={searchResults}
renderItem={renderSearchResult}
@@ -725,7 +716,10 @@ export const MessageListScreen: React.FC = () => {
}
return `user-${item.user?.id || index}`;
}}
contentContainerStyle={[styles.searchResultsContent, { paddingBottom: listBottomInset }]}
contentContainerStyle={[
styles.searchResultsContent,
{ paddingBottom: listBottomInset },
]}
showsVerticalScrollIndicator={false}
scrollEnabled={true}
keyboardShouldPersistTaps="handled"
@@ -757,14 +751,18 @@ export const MessageListScreen: React.FC = () => {
</View>
</View>
<TouchableOpacity
style={[styles.searchContainer, ...(isWideScreen ? [styles.searchContainerWide] : [])]}
onPress={handleSearch}
activeOpacity={0.7}
<TouchableOpacity
style={[styles.searchContainer, ...(isWideScreen ? [styles.searchContainerWide] : [])]}
onPress={handleSearch}
activeOpacity={0.85}
>
<View style={styles.searchBox}>
<MaterialCommunityIcons name="magnify" size={18} color={colors.text.hint} />
<Text style={styles.searchPlaceholder}></Text>
<View pointerEvents="none">
<SearchBar
value=""
onChangeText={() => {}}
onSubmit={() => {}}
placeholder="搜索"
/>
</View>
</TouchableOpacity>
@@ -1026,19 +1024,7 @@ function createMessageListStyles(colors: AppColors) {
paddingBottom: spacing.sm,
backgroundColor: colors.background.default,
},
searchBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 10,
paddingHorizontal: spacing.sm,
paddingVertical: 10,
},
searchPlaceholder: {
fontSize: 14,
color: colors.text.hint,
marginLeft: spacing.xs,
},
listContent: {
flexGrow: 1,
backgroundColor: colors.background.default,
@@ -1062,60 +1048,49 @@ function createMessageListStyles(colors: AppColors) {
},
searchModeContainer: {
flex: 1,
backgroundColor: colors.background.default,
backgroundColor: colors.background.paper,
},
searchInputContainer: {
searchHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 10,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}70`,
paddingHorizontal: spacing.md,
marginHorizontal: spacing.md,
marginTop: spacing.md,
height: 40,
paddingBottom: spacing.sm,
},
searchInput: {
searchShell: {
flex: 1,
fontSize: 15,
color: colors.text.primary,
marginLeft: spacing.md,
},
searchTabs: {
flexDirection: 'row',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
gap: spacing.sm,
cancelButton: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
marginLeft: spacing.sm,
},
searchTab: {
flex: 1,
paddingVertical: spacing.sm,
alignItems: 'center',
backgroundColor: colors.chat.surfaceInput,
borderRadius: 8,
cancelText: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.primary.main,
},
searchTabActive: {
backgroundColor: colors.primary.main,
},
searchTabText: {
fontSize: 14,
color: colors.text.secondary,
fontWeight: '500',
},
searchTabTextActive: {
color: colors.primary.contrast,
searchTabContainer: {
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}50`,
},
searchResultsContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
paddingTop: spacing.sm,
},
searchResultItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
padding: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 10,
borderRadius: borderRadius.lg,
marginBottom: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: StyleSheet.hairlineWidth,
borderColor: `${colors.divider}50`,
},
searchResultContent: {
flex: 1,