feat(content): add rich content rendering with @mentions and vote segments
- Add PostContentRenderer component for rendering posts/comments with segments - Add PostMentionInput component for @mention support in post/comment creation - Add VoteSegmentData and vote segment type for unified post creation API - Update post and comment services to support segments in API requests - Fix auth service to clear tokens before login/register - Improve database initialization with proper close/retry logic - Add verification check before allowing post creation - Add metro web shims for react-native-webrtc full package and requireNativeComponent - Update build config with git hash based version numbers
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 发帖页 CreatePostScreen(响应式适配)
|
||||
* 威友 - 发布新帖子
|
||||
* 参考微博发帖界面设计
|
||||
@@ -38,8 +38,10 @@ import { channelService, postService, showPrompt, voteService } from '../../serv
|
||||
import { ApiError } from '@/services/core';
|
||||
import { uploadService } from '@/services/upload';
|
||||
import VoteEditor from '../../components/business/VoteEditor';
|
||||
import PostMentionInput from '../../components/business/PostMentionInput';
|
||||
import { useResponsive, useResponsiveValue } from '../../hooks';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { MessageSegment } from '../../types';
|
||||
|
||||
// Props 接口
|
||||
interface CreatePostScreenProps {
|
||||
@@ -106,6 +108,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [segments, setSegments] = useState<MessageSegment[]>([]);
|
||||
const [images, setImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
|
||||
const [channelOptions, setChannelOptions] = useState<ChannelOption[]>([]);
|
||||
const [selectedChannelId, setSelectedChannelId] = useState<string | null>(null);
|
||||
@@ -402,6 +405,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
const updated = await postService.updatePost(editPostID, {
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
segments: segments.length > 0 ? segments : undefined,
|
||||
images: imageUrls,
|
||||
});
|
||||
if (!updated) {
|
||||
@@ -415,10 +419,10 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
});
|
||||
router.replace(hrefs.hrefHome());
|
||||
} else {
|
||||
// 创建普通帖子
|
||||
await postService.createPost({
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
segments: segments.length > 0 ? segments : undefined,
|
||||
images: imageUrls,
|
||||
channel_id: selectedChannelId || undefined,
|
||||
});
|
||||
@@ -598,22 +602,18 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
/>
|
||||
|
||||
{/* 正文输入 */}
|
||||
<TextInput
|
||||
ref={contentInputRef}
|
||||
{/* 正文输入(支持 @提及) */}
|
||||
<PostMentionInput
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
onSegmentsChange={setSegments}
|
||||
placeholder="分享新鲜事..."
|
||||
maxLength={MAX_CONTENT_LENGTH}
|
||||
minHeight={contentInputMinHeight}
|
||||
style={[
|
||||
styles.contentInput,
|
||||
isWideScreen && styles.contentInputWide,
|
||||
{ minHeight: contentInputMinHeight },
|
||||
]}
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
placeholder="分享新鲜事..."
|
||||
placeholderTextColor={colors.text.hint}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
maxLength={MAX_CONTENT_LENGTH}
|
||||
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
|
||||
/>
|
||||
|
||||
{/* 作为正文容器的子模块,追加在文本后面 */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 首页 HomeScreen
|
||||
* 威友 - 首页展示
|
||||
* 支持列表和多列网格模式(响应式布局)
|
||||
@@ -27,7 +27,7 @@ import { PagerView } from '../../components/common';
|
||||
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
|
||||
import { Post } from '../../types';
|
||||
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/auth';
|
||||
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
|
||||
import { channelService, postService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
@@ -169,6 +169,10 @@ export const HomeScreen: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const isVerified = useIsVerified();
|
||||
const fetchVerificationStatus = useVerificationStore((s) => s.fetchStatus);
|
||||
const hasFetchedVerification = useVerificationStore((s) => s.hasFetchedStatus);
|
||||
const colors = useAppColors();
|
||||
const resolvedScheme = useResolvedColorScheme();
|
||||
const styles = useMemo(() => createHomeStyles(colors), [colors]);
|
||||
@@ -207,6 +211,12 @@ export const HomeScreen: React.FC = () => {
|
||||
const [showShareSheet, setShowShareSheet] = useState(false);
|
||||
const [sharePost, setSharePost] = useState<Post | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !hasFetchedVerification) {
|
||||
fetchVerificationStatus();
|
||||
}
|
||||
}, [isAuthenticated, hasFetchedVerification, fetchVerificationStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCreatePost) {
|
||||
blurActiveElement();
|
||||
@@ -656,6 +666,14 @@ export const HomeScreen: React.FC = () => {
|
||||
|
||||
// 跳转到发帖页面(使用 Modal 方式)
|
||||
const handleCreatePost = () => {
|
||||
if (!isAuthenticated) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
if (!isVerified) {
|
||||
router.push(hrefs.hrefVerificationGuide());
|
||||
return;
|
||||
}
|
||||
setShowCreatePost(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 帖子详情页 PostDetailScreen - QQ频道风格(响应式版本)
|
||||
* 威友 - 显示完整帖子内容和评论
|
||||
* 桌面端使用双栏布局,移动端使用单栏布局
|
||||
@@ -33,13 +33,13 @@ import {
|
||||
useAppColors,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types';
|
||||
import { Post, Comment, VoteResultDTO, VoteOptionDTO, MessageSegment } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/auth';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { postSyncService } from '@/services/post';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard, ReportDialog, ShareSheet } from '../../components/business';
|
||||
import { CommentItem, VoteCard, ReportDialog, ShareSheet, PostContentRenderer, PostMentionInput } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
|
||||
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { handleError } from '@/services/core';
|
||||
@@ -191,6 +191,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
}, [commentsError, postId]);
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const [commentSegments, setCommentSegments] = useState<MessageSegment[]>([]);
|
||||
const [showImageModal, setShowImageModal] = useState(false);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
const [allImages, setAllImages] = useState<ImageGridItem[]>([]);
|
||||
@@ -741,6 +742,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
root_id: rootId,
|
||||
target_id: parentId,
|
||||
content: commentText.trim(),
|
||||
segments: commentSegments.length > 0 ? commentSegments : undefined,
|
||||
images: uploadedImageUrls.map(url => ({ url })),
|
||||
likes_count: 0,
|
||||
replies_count: 0,
|
||||
@@ -798,6 +800,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
const createdComment = await commentService.createComment({
|
||||
postId: post.id,
|
||||
content: commentText.trim(),
|
||||
segments: commentSegments.length > 0 ? commentSegments : undefined,
|
||||
parentId: parentId || undefined,
|
||||
images: uploadedImageUrls,
|
||||
});
|
||||
@@ -1097,18 +1100,17 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
{/* 内容 - 增大字体 */}
|
||||
<View style={styles.contentContainer}>
|
||||
<Text
|
||||
variant="body"
|
||||
style={[
|
||||
<PostContentRenderer
|
||||
content={post.content}
|
||||
segments={post.segments}
|
||||
textStyle={[
|
||||
styles.content,
|
||||
{
|
||||
fontSize: isDesktop ? fontSizes.xl : isTablet ? fontSizes.lg : fontSizes.lg,
|
||||
lineHeight: isDesktop ? 32 : isTablet ? 30 : 28
|
||||
}
|
||||
]}
|
||||
>
|
||||
{post.content}
|
||||
</Text>
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 图片 - 详情页显示所有图片,不限制数量 */}
|
||||
@@ -1442,15 +1444,14 @@ export const PostDetailScreen: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Text Input */}
|
||||
<TextInput
|
||||
style={styles.expandedTextInput}
|
||||
placeholder="来说点什么吧!"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
<PostMentionInput
|
||||
value={commentText}
|
||||
onChangeText={setCommentText}
|
||||
multiline
|
||||
autoFocus
|
||||
onSegmentsChange={setCommentSegments}
|
||||
placeholder="来说点什么吧!"
|
||||
maxLength={500}
|
||||
minHeight={40}
|
||||
style={styles.expandedTextInput}
|
||||
/>
|
||||
|
||||
{/* Image preview */}
|
||||
|
||||
Reference in New Issue
Block a user