Files
frontend/src/screens/profile/UserProfileScreen.tsx
lafay 5815f1a5f7
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
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
2026-04-26 12:04:27 +08:00

270 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 统一的用户主页组件 - Twitter/X 风格
* 支持两种模式self当前用户和 other其他用户
* 采用 Twitter/X 扁平化设计,全宽布局,无边框卡片
* 支持桌面端双栏布局
*/
import React, { useCallback, useMemo, useRef } from 'react';
import {
View,
RefreshControl,
ScrollView,
} from 'react-native';
import { FlashList, ListRenderItem } from '@shopify/flash-list';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAppColors } from '../../theme';
import { Post } from '../../types';
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
interface UserProfileScreenProps {
mode: ProfileMode;
userId?: string;
hasHeader?: boolean;
}
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, hasHeader = false }) => {
const colors = useAppColors();
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
const { isDesktop, isTablet } = useResponsive();
const {
user,
posts,
favorites,
loading,
refreshing,
activeTab,
setActiveTab,
scrollBottomInset,
onRefresh,
handleFollow,
handlePostAction,
handleFollowingPress,
handleFollowersPress,
handleMessage,
handleBlock,
isBlocked,
isBlockedProfile,
handleSettings,
handleEditProfile,
isCurrentUser,
currentUser,
} = useUserProfile({ mode, userId, isDesktop, isTablet });
// 当前显示的帖子列表
const currentPosts = activeTab === 0 ? posts : favorites;
// 渲染帖子项
const renderPostItem = useCallback<ListRenderItem<Post>>(({ item, index }) => {
const isPostAuthor = currentUser?.id === item.author?.id;
const isLast = index === currentPosts.length - 1;
return (
<View style={[
sharedStyles.postWrapper,
isLast && sharedStyles.lastPost,
]}>
<PostCard
post={item}
onAction={(action) => handlePostAction(item, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
}, [currentUser?.id, handlePostAction, currentPosts.length]);
const postKeyExtractor = useCallback((item: Post) => item.id, []);
// 渲染空状态
const renderEmptyPosts = useCallback(() => {
if (loading) return <Loading />;
if (isBlockedProfile && activeTab === 0 && mode === 'other') {
return (
<EmptyState
title="已将该用户拉黑"
description="你已将此用户拉黑,不再显示其帖子"
icon="account-off-outline"
variant="modern"
/>
);
}
const emptyTitle = activeTab === 0
? (mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子')
: (mode === 'self' ? '还没有收藏' : '这个用户还没有收藏任何帖子');
const emptyDesc = activeTab === 0
? (mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '')
: (mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : '');
return (
<EmptyState
title={emptyTitle}
description={emptyDesc}
icon={activeTab === 0 ? 'file-document-edit-outline' : 'bookmark-heart-outline'}
variant="modern"
/>
);
}, [loading, activeTab, mode, isBlockedProfile]);
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
const renderUserHeader = useMemo(() => {
if (!user) return null;
return (
<UserProfileHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onMessage={handleMessage}
onBlock={handleBlock}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
);
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
// 渲染 TabBar
const renderTabBar = useMemo(() => (
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
// 渲染 FlashList 头部(用户信息 + TabBar
const renderListHeader = useCallback(() => {
if (!user) return null;
return (
<>
{renderUserHeader}
{renderTabBar}
</>
);
}, [user, renderUserHeader, renderTabBar]);
// 未登录/用户不存在状态
if (mode === 'self' && !currentUser) {
return (
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
<EmptyState
title="未登录"
description="请先登录"
icon="account-off-outline"
/>
</SafeAreaView>
);
}
if (mode === 'other' && !user && !loading) {
return (
<SafeAreaView style={sharedStyles.container} edges={['bottom']}>
<EmptyState
title="用户不存在"
description="该用户可能已被删除"
icon="account-off-outline"
/>
</SafeAreaView>
);
}
const safeAreaEdges = hasHeader ? ['bottom'] : (mode === 'self' ? ['top', 'bottom'] : ['bottom']);
// 桌面端使用双栏布局
if (isDesktop || isTablet) {
return (
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
<ResponsiveContainer maxWidth={1200}>
<View style={sharedStyles.desktopContainer}>
{/* 左侧:用户信息 */}
<View style={sharedStyles.desktopSidebar}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
>
{renderListHeader()}
</ScrollView>
</View>
{/* 右侧:帖子列表 */}
<View style={sharedStyles.desktopContent}>
<FlashList
data={currentPosts}
renderItem={renderPostItem}
keyExtractor={postKeyExtractor}
ListHeaderComponent={
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
}
ListEmptyComponent={renderEmptyPosts}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
drawDistance={250}
/>
</View>
</View>
</ResponsiveContainer>
</SafeAreaView>
);
}
// 移动端使用单栏布局
return (
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
<FlashList
data={currentPosts}
renderItem={renderPostItem}
keyExtractor={postKeyExtractor}
ListHeaderComponent={renderListHeader}
ListEmptyComponent={renderEmptyPosts}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
drawDistance={250}
/>
</SafeAreaView>
);
};
export default UserProfileScreen;