feat(post): add post reference functionality with inline cards and suggestion search
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m42s
Frontend CI / build-android-apk (push) Has been cancelled
Frontend CI / ota-android (push) Has been cancelled

Add support for referencing/quoting other posts within post content. Includes:
- New `post_ref` segment type with `PostRefSegmentData` interface for post references
- New `PostRefCard` component to display referenced posts inline
- Post suggestion search triggered by `#` in `PostMentionInput`
- Updated `PostContentRenderer` to render post reference segments
- Add `suggestPosts` and `recordRefClick` API methods in postService

Additional improvements:
- Add image gallery preview for user profile covers and avatars
- Make text selectable across CommentItem, PostContentRenderer, and PostDetailScreen
- Migrate FlatList to FlashList in PostDetailScreen and UserProfileScreen
- Add nestedScrollEnabled to CreatePostScreen
- Add clickable member avatars in GroupMembersScreen
This commit is contained in:
lafay
2026-04-26 01:11:30 +08:00
parent 5fa5403d6a
commit 2dc6936aac
11 changed files with 554 additions and 193 deletions

View File

@@ -5,12 +5,13 @@
* 支持桌面端双栏布局
*/
import React, { useCallback, useMemo } from 'react';
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';
@@ -54,113 +55,90 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
currentUser,
} = useUserProfile({ mode, userId, isDesktop, isTablet });
// 渲染帖子列表 - Twitter 风格无边框
const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string, blockedTitle?: string, blockedDesc?: string) => {
if (isBlockedProfile && blockedTitle) {
// 当前显示的帖子列表
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={blockedTitle}
description={blockedDesc || ''}
title="已将该用户拉黑"
description="你已将此用户拉黑,不再显示其帖子"
icon="account-off-outline"
variant="modern"
/>
);
}
if (postList.length === 0) {
return (
<EmptyState
title={emptyTitle}
description={emptyDesc}
icon={activeTab === 0 ? 'file-document-edit-outline' : 'bookmark-heart-outline'}
variant="modern"
/>
);
}
const emptyTitle = activeTab === 0
? (mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子')
: (mode === 'self' ? '还没有收藏' : '这个用户还没有收藏任何帖子');
const emptyDesc = activeTab === 0
? (mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '')
: (mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : '');
return (
<View style={sharedStyles.postsContainer}>
{postList.map((post, index) => {
const isPostAuthor = currentUser?.id === post.author?.id;
return (
<View key={post.id} style={[
sharedStyles.postWrapper,
index === postList.length - 1 && sharedStyles.lastPost,
]}>
<PostCard
post={post}
onAction={(action) => handlePostAction(post, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
})}
</View>
);
}, [currentUser?.id, handlePostAction, activeTab, isBlockedProfile]);
// 渲染内容
const renderContent = useCallback(() => {
if (loading) return <Loading />;
if (activeTab === 0) {
return renderPostList(
posts,
mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子',
mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '',
mode === 'other' ? '已将该用户拉黑' : undefined,
mode === 'other' ? '你已将此用户拉黑,不再显示其帖子' : undefined
);
}
if (activeTab === 1) {
return renderPostList(
favorites,
mode === 'self' ? '还没有收藏' : '这个用户还没有收藏任何帖子',
mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : ''
);
}
return null;
}, [loading, activeTab, posts, favorites, mode, renderPostList]);
// 渲染用户信息头部
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}
<EmptyState
title={emptyTitle}
description={emptyDesc}
icon={activeTab === 0 ? 'file-document-edit-outline' : 'bookmark-heart-outline'}
variant="modern"
/>
);
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
}, [loading, activeTab, mode, isBlockedProfile]);
// 渲染 TabBar 和内容
const renderTabBarAndContent = useMemo(() => (
<>
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
// 渲染 FlashList 头部(用户信息 + TabBar
const renderListHeader = useCallback(() => {
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}
/>
</View>
<View style={sharedStyles.contentContainer}>
{renderContent()}
</View>
</>
), [activeTab, renderContent]);
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
</>
);
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress, activeTab, setActiveTab]);
// 未登录/用户不存在状态
if (mode === 'self' && !currentUser) {
@@ -209,18 +187,40 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
/>
}
>
{renderUserHeader}
{renderListHeader()}
</ScrollView>
</View>
{/* 右侧:帖子列表 */}
<View style={sharedStyles.desktopContent}>
<ScrollView
<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}
contentContainerStyle={[sharedStyles.desktopScrollContent, { paddingBottom: scrollBottomInset }]}
>
{renderTabBarAndContent}
</ScrollView>
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
drawDistance={250}
/>
</View>
</View>
</ResponsiveContainer>
@@ -231,7 +231,13 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
// 移动端使用单栏布局
return (
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
<ScrollView
<FlashList
data={currentPosts}
renderItem={renderPostItem}
keyExtractor={postKeyExtractor}
ListHeaderComponent={renderListHeader}
ListEmptyComponent={renderEmptyPosts}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
@@ -241,11 +247,8 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
tintColor={colors.primary.main}
/>
}
contentContainerStyle={[sharedStyles.scrollContent, { paddingBottom: scrollBottomInset }]}
>
{renderUserHeader}
{renderTabBarAndContent}
</ScrollView>
drawDistance={250}
/>
</SafeAreaView>
);
};