feat(business): implement block-based content editing and rich text rendering
All checks were successful
Frontend CI / ota-android (push) Successful in 1m18s
Frontend CI / ota-ios (push) Successful in 1m31s
Frontend CI / build-and-push-web (push) Successful in 2m49s
Frontend CI / build-android-apk (push) Successful in 1h18m20s

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:
2026-05-08 01:57:05 +08:00
parent ea9e51b0b0
commit d4c3e1f268
18 changed files with 1416 additions and 234 deletions

View 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;