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:
@@ -439,8 +439,18 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
// Tab 切换时刷新数据并重置列表
|
||||
useEffect(() => {
|
||||
reset();
|
||||
refresh();
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
reset();
|
||||
// reset 会清空 store 中的状态,但异步订阅可能有延迟;
|
||||
// 等一帧确保 store 的订阅回调已传播到 useDifferentialPosts
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
if (!cancelled) {
|
||||
await refresh();
|
||||
}
|
||||
};
|
||||
run();
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeIndex, currentChannelId]);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ScrollView,
|
||||
Image,
|
||||
Clipboard,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
|
||||
@@ -45,6 +46,55 @@ import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../h
|
||||
import { handleError } from '@/services/core';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
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;
|
||||
})();
|
||||
|
||||
function togglePostField(
|
||||
post: Post,
|
||||
field: 'is_liked' | 'is_favorited',
|
||||
@@ -199,6 +249,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// 评论图片相关状态
|
||||
const [commentImages, setCommentImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
|
||||
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
const [isFollowingMe, setIsFollowingMe] = useState(false);
|
||||
const [isFollowLoading, setIsFollowLoading] = useState(false);
|
||||
@@ -1359,9 +1410,27 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
const closeComposer = () => {
|
||||
setIsComposerVisible(false);
|
||||
setShowEmojiPanel(false);
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
const handleAtMention = () => {
|
||||
setCommentText(prev => prev + '@');
|
||||
setShowEmojiPanel(false);
|
||||
};
|
||||
|
||||
const handleToggleEmojiPanel = () => {
|
||||
const willShow = !showEmojiPanel;
|
||||
if (willShow) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
setShowEmojiPanel(willShow);
|
||||
};
|
||||
|
||||
const handleInsertEmoji = (emoji: string) => {
|
||||
setCommentText(prev => prev + emoji);
|
||||
};
|
||||
|
||||
const renderCommentInput = () => (
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
@@ -1443,14 +1512,15 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Text Input */}
|
||||
{/* Text Input —— 小红书风格:无边框、自动扩展 */}
|
||||
<PostMentionInput
|
||||
value={commentText}
|
||||
onChangeText={setCommentText}
|
||||
onSegmentsChange={setCommentSegments}
|
||||
placeholder="来说点什么吧!"
|
||||
placeholder={replyingTo ? `回复 ${replyingTo.author.nickname}...` : '来说点什么吧!'}
|
||||
maxLength={500}
|
||||
minHeight={40}
|
||||
minHeight={60}
|
||||
autoExpand
|
||||
style={styles.expandedTextInput}
|
||||
/>
|
||||
|
||||
@@ -1492,7 +1562,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.expandedToolbarItem}
|
||||
onPress={() => {}}
|
||||
onPress={handleAtMention}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="at"
|
||||
@@ -1502,12 +1572,12 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.expandedToolbarItem}
|
||||
onPress={() => {}}
|
||||
onPress={handleToggleEmojiPanel}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="gamepad-variant-outline"
|
||||
name={showEmojiPanel ? "emoticon-happy" : "emoticon-happy-outline"}
|
||||
size={22}
|
||||
color={colors.text.secondary}
|
||||
color={showEmojiPanel ? colors.primary.main : colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.expandedCharCount}>
|
||||
@@ -1526,6 +1596,40 @@ export const PostDetailScreen: React.FC = () => {
|
||||
<Text style={styles.expandedSendButtonText}>发送</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Emoji Panel —— 使用 FlatList 虚拟化渲染,解决显示不全和卡顿 */}
|
||||
{showEmojiPanel && (
|
||||
<View style={styles.emojiPanel}>
|
||||
<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}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
windowSize={5}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: EMOJI_ROW_HEIGHT,
|
||||
offset: EMOJI_ROW_HEIGHT * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -2043,10 +2147,10 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
inputTrigger: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: 18,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 2,
|
||||
minHeight: 38,
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputTriggerText: {
|
||||
@@ -2117,13 +2221,12 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
expandedTextInput: {
|
||||
minHeight: 80,
|
||||
maxHeight: 160,
|
||||
flexGrow: 1,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
textAlignVertical: 'top',
|
||||
lineHeight: 22,
|
||||
lineHeight: 24,
|
||||
},
|
||||
expandedToolbar: {
|
||||
flexDirection: 'row',
|
||||
@@ -2163,6 +2266,29 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
fontSize: fontSizes.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emojiPanel: {
|
||||
height: 220,
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.md,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
emojiRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
height: EMOJI_ROW_HEIGHT,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
emojiItem: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emojiText: {
|
||||
fontSize: 26,
|
||||
lineHeight: 32,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
|
||||
Reference in New Issue
Block a user