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:
124
src/components/business/PostRefCard.tsx
Normal file
124
src/components/business/PostRefCard.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user