From 5815f1a5f75bbbe7f5016de5a1833b1ef8e294d0 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 26 Apr 2026 12:04:27 +0800 Subject: [PATCH] refactor(home): redesign home screen with sort bar and add recommendation feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/business/PostCard/PostCard.tsx | 2 +- src/screens/home/HomeScreen.tsx | 277 ++++++++---------- src/screens/message/GroupInfoScreen.tsx | 116 +++----- src/screens/message/PrivateChatInfoScreen.tsx | 49 ++-- .../components/ChatScreen/EmojiPanel.tsx | 87 +++--- .../components/ChatScreen/MessageBubble.tsx | 2 +- .../message/components/ChatScreen/styles.ts | 6 +- .../components/ChatScreen/useChatScreen.ts | 13 +- src/screens/profile/UserProfileScreen.tsx | 57 ++-- src/services/post/postService.ts | 15 +- src/stores/post/sources.ts | 1 + src/types/dto.ts | 5 +- 12 files changed, 293 insertions(+), 337 deletions(-) diff --git a/src/components/business/PostCard/PostCard.tsx b/src/components/business/PostCard/PostCard.tsx index 50d70d4..ce1d55e 100644 --- a/src/components/business/PostCard/PostCard.tsx +++ b/src/components/business/PostCard/PostCard.tsx @@ -605,7 +605,7 @@ const PostCardInner: React.FC = (normalizedProps) => { size={14} color={post.is_liked ? colors.primary.main : colors.text.secondary} /> - + {formatNumber(post.likes_count || 0)} diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index a7b9e23..369dd33 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -23,13 +23,13 @@ import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter, useFocusEffect } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { PagerView } from '../../components/common'; + import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme'; import { Post } from '../../types'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth'; import { channelService, postService } from '../../services'; -import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business'; +import { PostCard, SearchBar, ShareSheet } from '../../components/business'; import type { PostCardAction } from '../../components/business/PostCard'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common'; import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive'; @@ -40,8 +40,9 @@ import { CreatePostScreen } from '../create/CreatePostScreen'; import * as hrefs from '../../navigation/hrefs'; import { blurActiveElement } from '../../infrastructure/platform'; -const TABS = ['关注', '最新', '热门']; -const TAB_ICONS = ['account-heart-outline', 'clock-outline', 'fire']; +const SORT_OPTIONS = ['关注', '推荐', '最新']; +const SORT_ICONS = ['account-heart', 'fire', 'clock-outline']; +const SORT_TYPES: PostType[] = ['follow', 'hot', 'latest']; const DEFAULT_PAGE_SIZE = 20; const MOBILE_TAB_BAR_HEIGHT = 64; const MOBILE_TAB_FLOATING_MARGIN = 12; @@ -83,13 +84,20 @@ function createHomeStyles(colors: AppColors) { alignItems: 'center', justifyContent: 'center', }, - capsuleWrapper: { - paddingBottom: spacing.sm, + sortBar: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, backgroundColor: colors.background.paper, }, - capsuleList: { + sortBarCapsuleScroll: { + flex: 1, + marginHorizontal: spacing.xs, + }, + sortBarCapsuleContent: { gap: spacing.xs, - paddingRight: spacing.lg, + alignItems: 'center', }, capsuleItem: { paddingHorizontal: spacing.md, @@ -104,12 +112,21 @@ function createHomeStyles(colors: AppColors) { capsuleTextActive: { color: colors.primary.main, }, + sortTrigger: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 4, + paddingHorizontal: 2, + }, + sortTriggerText: { + fontSize: 13, + fontWeight: '600', + color: colors.text.secondary, + marginLeft: 2, + }, listContent: { flexGrow: 1, }, - listHeader: { - width: '100%', - }, contentContainer: { flex: 1, }, @@ -181,7 +198,7 @@ export const HomeScreen: React.FC = () => { const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 }); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); - const [activeIndex, setActiveIndex] = useState(1); + const [sortIndex, setSortIndex] = useState(0); const [viewMode, setViewMode] = useState('list'); const [latestCapsules, setLatestCapsules] = useState([{ id: '', name: '全部' }]); const [activeCapsuleId, setActiveCapsuleId] = useState(''); @@ -217,9 +234,6 @@ export const HomeScreen: React.FC = () => { const postIdsRef = useRef>(new Set()); const isLoadingMoreRef = useRef(false); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pagerRef = useRef(null); const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll); const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll); @@ -325,28 +339,28 @@ export const HomeScreen: React.FC = () => { return map; }, [storePosts]); - // 获取当前 tab 对应的帖子类型 + // 获取当前排序对应的帖子类型 const getPostType = useCallback((): PostType => { - switch (activeIndex) { - case 0: return 'follow'; - case 1: return 'latest'; - case 2: return 'hot'; - default: return 'latest'; - } - }, [activeIndex]); + return SORT_TYPES[sortIndex] ?? 'hot'; + }, [sortIndex]); - const isLatestTab = activeIndex === 1; - const currentChannelId = isLatestTab && activeCapsuleId ? activeCapsuleId : undefined; + const currentChannelId = activeCapsuleId || undefined; useEffect(() => { homeListScrollYRef.current = 0; setBottomTabBarHiddenByScroll(false); - }, [activeIndex, currentChannelId, viewMode, setBottomTabBarHiddenByScroll]); + }, [sortIndex, currentChannelId, setBottomTabBarHiddenByScroll]); + + useEffect(() => { + requestAnimationFrame(() => { + flashListRef.current?.scrollToOffset({ offset: 0, animated: false }); + scrollViewRef.current?.scrollTo({ y: 0, animated: false }); + }); + }, [viewMode]); useLayoutEffect(() => { - if (!isLatestTab) return; restoreCapsuleStripScroll(); - }, [isLatestTab, activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]); + }, [activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]); // 使用差异更新 Hook 获取帖子列表 const listKey = useMemo( @@ -419,13 +433,10 @@ export const HomeScreen: React.FC = () => { } }, [listKey, getPostType, currentChannelId]); - // Tab 切换时刷新数据并重置列表 useEffect(() => { let cancelled = false; const run = async () => { reset(); - // reset 会清空 store 中的状态,但异步订阅可能有延迟; - // 等一帧确保 store 的订阅回调已传播到 useDifferentialPosts await new Promise(resolve => requestAnimationFrame(resolve)); if (!cancelled) { await refresh(); @@ -434,7 +445,7 @@ export const HomeScreen: React.FC = () => { run(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeIndex, currentChannelId]); + }, [sortIndex, currentChannelId]); // 将获取到的原始帖子与 store 中的状态合并 // 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等) @@ -511,23 +522,13 @@ export const HomeScreen: React.FC = () => { setViewMode(prev => prev === 'list' ? 'grid' : 'list'); }; - // 切换Tab(点击 TabBar 或手势滑动) - const changeTab = useCallback((nextIndex: number) => { - if (nextIndex < 0 || nextIndex >= TABS.length || nextIndex === activeIndex) { + // 切换排序 + const changeSort = useCallback((nextIndex: number) => { + if (nextIndex < 0 || nextIndex >= SORT_OPTIONS.length || nextIndex === sortIndex) { return; } - setActiveIndex(nextIndex); - if (Platform.OS !== 'web') { - pagerRef.current?.setPage(nextIndex); - } - }, [activeIndex]); - - const onPagerPageSelected = useCallback((e: { nativeEvent: { position: number } }) => { - const nextIndex = e.nativeEvent.position; - if (nextIndex !== activeIndex) { - setActiveIndex(nextIndex); - } - }, [activeIndex]); + setSortIndex(nextIndex); + }, [sortIndex]); useEffect(() => { const loadChannels = async () => { @@ -669,46 +670,41 @@ export const HomeScreen: React.FC = () => { setShowCreatePost(true); }; - const renderLatestCapsules = () => { - if (!isLatestTab) { - return null; - } - + const renderChannelCapsules = () => { return ( - - - {latestCapsules.map((item) => { - const isActive = item.id === activeCapsuleId; - return ( - setActiveCapsuleId(item.id)} - style={styles.capsuleItem} + + {latestCapsules.map((item) => { + const isActive = item.id === activeCapsuleId; + return ( + setActiveCapsuleId(item.id)} + style={styles.capsuleItem} + > + - - {item.name} - - - ); - })} - - + {item.name} + + + ); + })} + ); }; @@ -758,6 +754,7 @@ export const HomeScreen: React.FC = () => { const renderResponsiveGrid = () => { return ( { numColumns={gridColumns} masonry optimizeItemArrangement - ListHeaderComponent={renderLatestCapsules()} - ListHeaderComponentStyle={styles.listHeader} contentContainerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 + responsivePadding, @@ -798,10 +793,10 @@ export const HomeScreen: React.FC = () => { return ( ); - }, [activeIndex, isInitialLoading]); + }, [isInitialLoading]); // 渲染列表内容 const renderListContent = () => { @@ -813,12 +808,11 @@ export const HomeScreen: React.FC = () => { // 移动端和宽屏都使用单列 FlashList,宽屏下居中显示 return ( { /> - {/* Tab切换 */} - - - - } - /> + {/* 排序筛选 + 频道 */} + + + + + + {renderChannelCapsules()} + + changeSort((sortIndex + 1) % SORT_OPTIONS.length)} + style={styles.sortTrigger} + > + + + {SORT_OPTIONS[sortIndex]} + + + {/* 帖子列表 */} - {Platform.OS === 'web' ? ( - - {isInitialLoading ? ( - - ) : viewMode === 'list' ? ( - renderListContent() - ) : ( - renderResponsiveGrid() - )} - - ) : ( - - - {isInitialLoading && activeIndex === 0 ? ( - - ) : activeIndex === 0 ? ( - viewMode === 'list' ? renderListContent() : renderResponsiveGrid() - ) : ( - - )} - - - {isInitialLoading && activeIndex === 1 ? ( - - ) : activeIndex === 1 ? ( - viewMode === 'list' ? renderListContent() : renderResponsiveGrid() - ) : ( - - )} - - - {isInitialLoading && activeIndex === 2 ? ( - - ) : activeIndex === 2 ? ( - viewMode === 'list' ? renderListContent() : renderResponsiveGrid() - ) : ( - - )} - - - )} + + {isInitialLoading ? ( + + ) : viewMode === 'list' ? ( + renderListContent() + ) : ( + renderResponsiveGrid() + )} + {/* 漂浮发帖按钮 */} { contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} > - {/* 群组基本信息卡片 */} + {/* 群组基本信息 */} @@ -621,7 +621,7 @@ const GroupInfoScreen: React.FC = () => { {group.name} - + {group.member_count} 人 @@ -661,7 +661,7 @@ const GroupInfoScreen: React.FC = () => { )} - {/* 群公告卡片 */} + {/* 群公告 */} {announcements.length > 0 && ( @@ -684,7 +684,7 @@ const GroupInfoScreen: React.FC = () => { )} - {/* 成员预览卡片 */} + {/* 成员预览 */} @@ -813,6 +813,7 @@ const GroupInfoScreen: React.FC = () => { thumbColor={isPinned ? colors.primary.main : colors.background.paper} /> + @@ -1126,22 +1127,20 @@ function createGroupInfoStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: colors.background.default, + backgroundColor: colors.background.paper, }, pageHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: spacing.md, + paddingHorizontal: spacing.lg, paddingVertical: spacing.md, backgroundColor: colors.background.paper, - borderBottomWidth: 0.5, - borderBottomColor: colors.divider + '60', + borderBottomWidth: 0, }, pageTitle: { - fontWeight: '700', + fontWeight: '600', fontSize: fontSizes.xl, - letterSpacing: 0.5, }, pageHeaderPlaceholder: { width: 40, @@ -1151,7 +1150,7 @@ function createGroupInfoStyles(colors: AppColors) { flex: 1, }, scrollContent: { - padding: spacing.md, + paddingHorizontal: spacing.lg, paddingBottom: spacing.xl * 2, }, errorContainer: { @@ -1160,29 +1159,25 @@ function createGroupInfoStyles(colors: AppColors) { alignItems: 'center', }, - // 头部卡片样式 - 扁平化设计 + // 头部区域 - 扁平化无卡片 headerCard: { backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - padding: spacing.lg, + paddingVertical: spacing.lg, marginBottom: spacing.md, - borderWidth: 0.5, - borderColor: colors.divider + '40', }, groupHeader: { flexDirection: 'row', alignItems: 'center', - marginBottom: spacing.md, + marginBottom: spacing.lg, }, groupInfo: { marginLeft: spacing.lg, flex: 1, }, groupName: { - fontWeight: '800', - fontSize: fontSizes.xl + 2, + fontWeight: '700', + fontSize: fontSizes['2xl'], marginBottom: spacing.xs, - letterSpacing: 0.3, }, groupMeta: { flexDirection: 'row', @@ -1213,8 +1208,6 @@ function createGroupInfoStyles(colors: AppColors) { backgroundColor: colors.background.default, borderRadius: borderRadius.lg, padding: spacing.md, - borderWidth: 0.5, - borderColor: colors.divider + '30', }, groupDesc: { marginLeft: spacing.sm, @@ -1223,19 +1216,16 @@ function createGroupInfoStyles(colors: AppColors) { fontWeight: '400', }, - // 通用卡片样式 - 扁平化 + // 通用卡片样式 - 扁平化:无阴影无圆角,使用分割线 card: { backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, - padding: spacing.lg, marginBottom: spacing.md, - borderWidth: 0.5, - borderColor: colors.divider + '40', }, cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.md, + paddingTop: spacing.sm, }, cardIconContainer: { width: 36, @@ -1247,13 +1237,12 @@ function createGroupInfoStyles(colors: AppColors) { marginRight: spacing.sm, }, cardTitle: { - fontWeight: '700', - fontSize: fontSizes.md + 1, + fontWeight: '600', + fontSize: fontSizes.md, flex: 1, - letterSpacing: 0.3, }, - // 公告样式 - 更现代的卡片 + // 公告样式 - 扁平化 announcementContent: { backgroundColor: colors.warning.light + '08', borderRadius: borderRadius.lg, @@ -1324,7 +1313,7 @@ function createGroupInfoStyles(colors: AppColors) { fontSize: fontSizes.sm, }, - // 邀请按钮样式 - 更加醒目 + // 邀请按钮样式 - 扁平化 inviteButton: { flexDirection: 'row', alignItems: 'center', @@ -1348,25 +1337,23 @@ function createGroupInfoStyles(colors: AppColors) { inviteButtonText: { fontWeight: '700', marginRight: spacing.xs, - letterSpacing: 0.3, }, - // 设置项样式 - 更加扁平 + // 设置项样式 - 扁平化:无圆角背景,简洁分割线 settingsList: { - marginTop: spacing.sm, + marginTop: 0, }, settingItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, - borderRadius: borderRadius.md, - paddingHorizontal: spacing.sm, - marginLeft: -spacing.sm, - marginRight: -spacing.sm, + paddingHorizontal: 0, + marginLeft: 0, + marginRight: 0, }, settingIconContainer: { - width: 40, - height: 40, + width: 36, + height: 36, borderRadius: borderRadius.md, backgroundColor: colors.background.default, justifyContent: 'center', @@ -1384,18 +1371,16 @@ function createGroupInfoStyles(colors: AppColors) { fontWeight: '600', }, settingDivider: { - marginLeft: 56, + marginLeft: 52, width: 'auto', - marginVertical: spacing.xs, + marginVertical: 0, opacity: 0.5, }, - // 危险操作卡片 - 更加醒目 + // 危险操作区域 - 扁平化 dangerCard: { padding: 0, overflow: 'hidden', - borderTopWidth: 1, - borderTopColor: colors.error.light + '30', marginTop: spacing.sm, }, dangerButton: { @@ -1404,14 +1389,12 @@ function createGroupInfoStyles(colors: AppColors) { justifyContent: 'center', paddingVertical: spacing.lg, gap: spacing.sm, - backgroundColor: colors.error.light + '08', }, dangerButtonText: { - fontWeight: '700', - letterSpacing: 0.3, + fontWeight: '600', }, - // 模态框样式 - 更现代 + // 模态框样式 - 扁平化 modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.45)', @@ -1440,20 +1423,18 @@ function createGroupInfoStyles(colors: AppColors) { minWidth: 60, }, modalTitle: { - fontWeight: '800', - fontSize: fontSizes.xl + 1, - letterSpacing: 0.5, + fontWeight: '700', + fontSize: fontSizes.xl, }, saveButton: { - fontWeight: '700', - letterSpacing: 0.3, + fontWeight: '600', }, confirmButton: { - fontWeight: '700', + fontWeight: '600', textAlign: 'right', }, - // 编辑表单样式 - 更现代 + // 编辑表单样式 - 扁平化 editForm: { alignItems: 'center', }, @@ -1465,7 +1446,7 @@ function createGroupInfoStyles(colors: AppColors) { width: 90, height: 90, borderRadius: 45, - borderWidth: 3, + borderWidth: 2, borderColor: colors.primary.light + '40', }, editAvatarPlaceholder: { @@ -1496,9 +1477,8 @@ function createGroupInfoStyles(colors: AppColors) { }, editLabel: { marginBottom: spacing.sm, - fontWeight: '700', - fontSize: fontSizes.sm + 1, - letterSpacing: 0.3, + fontWeight: '600', + fontSize: fontSizes.sm, }, editInput: { backgroundColor: colors.background.default, @@ -1507,7 +1487,7 @@ function createGroupInfoStyles(colors: AppColors) { paddingVertical: spacing.md, fontSize: fontSizes.md, color: colors.text.primary, - borderWidth: 1.5, + borderWidth: 1, borderColor: colors.divider + '80', fontWeight: '400', }, @@ -1533,7 +1513,7 @@ function createGroupInfoStyles(colors: AppColors) { padding: spacing.md, fontSize: fontSizes.md, color: colors.text.primary, - borderWidth: 1.5, + borderWidth: 1, borderColor: colors.divider + '80', lineHeight: 22, textAlignVertical: 'top', @@ -1553,14 +1533,14 @@ function createGroupInfoStyles(colors: AppColors) { alignItems: 'center', padding: spacing.md, borderRadius: borderRadius.lg, - borderWidth: 1.5, + borderWidth: 1, borderColor: colors.divider + '80', backgroundColor: colors.background.default, }, joinTypeItemSelected: { borderColor: colors.primary.main, backgroundColor: colors.primary.light + '12', - borderWidth: 2, + borderWidth: 1.5, }, joinTypeIcon: { width: 48, @@ -1602,7 +1582,7 @@ function createGroupInfoStyles(colors: AppColors) { }, transferItemSelected: { backgroundColor: colors.primary.light + '12', - borderWidth: 1.5, + borderWidth: 1, borderColor: colors.primary.light + '60', }, transferItemInfo: { @@ -1681,7 +1661,7 @@ function createGroupInfoStyles(colors: AppColors) { }, friendItemSelected: { backgroundColor: colors.primary.light + '12', - borderWidth: 1.5, + borderWidth: 1, borderColor: colors.primary.light + '60', }, friendInfo: { diff --git a/src/screens/message/PrivateChatInfoScreen.tsx b/src/screens/message/PrivateChatInfoScreen.tsx index 1f4d6a1..04a912f 100644 --- a/src/screens/message/PrivateChatInfoScreen.tsx +++ b/src/screens/message/PrivateChatInfoScreen.tsx @@ -16,7 +16,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; +import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import { authService } from '@/services/auth'; import { messageService } from '@/services/message'; @@ -336,7 +336,7 @@ const PrivateChatInfoScreen: React.FC = () => { contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} > - {/* 用户基本信息卡片 */} + {/* 用户基本信息 */} { > @@ -421,20 +421,20 @@ function createPrivateChatInfoStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: colors.background.default, + backgroundColor: colors.background.paper, }, pageHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: spacing.md, - paddingVertical: spacing.sm, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, + borderBottomWidth: 0, }, pageTitle: { fontWeight: '600', + fontSize: fontSizes.xl, }, pageHeaderPlaceholder: { width: 40, @@ -444,15 +444,13 @@ function createPrivateChatInfoStyles(colors: AppColors) { flex: 1, }, scrollContent: { + paddingHorizontal: spacing.lg, paddingBottom: spacing.xl, }, headerCard: { backgroundColor: colors.background.paper, - marginHorizontal: spacing.md, - marginTop: spacing.md, - borderRadius: borderRadius.lg, - padding: spacing.lg, - ...shadows.sm, + paddingVertical: spacing.lg, + marginBottom: spacing.md, }, userHeader: { flexDirection: 'row', @@ -460,58 +458,61 @@ function createPrivateChatInfoStyles(colors: AppColors) { }, userInfo: { flex: 1, - marginLeft: spacing.md, + marginLeft: spacing.lg, }, userName: { - fontWeight: '600', + fontWeight: '700', + fontSize: fontSizes['2xl'], marginBottom: spacing.xs, }, section: { marginTop: spacing.lg, - paddingHorizontal: spacing.md, }, sectionTitle: { marginBottom: spacing.sm, marginLeft: spacing.sm, fontWeight: '500', + fontSize: fontSizes.sm, + color: colors.text.secondary, }, card: { backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, - ...shadows.sm, overflow: 'hidden', }, settingItem: { flexDirection: 'row', alignItems: 'center', - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + paddingHorizontal: 0, }, settingIconContainer: { width: 36, height: 36, borderRadius: borderRadius.md, - backgroundColor: colors.primary.light + '20', + backgroundColor: colors.background.default, justifyContent: 'center', alignItems: 'center', marginRight: spacing.md, }, settingIconDanger: { - backgroundColor: colors.error.light + '20', + backgroundColor: colors.error.light + '15', }, settingTitle: { flex: 1, }, dangerText: { color: colors.error.main, + fontWeight: '600', }, divider: { - marginLeft: spacing.xl + 36, + marginLeft: 52, width: 'auto', - marginVertical: spacing.xs, + marginVertical: 0, }, deleteButton: { marginTop: spacing.sm, + borderRadius: borderRadius.lg, + height: 52, }, }); } diff --git a/src/screens/message/components/ChatScreen/EmojiPanel.tsx b/src/screens/message/components/ChatScreen/EmojiPanel.tsx index dd53cdd..6f8a2f6 100644 --- a/src/screens/message/components/ChatScreen/EmojiPanel.tsx +++ b/src/screens/message/components/ChatScreen/EmojiPanel.tsx @@ -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 = ({ return StyleSheet.create({ ...baseStyles, emojiContainer: { - flexDirection: 'row', - flexWrap: 'wrap', + flex: 1, padding: spacing.sm, }, emojiItem: { @@ -160,26 +160,19 @@ export const EmojiPanel: React.FC = ({ ), [styles.emojiItem, styles.emojiText, handleEmojiPress]); - // 渲染 Emoji 面板(使用 FlatList 虚拟化,只渲染可见行) + // 渲染 Emoji 面板(使用 FlashList 虚拟化,只渲染可见行) const renderEmojiPanel = () => ( - 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, - })} - /> + + item.id} + renderItem={renderEmojiRow} + showsVerticalScrollIndicator={true} + keyboardShouldPersistTaps="handled" + style={{ flex: 1 }} + /> + ); // 添加表情(从相册选择) @@ -347,20 +340,18 @@ export const EmojiPanel: React.FC = ({ } return ( - 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} - /> + + item.type === 'manage' ? 'manage-button' : item.sticker.id} + renderItem={renderStickerListItem} + showsVerticalScrollIndicator={false} + keyboardShouldPersistTaps="handled" + style={{ flex: 1 }} + /> + ); }; @@ -494,19 +485,17 @@ export const EmojiPanel: React.FC = ({ ) : ( - 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} - /> + + item.type === 'add' ? 'manage-add-button' : item.sticker.id} + renderItem={renderManageListItem} + showsVerticalScrollIndicator={false} + keyboardShouldPersistTaps="handled" + style={{ flex: 1 }} + /> + )} {/* 底部操作栏(管理模式下显示) */} diff --git a/src/screens/message/components/ChatScreen/MessageBubble.tsx b/src/screens/message/components/ChatScreen/MessageBubble.tsx index 2ec28da..00af143 100644 --- a/src/screens/message/components/ChatScreen/MessageBubble.tsx +++ b/src/screens/message/components/ChatScreen/MessageBubble.tsx @@ -427,7 +427,7 @@ const MessageBubbleInner: React.FC = ({ {/* 群聊模式:显示发送者昵称 */} {isGroupChat && senderInfo && ( - {senderInfo.nickname} + {senderInfo.nickname} )} {renderMessageContent()} diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index f0b3249..20f6f4d 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -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, diff --git a/src/screens/message/components/ChatScreen/useChatScreen.ts b/src/screens/message/components/ChatScreen/useChatScreen.ts index 8a3628b..e8f9846 100644 --- a/src/screens/message/components/ChatScreen/useChatScreen.ts +++ b/src/screens/message/components/ChatScreen/useChatScreen.ts @@ -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; diff --git a/src/screens/profile/UserProfileScreen.tsx b/src/screens/profile/UserProfileScreen.tsx index 775e4cd..be27e4c 100644 --- a/src/screens/profile/UserProfileScreen.tsx +++ b/src/screens/profile/UserProfileScreen.tsx @@ -110,35 +110,48 @@ export const UserProfileScreen: React.FC = ({ mode, user ); }, [loading, activeTab, mode, isBlockedProfile]); + // 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染) + const renderUserHeader = useMemo(() => { + if (!user) return null; + return ( + + ); + }, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]); + + // 渲染 TabBar + const renderTabBar = useMemo(() => ( + + + + ), [activeTab, setActiveTab, sharedStyles.tabBarContainer]); + // 渲染 FlashList 头部(用户信息 + TabBar) const renderListHeader = useCallback(() => { if (!user) return null; return ( <> - - - - + {renderUserHeader} + {renderTabBar} ); - }, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress, activeTab, setActiveTab]); + }, [user, renderUserHeader, renderTabBar]); // 未登录/用户不存在状态 if (mode === 'self' && !currentUser) { diff --git a/src/services/post/postService.ts b/src/services/post/postService.ts index f41b014..be83ce9 100644 --- a/src/services/post/postService.ts +++ b/src/services/post/postService.ts @@ -251,12 +251,15 @@ class PostService { page_size: params.page_size, }; - // 将 post_type 映射为后端的 tab 参数 - if (params.post_type) { - requestParams.tab = params.post_type; - } - - const response = await api.get('/posts', requestParams); + if (params.post_type) { + requestParams.tab = params.post_type; + } + + if (params.channel_id) { + requestParams.channel_id = params.channel_id; + } + + const response = await api.get('/posts', requestParams); const data = response.data; diff --git a/src/stores/post/sources.ts b/src/stores/post/sources.ts index 34629ba..2ee9b4e 100644 --- a/src/stores/post/sources.ts +++ b/src/stores/post/sources.ts @@ -62,6 +62,7 @@ export class NetworkCursorPostListPagedSource implements IPostListPagedSource { cursor: this.nextCursor ?? '', page_size: this.pageSize, post_type: this.tab, + channel_id: this.channelId, }; const result = await postService.getPostsCursor(params); diff --git a/src/types/dto.ts b/src/types/dto.ts index 9166d99..2ee779b 100644 --- a/src/types/dto.ts +++ b/src/types/dto.ts @@ -520,14 +520,11 @@ export type DeviceType = 'ios' | 'android' | 'web'; * 游标分页请求参数 */ export interface CursorPaginationRequest { - /** 游标字符串(可选,首次请求不传) */ cursor?: string; - /** 分页方向:forward 或 backward(默认 forward) */ direction?: 'forward' | 'backward'; - /** 每页数量(默认 20,最大 100) */ page_size?: number; - /** 帖子类型筛选(可选):follow, hot, latest */ post_type?: 'follow' | 'hot' | 'latest'; + channel_id?: string; } /**