refactor(home): redesign home screen with sort bar and add recommendation feed
- Replace PagerView-based tabs with modern sort bar navigation - Add "推荐" (recommended) feed option with "hot" sort type - Update tab icons and labels to match new sort paradigm - Optimize PostCard, MessageBubble conditional styles perf(chat): migrate FlatList to FlashList for emoji/sticker panels - Improve virtualized rendering performance for emoji and sticker grids - Fix jump-to-latest scroll behavior for inverted FlashList perf(profile): extract user header and tab bar into separate memoized renders - Prevent unnecessary re-renders when switching tabs feat(api): add channel_id filter support for post queries - Include channel_id in cursor pagination requests - Update CursorPaginationRequest type definition style(chat-info): redesign group and private chat info screens with flat layout - Remove card borders and shadows for cleaner appearance - Adjust avatar sizes and spacing for consistency
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, Platform } from 'react-native';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, Platform } from 'react-native';
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
@@ -72,8 +73,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
return StyleSheet.create({
|
||||
...baseStyles,
|
||||
emojiContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flex: 1,
|
||||
padding: spacing.sm,
|
||||
},
|
||||
emojiItem: {
|
||||
@@ -160,26 +160,19 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
</View>
|
||||
), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
|
||||
|
||||
// 渲染 Emoji 面板(使用 FlatList 虚拟化,只渲染可见行)
|
||||
// 渲染 Emoji 面板(使用 FlashList 虚拟化,只渲染可见行)
|
||||
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,
|
||||
})}
|
||||
/>
|
||||
<View style={styles.emojiContainer}>
|
||||
<FlashList
|
||||
key="emoji-flatlist"
|
||||
data={emojiRows}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderEmojiRow}
|
||||
showsVerticalScrollIndicator={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 添加表情(从相册选择)
|
||||
@@ -347,20 +340,18 @@ 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}
|
||||
renderItem={renderStickerListItem}
|
||||
contentContainerStyle={styles.stickerGrid}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={16}
|
||||
maxToRenderPerBatch={16}
|
||||
windowSize={7}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
<View style={styles.stickerGrid}>
|
||||
<FlashList
|
||||
key="sticker-flatlist"
|
||||
data={stickerListData}
|
||||
numColumns={4}
|
||||
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
|
||||
renderItem={renderStickerListItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -494,19 +485,17 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={manageListData}
|
||||
numColumns={4}
|
||||
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
|
||||
renderItem={renderManageListItem}
|
||||
contentContainerStyle={styles.manageStickerGrid}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={16}
|
||||
maxToRenderPerBatch={16}
|
||||
windowSize={7}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
<View style={styles.manageStickerGrid}>
|
||||
<FlashList
|
||||
data={manageListData}
|
||||
numColumns={4}
|
||||
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
|
||||
renderItem={renderManageListItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 底部操作栏(管理模式下显示) */}
|
||||
|
||||
@@ -427,7 +427,7 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
||||
{/* 群聊模式:显示发送者昵称 */}
|
||||
{isGroupChat && senderInfo && (
|
||||
<Text style={[styles.senderName, isMe && styles.mySenderName]}>{senderInfo.nickname}</Text>
|
||||
<Text style={[styles.senderName, isMe ? styles.mySenderName : {}]}>{senderInfo.nickname}</Text>
|
||||
)}
|
||||
|
||||
{renderMessageContent()}
|
||||
|
||||
@@ -711,8 +711,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
stickerGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flex: 1,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
@@ -1259,8 +1258,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
color: colors.chat.textSecondary,
|
||||
},
|
||||
manageStickerGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flex: 1,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
|
||||
@@ -562,8 +562,17 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
const jumpToLatestMessages = useCallback(() => {
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
scrollToLatest(true, true, 'jump-latest-button');
|
||||
}, [scrollToLatest]);
|
||||
// FlashList 在 inverted 模式下 scrollToOffset({ offset: 0 }) 可能无法真正滚到最底部,
|
||||
// 因为列表内容高度变化后最小 offset 可能不是 0。先尝试滚到负值确保到底,再补偿回 0。
|
||||
isProgrammaticScrollRef.current = true;
|
||||
flatListRef.current?.scrollToOffset({ offset: -99999, animated: false });
|
||||
requestAnimationFrame(() => {
|
||||
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
setTimeout(() => {
|
||||
isProgrammaticScrollRef.current = false;
|
||||
}, 220);
|
||||
});
|
||||
}, [flatListRef]);
|
||||
|
||||
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||
isBrowsingHistoryRef.current = browsing;
|
||||
|
||||
Reference in New Issue
Block a user