refactor(home): redesign home screen with sort bar and add recommendation feed
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m49s
Frontend CI / ota-android (push) Successful in 10m29s
Frontend CI / build-android-apk (push) Successful in 40m32s

- 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:
lafay
2026-04-26 12:04:27 +08:00
parent e9d7098ad0
commit 5815f1a5f7
12 changed files with 293 additions and 337 deletions

View File

@@ -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>
)}
{/* 底部操作栏(管理模式下显示) */}

View File

@@ -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()}

View File

@@ -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,

View File

@@ -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;