feat(post): add post reference functionality with inline cards and suggestion search
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:
@@ -7,7 +7,6 @@
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
Clipboard,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
@@ -253,7 +253,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
const [isFollowingMe, setIsFollowingMe] = useState(false);
|
||||
const [isFollowLoading, setIsFollowLoading] = useState(false);
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const flatListRef = useRef<FlashListRef<Comment> | null>(null);
|
||||
const hasRecordedView = useRef(false); // 是否已记录浏览量
|
||||
|
||||
// 举报相关状态
|
||||
@@ -1136,6 +1136,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
<View style={styles.titleContainer}>
|
||||
<Text
|
||||
variant="h2"
|
||||
selectable
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
@@ -1361,7 +1362,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
// 渲染评论 - 带楼层号
|
||||
const commentKeyExtractor = useCallback((item: Comment) => item.id, []);
|
||||
|
||||
const renderComment = useCallback(({ item, index }: { item: Comment; index: number }) => {
|
||||
const renderComment = useCallback<ListRenderItem<Comment>>(({ item, index }) => {
|
||||
const authorId = item.author?.id || '';
|
||||
// 收集当前评论的所有回复,用于根据 target_id 查找被回复用户
|
||||
const allReplies = item.replies || [];
|
||||
@@ -1597,10 +1598,10 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Emoji Panel —— 使用 FlatList 虚拟化渲染,解决显示不全和卡顿 */}
|
||||
{/* Emoji Panel —— 使用 FlashList 虚拟化渲染,解决显示不全和卡顿 */}
|
||||
{showEmojiPanel && (
|
||||
<View style={styles.emojiPanel}>
|
||||
<FlatList
|
||||
<FlashList
|
||||
data={EMOJI_ROWS}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
@@ -1619,14 +1620,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
)}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
windowSize={5}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: EMOJI_ROW_HEIGHT,
|
||||
offset: EMOJI_ROW_HEIGHT * index,
|
||||
index,
|
||||
})}
|
||||
drawDistance={100}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -1637,7 +1631,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
// 渲染评论列表
|
||||
const renderCommentsList = () => (
|
||||
<FlatList
|
||||
<FlashList
|
||||
ref={flatListRef}
|
||||
data={comments}
|
||||
renderItem={renderComment}
|
||||
@@ -1645,11 +1639,6 @@ export const PostDetailScreen: React.FC = () => {
|
||||
ListEmptyComponent={renderEmptyComments}
|
||||
contentContainerStyle={{ paddingBottom: responsiveGap }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScrollToIndexFailed={() => {
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPostRefreshing || isCommentsRefreshing}
|
||||
@@ -1660,11 +1649,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
onEndReached={handleLoadMoreComments}
|
||||
onEndReachedThreshold={0.3}
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={60}
|
||||
windowSize={7}
|
||||
removeClippedSubviews
|
||||
drawDistance={250}
|
||||
ListFooterComponent={
|
||||
isCommentsInitialLoading ? (
|
||||
<View style={styles.commentsLoadingFooter}>
|
||||
@@ -1735,8 +1720,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
sidebarPosition="right"
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
<ScrollView
|
||||
style={styles.flex}
|
||||
<FlashList
|
||||
ref={flatListRef}
|
||||
data={comments}
|
||||
renderItem={renderComment}
|
||||
keyExtractor={commentKeyExtractor}
|
||||
ListHeaderComponent={renderPostHeader}
|
||||
ListEmptyComponent={renderEmptyComments}
|
||||
contentContainerStyle={{ paddingBottom: responsivePadding }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
@@ -1747,10 +1737,34 @@ export const PostDetailScreen: React.FC = () => {
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{renderPostHeader()}
|
||||
{renderCommentsList()}
|
||||
</ScrollView>
|
||||
onEndReached={handleLoadMoreComments}
|
||||
onEndReachedThreshold={0.3}
|
||||
drawDistance={250}
|
||||
ListFooterComponent={
|
||||
isCommentsInitialLoading ? (
|
||||
<View style={styles.commentsLoadingFooter}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
) : isCommentsLoadingMore ? (
|
||||
<View style={styles.commentsLoadingFooter}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
) : hasMoreComments ? (
|
||||
<TouchableOpacity
|
||||
style={styles.loadMoreButton}
|
||||
onPress={handleLoadMoreComments}
|
||||
>
|
||||
<Text variant="caption" color={colors.primary.main}>
|
||||
加载更多评论
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (comments?.length ?? 0) > 0 ? (
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
|
||||
没有更多评论了
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{renderCommentInput()}
|
||||
</View>
|
||||
</AdaptiveLayout>
|
||||
@@ -1778,7 +1792,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={88}
|
||||
>
|
||||
<FlatList
|
||||
<FlashList
|
||||
ref={flatListRef}
|
||||
data={comments}
|
||||
renderItem={renderComment}
|
||||
@@ -1787,11 +1801,6 @@ export const PostDetailScreen: React.FC = () => {
|
||||
ListEmptyComponent={renderEmptyComments}
|
||||
contentContainerStyle={{ paddingBottom: responsiveGap }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScrollToIndexFailed={() => {
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPostRefreshing || isCommentsRefreshing}
|
||||
@@ -1802,11 +1811,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
onEndReached={handleLoadMoreComments}
|
||||
onEndReachedThreshold={0.3}
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={60}
|
||||
windowSize={7}
|
||||
removeClippedSubviews
|
||||
drawDistance={250}
|
||||
ListFooterComponent={
|
||||
isCommentsInitialLoading ? (
|
||||
<View style={styles.commentsLoadingFooter}>
|
||||
|
||||
Reference in New Issue
Block a user