feat(business): implement block-based content editing and rich text rendering
Introduce a new `BlockEditor` component and upgrade the post/message rendering system to support rich text segments (images, @mentions, votes, and post references). - Implement `BlockEditor` for long-form post creation with image embedding support. - Upgrade `PostContentRenderer` and `SegmentRenderer` to handle complex segment types including inline images and block elements. - Refactor `PostCard` to use segment-based content and image signatures for optimized memoization. - Centralize segment partitioning logic in `segmentUtils.ts` to ensure consistent rendering across chat and post modules. - Update `PostRepository` and `Post` entity to support the new `segments` data structure.
This commit is contained in:
118
src/components/business/BlockEditor/BlockEditor.tsx
Normal file
118
src/components/business/BlockEditor/BlockEditor.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useImperativeHandle, useCallback } from 'react';
|
||||
import { View, ScrollView, StyleSheet } from 'react-native';
|
||||
|
||||
import { useAppColors, spacing } from '../../../theme';
|
||||
import { MessageSegment } from '../../../types';
|
||||
import TextBlockInput from './TextBlockInput';
|
||||
import ImageBlockView from './ImageBlockView';
|
||||
import { useBlockEditor } from './useBlockEditor';
|
||||
|
||||
export interface BlockEditorHandle {
|
||||
insertImage: () => Promise<void>;
|
||||
insertTextAtCursor: (text: string) => void;
|
||||
getSegments: () => MessageSegment[];
|
||||
getContent: () => string;
|
||||
hasUploadingImages: () => boolean;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
interface BlockEditorProps {
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
style?: any;
|
||||
textStyle?: any;
|
||||
onContentChange?: (content: string) => void;
|
||||
}
|
||||
|
||||
const BlockEditor = React.forwardRef<BlockEditorHandle, BlockEditorProps>(
|
||||
({ placeholder = '添加正文', maxLength = 2000, style, textStyle, onContentChange }, ref) => {
|
||||
const colors = useAppColors();
|
||||
const editor = useBlockEditor();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
insertImage: editor.insertImage,
|
||||
insertTextAtCursor: editor.insertTextAtCursor,
|
||||
getSegments: editor.getSegments,
|
||||
getContent: editor.getContent,
|
||||
hasUploadingImages: editor.hasUploadingImages,
|
||||
focus: () => {
|
||||
const firstTextBlock = editor.blocks.find(b => b.type === 'text');
|
||||
if (firstTextBlock) {
|
||||
editor.focusBlock(firstTextBlock.id);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const handleTextChange = useCallback(
|
||||
(blockId: string, text: string, mentions: any) => {
|
||||
editor.updateTextBlock(blockId, text, mentions);
|
||||
onContentChange?.(editor.getContent());
|
||||
},
|
||||
[editor.updateTextBlock, editor.getContent, onContentChange],
|
||||
);
|
||||
|
||||
const handleRemoveImage = useCallback(
|
||||
(blockId: string) => {
|
||||
editor.removeImageBlock(blockId);
|
||||
onContentChange?.(editor.getContent());
|
||||
},
|
||||
[editor.removeImageBlock, editor.getContent, onContentChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
nestedScrollEnabled
|
||||
>
|
||||
{editor.blocks.map(block => {
|
||||
if (block.type === 'text') {
|
||||
return (
|
||||
<TextBlockInput
|
||||
key={block.id}
|
||||
block={block}
|
||||
isActive={editor.activeBlockId === block.id}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
onTextChange={handleTextChange}
|
||||
onFocusBlock={editor.setActiveBlockId}
|
||||
onSelectMention={() => {}}
|
||||
inputRef={(ref) => editor.registerInputRef(block.id, ref)}
|
||||
style={textStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageBlockView
|
||||
key={block.id}
|
||||
block={block}
|
||||
onRemove={handleRemoveImage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BlockEditor.displayName = 'BlockEditor';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: spacing.xs,
|
||||
},
|
||||
});
|
||||
|
||||
export default BlockEditor;
|
||||
Reference in New Issue
Block a user