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:
@@ -18,6 +18,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { PostCardProps, PostCardAction } from './types';
|
||||
import { ImageGridItem, SmartImage } from '../../common';
|
||||
import Text from '../../common/Text';
|
||||
import HighlightText from '../../common/HighlightText';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../../theme';
|
||||
import { getPreviewImageUrl } from '../../../utils/imageHelper';
|
||||
@@ -84,6 +85,7 @@ function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolea
|
||||
if (prev.isAuthor !== next.isAuthor) return false;
|
||||
if (prev.style !== next.style) return false;
|
||||
if (featuresComparable(prev.features) !== featuresComparable(next.features)) return false;
|
||||
if (prev.highlightKeyword !== next.highlightKeyword) return false;
|
||||
|
||||
const a = prev.post;
|
||||
const b = next.post;
|
||||
@@ -374,6 +376,10 @@ function createPostCardStyles(colors: AppColors) {
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
highlight: {
|
||||
backgroundColor: `${colors.warning.main}40`,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,6 +397,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
isAuthor = false,
|
||||
index,
|
||||
style,
|
||||
highlightKeyword,
|
||||
} = normalizedProps;
|
||||
|
||||
const colors = useAppColors();
|
||||
@@ -556,7 +563,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
) : (
|
||||
<View style={styles.gridNoImagePreview}>
|
||||
<Text style={styles.gridNoImageText} numberOfLines={6}>
|
||||
{contentPreview}
|
||||
{highlightKeyword ? <HighlightText text={contentPreview} keyword={highlightKeyword} style={styles.highlight} /> : contentPreview}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -570,7 +577,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
|
||||
{!!post.title && (
|
||||
<Text style={styles.gridTitleMain} numberOfLines={hasImage ? 2 : 3}>
|
||||
{post.title}
|
||||
{highlightKeyword ? <HighlightText text={post.title} keyword={highlightKeyword} style={styles.highlight} /> : post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -636,7 +643,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
|
||||
{!!post.title && (
|
||||
<Text style={styles.title} numberOfLines={isCompact ? 2 : undefined}>
|
||||
{post.title}
|
||||
{highlightKeyword ? <HighlightText text={post.title} keyword={highlightKeyword} style={styles.highlight} /> : post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -646,7 +653,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
style={styles.content}
|
||||
numberOfLines={isExpanded ? undefined : contentNumberOfLines}
|
||||
>
|
||||
{content}
|
||||
{highlightKeyword ? <HighlightText text={content} keyword={highlightKeyword} style={styles.highlight} /> : content}
|
||||
</Text>
|
||||
{shouldTruncate && (
|
||||
<TouchableOpacity onPress={() => setIsExpanded((prev) => !prev)} style={styles.expandBtn}>
|
||||
|
||||
@@ -118,6 +118,9 @@ export interface PostCardProps {
|
||||
/** 索引(用于虚拟化列表优化) */
|
||||
index?: number;
|
||||
|
||||
/** 搜索高亮关键词 */
|
||||
highlightKeyword?: string;
|
||||
|
||||
/** 自定义样式 */
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
@@ -195,18 +195,23 @@ const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose })
|
||||
|
||||
// 动态导入 expo-camera,避免 Web 端加载
|
||||
const [cameraModule, setCameraModule] = useState<typeof import('expo-camera') | null>(null);
|
||||
const [permission, setPermission] = useState<any>(null);
|
||||
const [permissionStatus, setPermissionStatus] = useState<'undetermined' | 'granted' | 'denied'>('undetermined');
|
||||
const [scanned, setScanned] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && Platform.OS !== 'web') {
|
||||
import('expo-camera').then((module) => {
|
||||
setCameraModule(module);
|
||||
const [perm, requestPerm] = module.useCameraPermissions();
|
||||
setPermission(perm);
|
||||
if (!perm?.granted) {
|
||||
requestPerm();
|
||||
}
|
||||
module.Camera.getCameraPermissionsAsync().then((result) => {
|
||||
setPermissionStatus(result.granted ? 'granted' : result.canAskAgain ? 'undetermined' : 'denied');
|
||||
if (!result.granted && result.canAskAgain) {
|
||||
module.Camera.requestCameraPermissionsAsync().then((reqResult) => {
|
||||
setPermissionStatus(reqResult.granted ? 'granted' : 'denied');
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn('Failed to load expo-camera:', err);
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
@@ -237,7 +242,7 @@ const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose })
|
||||
}
|
||||
};
|
||||
|
||||
if (!cameraModule || !permission?.granted) {
|
||||
if (!cameraModule || permissionStatus !== 'granted') {
|
||||
return (
|
||||
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
|
||||
<View style={styles.container}>
|
||||
@@ -251,9 +256,15 @@ const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose })
|
||||
<View style={styles.permissionContainer}>
|
||||
<MaterialCommunityIcons name="camera-off" size={64} color="rgba(255,255,255,0.5)" />
|
||||
<Text style={styles.permissionText}>需要相机权限才能扫码</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.permissionButton}
|
||||
onPress={() => cameraModule?.useCameraPermissions()[1]()}
|
||||
<TouchableOpacity
|
||||
style={styles.permissionButton}
|
||||
onPress={() => {
|
||||
if (cameraModule) {
|
||||
cameraModule.Camera.requestCameraPermissionsAsync().then((result) => {
|
||||
setPermissionStatus(result.granted ? 'granted' : 'denied');
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text style={styles.permissionButtonText}>授予权限</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
Reference in New Issue
Block a user