feat(search): add keyword highlighting to PostCard and improve QRCodeScanner
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:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, ScrollView, Platform } from 'react-native';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, Platform } from 'react-native';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
@@ -17,7 +17,7 @@ import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUr
|
||||
import { useAppColors, spacing } from '../../../../theme';
|
||||
import { blurActiveElement } from '../../../../infrastructure/platform';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
// 表情尺寸配置 - 固定宽度 40px,使用 flex 布局
|
||||
const EMOJI_SIZES = {
|
||||
@@ -35,12 +35,14 @@ interface EmojiPanelProps {
|
||||
onInsertEmoji: (emoji: string) => void;
|
||||
onInsertSticker: (stickerUrl: string) => void;
|
||||
onClose: () => void;
|
||||
onFocusInput?: () => void;
|
||||
}
|
||||
|
||||
export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
onInsertEmoji,
|
||||
onInsertSticker,
|
||||
onClose,
|
||||
onFocusInput,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const baseStyles = useChatScreenStyles();
|
||||
@@ -88,6 +90,25 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
});
|
||||
}, [emojiSize, baseStyles]);
|
||||
|
||||
// 每行显示的 emoji 数量(基于 40px 宽度 + 20px margin)
|
||||
const emojisPerRow = useMemo(() => {
|
||||
const itemTotalWidth = 40 + 20; // width + margin*2
|
||||
const containerWidth = SCREEN_WIDTH - spacing.sm * 2;
|
||||
return Math.floor(containerWidth / itemTotalWidth);
|
||||
}, []);
|
||||
|
||||
// 将 EMOJIS 分组成行数据,用于 FlatList 虚拟化渲染
|
||||
const emojiRows = useMemo(() => {
|
||||
const rows: { id: string; emojis: string[] }[] = [];
|
||||
for (let i = 0; i < EMOJIS.length; i += emojisPerRow) {
|
||||
rows.push({
|
||||
id: `emoji-row-${i}`,
|
||||
emojis: EMOJIS.slice(i, i + emojisPerRow),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}, [emojisPerRow]);
|
||||
|
||||
// 加载自定义表情
|
||||
const loadStickers = useCallback(async () => {
|
||||
setLoadingStickers(true);
|
||||
@@ -110,24 +131,55 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
|
||||
|
||||
|
||||
// 渲染 Emoji 面板(使用 flex 布局,每个表情固定 40px 宽度)
|
||||
const renderEmojiPanel = () => (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.emojiContainer}
|
||||
showsVerticalScrollIndicator={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{EMOJIS.map((emoji, index) => (
|
||||
// 处理 emoji 点击:插入后自动聚焦输入框
|
||||
const handleEmojiPress = useCallback((emoji: string) => {
|
||||
onInsertEmoji(emoji);
|
||||
// 使用 requestAnimationFrame 确保在 UI 更新后聚焦,减少卡顿感
|
||||
if (Platform.OS === 'web') {
|
||||
onFocusInput?.();
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
onFocusInput?.();
|
||||
});
|
||||
}
|
||||
}, [onInsertEmoji, onFocusInput]);
|
||||
|
||||
// 渲染单行 emoji
|
||||
const renderEmojiRow = useCallback(({ item }: { item: { id: string; emojis: string[] } }) => (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{item.emojis.map((emoji, index) => (
|
||||
<TouchableOpacity
|
||||
key={`${emoji}-${index}`}
|
||||
key={`${item.id}-${index}`}
|
||||
style={styles.emojiItem}
|
||||
onPress={() => onInsertEmoji(emoji)}
|
||||
onPress={() => handleEmojiPress(emoji)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.emojiText}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
|
||||
|
||||
// 渲染 Emoji 面板(使用 FlatList 虚拟化,只渲染可见行)
|
||||
const renderEmojiPanel = () => (
|
||||
<FlatList
|
||||
key="emoji-flatlist"
|
||||
data={emojiRows}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderEmojiRow}
|
||||
contentContainerStyle={styles.emojiContainer}
|
||||
showsVerticalScrollIndicator={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={12}
|
||||
maxToRenderPerBatch={12}
|
||||
windowSize={5}
|
||||
removeClippedSubviews={true}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: 60,
|
||||
offset: 60 * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
// 添加表情(从相册选择)
|
||||
@@ -296,6 +348,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
key="sticker-flatlist"
|
||||
data={stickerListData}
|
||||
numColumns={4}
|
||||
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
|
||||
|
||||
Reference in New Issue
Block a user