feat(ui): add emoji picker with FlatList virtualization and improve input components
- Add full emoji picker with virtualized FlatList to CreatePostScreen and PostDetailScreen - Add autoExpand prop to PostMentionInput for XHS-style content area - Improve mention item styling with @ icon and hint text - Fix HomeScreen tab switching race condition with requestAnimationFrame - Fix PostRepository pagination to prefer cursor over page parameter - Fix user store imports to use explicit UserManager path - Refactor useCurrentUser hook to use sessionStore directly
This commit is contained in:
@@ -20,6 +20,8 @@ import {
|
||||
Animated,
|
||||
Image,
|
||||
useWindowDimensions,
|
||||
FlatList,
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter, useLocalSearchParams, useNavigation } from 'expo-router';
|
||||
@@ -82,6 +84,22 @@ const EMOJIS = [
|
||||
'💯', '💢', '💥', '💫', '💦', '💨', '🕳️',
|
||||
];
|
||||
|
||||
// 每行显示的 emoji 数量
|
||||
const EMOJIS_PER_ROW = 8;
|
||||
const EMOJI_ROW_HEIGHT = 52;
|
||||
|
||||
// 将 emoji 分组成行,用于 FlatList 虚拟化
|
||||
const EMOJI_ROWS = (() => {
|
||||
const rows: { id: string; emojis: string[] }[] = [];
|
||||
for (let i = 0; i < EMOJIS.length; i += EMOJIS_PER_ROW) {
|
||||
rows.push({
|
||||
id: `emoji-row-${i}`,
|
||||
emojis: EMOJIS.slice(i, i + EMOJIS_PER_ROW),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
})();
|
||||
|
||||
// 动画值
|
||||
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
||||
|
||||
@@ -132,15 +150,15 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
// 响应式图片网格配置
|
||||
const imagesPerRow = useResponsiveValue({ xs: 3, sm: 3, md: 4, lg: 5, xl: 6 });
|
||||
const imageGap = 4;
|
||||
const availableWidth = isWideScreen
|
||||
? Math.min(width, 800) - spacing.lg * 2
|
||||
const availableWidth = isWideScreen
|
||||
? Math.min(width, 800) - spacing.lg * 2
|
||||
: width - spacing.lg * 2;
|
||||
const imageSize = Math.floor((availableWidth - imageGap * (imagesPerRow - 1)) / imagesPerRow);
|
||||
// 小红书风格:内容区域占满剩余空间,输入框自动扩展
|
||||
const contentInputMinHeight = Math.max(
|
||||
isWideScreen ? 460 : 320,
|
||||
Math.floor(windowHeight * (isWideScreen ? 0.56 : 0.5))
|
||||
isWideScreen ? 200 : 160,
|
||||
Math.floor(windowHeight * (isWideScreen ? 0.3 : 0.25))
|
||||
);
|
||||
const contentSectionMinHeight = Math.floor(windowHeight * (isWideScreen ? 0.68 : 0.62));
|
||||
|
||||
React.useEffect(() => {
|
||||
Animated.parallel([
|
||||
@@ -312,8 +330,12 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
|
||||
// 切换表情面板
|
||||
const handleToggleEmojiPanel = () => {
|
||||
const willShow = !showEmojiPanel;
|
||||
closeAllPanels();
|
||||
setShowEmojiPanel(!showEmojiPanel);
|
||||
if (willShow) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
setShowEmojiPanel(willShow);
|
||||
};
|
||||
|
||||
// 切换频道选择
|
||||
@@ -389,7 +411,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
await voteService.createVotePost({
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
images: imageUrls,
|
||||
images: imageUrls.length > 0 ? imageUrls : undefined,
|
||||
channel_id: selectedChannelId || undefined,
|
||||
vote_options: validOptions.map(opt => ({ content: opt })),
|
||||
});
|
||||
@@ -405,8 +427,8 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
const updated = await postService.updatePost(editPostID, {
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
segments: segments.length > 0 ? segments : undefined,
|
||||
images: imageUrls,
|
||||
segments: segments.some(s => s.type !== 'text') ? segments : undefined,
|
||||
images: imageUrls.length > 0 ? imageUrls : undefined,
|
||||
});
|
||||
if (!updated) {
|
||||
throw new Error('更新帖子失败');
|
||||
@@ -422,8 +444,8 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
await postService.createPost({
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
segments: segments.length > 0 ? segments : undefined,
|
||||
images: imageUrls,
|
||||
segments: segments.some(s => s.type !== 'text') ? segments : undefined,
|
||||
images: imageUrls.length > 0 ? imageUrls : undefined,
|
||||
channel_id: selectedChannelId || undefined,
|
||||
});
|
||||
showPrompt({
|
||||
@@ -583,12 +605,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
|
||||
// 渲染内容输入区
|
||||
const renderContentSection = () => (
|
||||
<View
|
||||
style={[
|
||||
styles.contentSection,
|
||||
contentSectionMinHeight ? { minHeight: contentSectionMinHeight } : null,
|
||||
]}
|
||||
>
|
||||
<View style={styles.contentSection}>
|
||||
{/* 标题输入 */}
|
||||
<TextInput
|
||||
style={[
|
||||
@@ -597,19 +614,20 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
]}
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
placeholder="请输入标题(必填)"
|
||||
placeholder="填写标题会有更多赞哦~"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
/>
|
||||
|
||||
{/* 正文输入(支持 @提及) */}
|
||||
{/* 正文输入(支持 @提及)——小红书风格:无边框、无背景、自动扩展 */}
|
||||
<PostMentionInput
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
onSegmentsChange={setSegments}
|
||||
placeholder="分享新鲜事..."
|
||||
placeholder="添加正文"
|
||||
maxLength={MAX_CONTENT_LENGTH}
|
||||
minHeight={contentInputMinHeight}
|
||||
autoExpand
|
||||
style={[
|
||||
styles.contentInput,
|
||||
isWideScreen && styles.contentInputWide,
|
||||
@@ -622,7 +640,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染表情面板
|
||||
// 渲染表情面板 —— 使用 FlatList 虚拟化渲染,解决显示不全和卡顿
|
||||
const renderEmojiPanel = () => {
|
||||
if (!showEmojiPanel) return null;
|
||||
|
||||
@@ -631,20 +649,34 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
styles.emojiPanel,
|
||||
isWideScreen && styles.emojiPanelWide,
|
||||
]}>
|
||||
<ScrollView
|
||||
<FlatList
|
||||
data={EMOJI_ROWS}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.emojiRow}>
|
||||
{item.emojis.map((emoji, index) => (
|
||||
<TouchableOpacity
|
||||
key={`${item.id}-${index}`}
|
||||
style={styles.emojiItem}
|
||||
onPress={() => handleInsertEmoji(emoji)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.emojiText}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.emojiGrid}
|
||||
>
|
||||
{EMOJIS.map((emoji, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={styles.emojiItem}
|
||||
onPress={() => handleInsertEmoji(emoji)}
|
||||
>
|
||||
<Text style={styles.emojiText}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
windowSize={5}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: EMOJI_ROW_HEIGHT,
|
||||
offset: EMOJI_ROW_HEIGHT * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -874,7 +906,7 @@ function createCreatePostStyles(colors: AppColors) {
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
titleInputWide: {
|
||||
fontSize: fontSizes.xl,
|
||||
@@ -883,12 +915,12 @@ function createCreatePostStyles(colors: AppColors) {
|
||||
flexGrow: 1,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
lineHeight: 22,
|
||||
lineHeight: 24,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
contentInputWide: {
|
||||
fontSize: fontSizes.lg,
|
||||
lineHeight: 26,
|
||||
lineHeight: 28,
|
||||
},
|
||||
// 图片网格
|
||||
imageGrid: {
|
||||
@@ -1036,20 +1068,22 @@ function createCreatePostStyles(colors: AppColors) {
|
||||
emojiPanelWide: {
|
||||
height: 320,
|
||||
},
|
||||
emojiGrid: {
|
||||
emojiRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
padding: spacing.md,
|
||||
justifyContent: 'flex-start',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
height: EMOJI_ROW_HEIGHT,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
emojiItem: {
|
||||
width: '12.5%',
|
||||
aspectRatio: 1,
|
||||
width: 44,
|
||||
height: 44,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
emojiText: {
|
||||
fontSize: 24,
|
||||
fontSize: 26,
|
||||
lineHeight: 32,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
|
||||
Reference in New Issue
Block a user