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

@@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import { View, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import Text from '../common/Text';
import { useAppColors, spacing, borderRadius } from '../../theme';
import type { PostRefSegmentData } from '../../types';
interface PostRefCardProps {
data: PostRefSegmentData;
compact?: boolean;
onPress?: (postId: string) => void;
}
const PostRefCard: React.FC<PostRefCardProps> = ({ data, compact = false, onPress }) => {
const colors = useAppColors();
const router = useRouter();
const handlePress = useCallback(() => {
if (onPress) {
onPress(data.post_id);
} else {
router.push(`/post/${data.post_id}` as any);
}
}, [data.post_id, onPress, router]);
const isDeleted = data.status === 'deleted';
const isHidden = data.status === 'hidden';
const isUnavailable = isDeleted || isHidden || !data.accessible;
const authorName = data.author?.nickname || '';
if (isUnavailable) {
return (
<View
style={[
styles.container,
{ backgroundColor: colors.background.default, borderColor: colors.divider },
compact && styles.containerCompact,
]}
>
<MaterialCommunityIcons name="link-off" size={16} color={colors.text.hint} />
<Text style={[styles.unavailableText, { color: colors.text.hint }]} numberOfLines={1}>
{isDeleted ? '该帖子已删除' : isHidden ? '该帖子不可见' : '请登录后查看'}
</Text>
</View>
);
}
return (
<TouchableOpacity
style={[
styles.container,
{ backgroundColor: colors.background.default, borderColor: colors.primary.light },
compact && styles.containerCompact,
]}
onPress={handlePress}
activeOpacity={0.7}
>
<View style={[styles.iconWrap, { backgroundColor: colors.primary.light }]}>
<MaterialCommunityIcons name="file-document-outline" size={14} color={colors.primary.main} />
</View>
<View style={styles.contentWrap}>
<Text
style={[styles.title, { color: colors.text.primary }]}
numberOfLines={compact ? 1 : 2}
>
{data.title || '帖子'}
</Text>
{authorName ? (
<Text style={[styles.author, { color: colors.text.secondary }]} numberOfLines={1}>
@{authorName}
</Text>
) : null}
</View>
<MaterialCommunityIcons name="chevron-right" size={16} color={colors.text.hint} />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: borderRadius.md,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
marginTop: spacing.xs,
marginBottom: spacing.xs,
},
containerCompact: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
},
iconWrap: {
width: 24,
height: 24,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.sm,
},
contentWrap: {
flex: 1,
justifyContent: 'center',
},
title: {
fontSize: 13,
fontWeight: '500',
lineHeight: 18,
},
author: {
fontSize: 11,
lineHeight: 14,
marginTop: 1,
},
unavailableText: {
fontSize: 13,
marginLeft: spacing.sm,
},
});
export default PostRefCard;