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

@@ -1,10 +1,65 @@
/**
* Segment text extraction utilities
* Segment utilities
*/
import type { MessageSegment, TextSegmentData, AtSegmentData, FileSegmentData, FaceSegmentData, LinkSegmentData, PostRefSegmentData } from './message';
import type { UserDTO } from './user';
// ==================== Chunk Types ====================
export type ContentChunk =
| { kind: 'inline'; parts: MessageSegment[] }
| { kind: 'images'; parts: MessageSegment[] }
| { kind: 'block'; segment: MessageSegment };
/**
* 将 segments 分成三种 chunk
* - inline: text / at / face 等行内元素
* - images: 连续的图片 segment 组合
* - block: vote / post_ref / video / file / link / voice 等块级元素
*
* 与聊天和帖子详情共用同一套分组逻辑。
*/
export function partitionSegments(segments: MessageSegment[]): ContentChunk[] {
const out: ContentChunk[] = [];
let inlineBuf: MessageSegment[] = [];
const flushInline = () => {
if (inlineBuf.length) {
out.push({ kind: 'inline', parts: [...inlineBuf] });
inlineBuf = [];
}
};
for (const s of segments) {
if (s.type === 'image') {
flushInline();
const last = out[out.length - 1];
if (last?.kind === 'images') {
last.parts.push(s);
} else {
out.push({ kind: 'images', parts: [s] });
}
} else if (
s.type === 'vote' ||
s.type === 'post_ref' ||
s.type === 'video' ||
s.type === 'file' ||
s.type === 'link' ||
s.type === 'voice'
) {
flushInline();
out.push({ kind: 'block', segment: s });
} else if (s.type === 'text' || s.type === 'at' || s.type === 'face') {
inlineBuf.push(s);
} else {
// 未知类型也当作 inline 处理,避免丢失内容
inlineBuf.push(s);
}
}
flushInline();
return out;
}
/**
* Extract plain text from message segments for display
*/