refactor(core): introduce EventBus and refactor store infrastructure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m46s
Frontend CI / ota-android (push) Successful in 10m34s
Frontend CI / build-android-apk (push) Successful in 1h15m8s

- Add EventBus for decoupled event-driven communication between services
- Add EventSubscriber component for centralized event handling
- Add requestDedupe utility for shared request deduplication
- Refactor api/wsService to use eventBus instead of direct router navigation
- Extract MessageMapper.toCachedMessages for consistent message mapping
- Remove deprecated BaseManager and CacheBus classes
- Improve @mention rendering with memberMap support in segment rendering
- Update hooks to use useRef instead of useState for fetch tracking
This commit is contained in:
lafay
2026-04-12 18:14:29 +08:00
parent 4b5ce1ba21
commit 6610d2f173
28 changed files with 378 additions and 512 deletions

View File

@@ -34,6 +34,8 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
onRecall,
onDelete,
onAddSticker,
onReport,
memberMap,
}) => {
const styles = useChatScreenStyles();
const scaleAnimation = useRef(new Animated.Value(0)).current;
@@ -90,7 +92,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
// 复制文本到剪贴板
const handleCopy = () => {
const text = extractTextFromSegments(message.segments);
const text = extractTextFromSegments(message.segments, memberMap);
if (text) {
Clipboard.setString(text);
onClose();
@@ -99,7 +101,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
// 多选功能(这里简化为复制全部)
const handleMultiSelect = () => {
const text = extractTextFromSegments(message.segments);
const text = extractTextFromSegments(message.segments, memberMap);
if (text) {
Clipboard.setString(text);
onClose();

View File

@@ -141,7 +141,7 @@ export const renderSegment = (
};
/**
* 渲染文本 Segment
* 渲染文本 Segment(非内联,块级使用)
*/
const TextSegmentBody: React.FC<{ data: TextSegmentData; isMe: boolean }> = ({ data, isMe }) => {
const styles = useSegmentStyles();
@@ -392,7 +392,7 @@ const AtSegmentBody: React.FC<{
return (
<Text
style={[
styles.atText,
styles.textContent,
isMe ? styles.atTextMe : styles.atTextOther,
isAtMe && styles.atHighlight,
]}
@@ -773,15 +773,47 @@ const MessageSegmentsRendererInner: React.FC<{
{chunks.map((chunk, i) => {
if (chunk.kind === 'inline') {
return (
<View key={`inl-${i}`} style={segStyles.inlineChunk}>
{chunk.parts.map((segment, j) => (
<React.Fragment
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || j}`}
>
{renderSegment(segment, renderProps)}
</React.Fragment>
))}
</View>
<Text key={`inl-${i}`} style={[segStyles.inlineChunkBase, isMe ? segStyles.textMe : segStyles.textOther]} selectable={true}>
{chunk.parts.map((segment, j) => {
if (segment.type === 'text') {
const data = segment.data as TextSegmentData;
return data.content ?? data.text ?? '';
}
if (segment.type === 'at') {
const data = segment.data as AtSegmentData;
const isAtMe = data.user_id === currentUserId || data.user_id === 'all';
let displayName: string;
if (data.user_id === 'all') {
displayName = '所有人';
} else {
const member = memberMap?.get(data.user_id);
displayName = member?.nickname || data.nickname || '用户';
}
return (
<Text
key={`at-${j}`}
style={[
isMe ? segStyles.atTextMe : segStyles.atTextOther,
isAtMe && segStyles.atHighlight,
]}
onPress={() => data.user_id !== 'all' && onAtPress?.(data.user_id)}
>
@{displayName}
</Text>
);
}
if (segment.type === 'face') {
const data = segment.data as FaceSegmentData;
if (data.url) {
return (
<Image key={`face-${j}`} source={{ uri: data.url }} style={segStyles.faceImage} />
);
}
return `[${data.name || `表情${data.id}`}]`;
}
return null;
})}
</Text>
);
}
if (chunk.kind === 'images') {
@@ -836,11 +868,11 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
flexDirection: 'column',
width: '100%',
},
inlineChunk: {
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-end',
width: '100%',
inlineChunkBase: {
fontSize,
lineHeight: Math.round(fontSize * 1.43),
letterSpacing: 0.1,
fontWeight: '400',
},
imagesChunk: {
width: '100%',
@@ -874,11 +906,7 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
color: colors.chat.textPrimary,
},
// @提及 - 与普通文本字号一致,仅颜色区分
atText: {
fontSize,
lineHeight: Math.round(fontSize * 1.43),
},
// @提及 - 仅颜色区分,继承父元素字号
atTextMe: {
color: '#1A5F9E',
},

View File

@@ -166,7 +166,8 @@ export interface LongPressMenuProps {
onRecall: (messageId: string) => void;
onDelete: (messageId: string) => void;
onAddSticker?: (imageUrl: string) => void;
onReport?: (message: GroupMessage) => void; // 举报消息回调
onReport?: (message: GroupMessage) => void;
memberMap?: Map<string, { nickname: string; user_id: string }>;
}
// 聊天头部 Props