Files
frontend/src/components/business/SystemMessageItem.tsx
lan 3196972596
All checks were successful
Frontend CI / ota-android (push) Successful in 1m40s
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / build-and-push-web (push) Successful in 3m30s
Frontend CI / build-android-apk (push) Successful in 1h1m8s
refactor: restructure core architecture and responsive system
This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase.

Key changes include:
- **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience.
- **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`).
- **Data Layer Improvements**:
    - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`.
    - Cleaned up repository implementations by removing redundant local utility functions.
    - Refactored `MessageMapper` to use the new centralized JSON utility.
- **Type System Cleanup**:
    - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory.
    - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities.
- **Utility Consolidation**:
    - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components.
    - Removed deprecated responsive utility files in favor of the new hook-based system.
- **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
2026-05-05 19:07:33 +08:00

496 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SystemMessageItem 系统消息项组件 - 扁平化风格
* 根据系统消息类型显示不同图标和内容
* 与登录、注册、设置页面风格保持一致
*/
import React, { useMemo } from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { formatChatTime } from '../../utils/formatTime';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { SystemMessageResponse, SystemMessageType } from '../../types/dto';
import Text from '../common/Text';
import Avatar from '../common/Avatar';
interface SystemMessageItemProps {
message: SystemMessageResponse;
onPress?: () => void;
onAvatarPress?: () => void;
onRequestAction?: (approve: boolean) => void;
requestActionLoading?: boolean;
index?: number; // 用于交错动画
}
const getSystemMessageIcon = (
systemType: SystemMessageType,
colors: AppColors
): { icon: keyof typeof MaterialCommunityIcons.glyphMap; color: string; bgColor: string; gradient: string[] } => {
switch (systemType) {
case 'like_post':
case 'like_comment':
case 'like_reply':
case 'favorite_post':
return {
icon: 'heart',
color: '#FF4757',
bgColor: '#FFE4E6',
gradient: ['#FF4757', '#FF6B7A'],
};
case 'comment':
return {
icon: 'comment-text',
color: '#2196F3',
bgColor: '#E3F2FD',
gradient: ['#2196F3', '#64B5F6'],
};
case 'reply':
return {
icon: 'reply',
color: '#4CAF50',
bgColor: '#E8F5E9',
gradient: ['#4CAF50', '#81C784'],
};
case 'follow':
return {
icon: 'account-plus',
color: '#9C27B0',
bgColor: '#F3E5F5',
gradient: ['#9C27B0', '#BA68C8'],
};
case 'mention':
return {
icon: 'at',
color: '#FF9800',
bgColor: '#FFF3E0',
gradient: ['#FF9800', '#FFB74D'],
};
case 'system':
return {
icon: 'cog',
color: '#607D8B',
bgColor: '#ECEFF1',
gradient: ['#607D8B', '#90A4AE'],
};
case 'announcement':
case 'announce':
return {
icon: 'bullhorn',
color: colors.primary.main,
bgColor: colors.primary.light + '30',
gradient: [colors.primary.main, colors.primary.light],
};
case 'group_invite':
return {
icon: 'account-multiple-plus',
color: '#00BCD4',
bgColor: '#E0F7FA',
gradient: ['#00BCD4', '#4DD0E1'],
};
case 'group_join_apply':
return {
icon: 'account-clock',
color: '#FF9800',
bgColor: '#FFF3E0',
gradient: ['#FF9800', '#FFB74D'],
};
case 'group_join_approved':
return {
icon: 'check-circle',
color: '#4CAF50',
bgColor: '#E8F5E9',
gradient: ['#4CAF50', '#81C784'],
};
case 'group_join_rejected':
return {
icon: 'close-circle',
color: '#F44336',
bgColor: '#FFEBEE',
gradient: ['#F44336', '#E57373'],
};
default:
return {
icon: 'bell',
color: '#757575',
bgColor: '#F5F5F5',
gradient: ['#757575', '#9E9E9E'],
};
}
};
function createSystemMessageStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: 16,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider || '#E5E5EA',
},
unreadContainer: {
backgroundColor: colors.primary.main + '0A',
},
unreadIndicator: {
position: 'absolute',
left: 6,
top: '50%',
marginTop: -4,
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.primary.main,
},
iconContainer: {
marginRight: 14,
},
avatarWrapper: {
position: 'relative',
},
iconWrapper: {
width: 44,
height: 44,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
typeIconBadge: {
position: 'absolute',
bottom: -2,
right: -2,
width: 18,
height: 18,
borderRadius: 9,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
content: {
flex: 1,
justifyContent: 'center',
minWidth: 0,
},
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
titleLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
marginRight: 8,
},
title: {
fontWeight: '500',
fontSize: 15,
color: colors.text.primary,
},
unreadTitle: {
fontWeight: '600',
color: colors.text.primary,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
marginLeft: 6,
},
statusText: {
fontSize: 10,
fontWeight: '500',
marginLeft: 2,
},
time: {
fontSize: 13,
flexShrink: 0,
color: colors.text.hint,
},
messageContent: {
lineHeight: 20,
fontSize: 14,
color: colors.text.secondary,
},
extraInfo: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
backgroundColor: colors.background.default,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 8,
alignSelf: 'flex-start',
},
extraText: {
marginLeft: 6,
flex: 1,
fontWeight: '500',
fontSize: 13,
},
actionsRow: {
flexDirection: 'row',
marginTop: 12,
gap: 10,
},
actionBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 10,
borderWidth: 1,
gap: 4,
},
rejectBtn: {
borderColor: colors.error.main + '40',
backgroundColor: colors.error.light + '20',
},
approveBtn: {
borderColor: colors.success.main + '40',
backgroundColor: colors.success.light + '20',
},
actionBtnText: {
fontWeight: '500',
fontSize: 13,
},
arrowContainer: {
marginLeft: 8,
justifyContent: 'center',
alignItems: 'center',
height: 20,
width: 20,
},
});
}
// 获取系统消息标题
const getSystemMessageTitle = (message: SystemMessageResponse): string => {
const { system_type, extra_data } = message;
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
const groupName = extra_data?.group_name || extra_data?.target_title;
switch (system_type) {
case 'like_post':
return operatorName ? `${operatorName} 赞了你的帖子` : '有人赞了你的帖子';
case 'like_comment':
return operatorName ? `${operatorName} 赞了你的评论` : '有人赞了你的评论';
case 'like_reply':
return operatorName ? `${operatorName} 赞了你的回复` : '有人赞了你的回复';
case 'favorite_post':
return operatorName ? `${operatorName} 收藏了你的帖子` : '有人收藏了你的帖子';
case 'comment':
return operatorName ? `${operatorName} 评论了你的帖子` : '有人评论了你的帖子';
case 'reply':
return operatorName ? `${operatorName} 回复了你` : '有人回复了你';
case 'follow':
return operatorName ? `${operatorName} 关注了你` : '有人关注了你';
case 'mention':
return operatorName ? `${operatorName} @提到了你` : '有人@提到了你';
case 'system':
return '系统通知';
case 'announcement':
return '公告';
case 'group_invite':
return operatorName
? `${operatorName} 邀请加入群聊 ${groupName || ''}`.trim()
: `收到群邀请${groupName ? `${groupName}` : ''}`;
case 'group_join_apply':
if (extra_data?.request_status === 'accepted') {
return operatorName ? `${operatorName} 已同意` : '该请求已同意';
}
if (extra_data?.request_status === 'rejected') {
return operatorName ? `${operatorName} 已拒绝` : '该请求已拒绝';
}
return operatorName
? `${operatorName} 申请加入群聊 ${groupName || ''}`.trim()
: `收到加群申请${groupName ? `${groupName}` : ''}`;
case 'group_join_approved':
return '加群申请已通过';
case 'group_join_rejected':
return '加群申请被拒绝';
default:
return '通知';
}
};
const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
message,
onPress,
onAvatarPress,
onRequestAction,
requestActionLoading = false,
index = 0,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createSystemMessageStyles(colors), [colors]);
const { icon, color, bgColor } = getSystemMessageIcon(message.system_type, colors);
const title = getSystemMessageTitle(message);
const { extra_data } = message;
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
const operatorAvatar = extra_data?.avatar_url || extra_data?.operator_avatar;
const groupAvatar = extra_data?.group_avatar;
const requestStatus = extra_data?.request_status;
const isActionable =
(message.system_type === 'group_invite' || message.system_type === 'group_join_apply') &&
requestStatus === 'pending' &&
!!onRequestAction;
const isUnread = message.is_read !== true;
// 判断是否显示操作者头像
const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes(
message.system_type
);
const showGroupAvatar = message.system_type === 'group_invite';
// 获取状态标签
const getStatusBadge = () => {
if (requestStatus === 'accepted') {
return (
<View style={[styles.statusBadge, { backgroundColor: `${colors.success.main}22` }]}>
<MaterialCommunityIcons name="check" size={10} color={colors.success.main} />
<Text variant="caption" color={colors.success.main} style={styles.statusText}></Text>
</View>
);
}
if (requestStatus === 'rejected') {
return (
<View style={[styles.statusBadge, { backgroundColor: `${colors.error.main}22` }]}>
<MaterialCommunityIcons name="close" size={10} color={colors.error.main} />
<Text variant="caption" color={colors.error.main} style={styles.statusText}></Text>
</View>
);
}
return null;
};
return (
<TouchableOpacity
style={[
styles.container,
isUnread && styles.unreadContainer,
]}
onPress={onPress}
activeOpacity={onPress ? 0.85 : 1}
disabled={!onPress}
>
{/* 左侧未读指示器 */}
{isUnread && <View style={styles.unreadIndicator} />}
{/* 图标/头像区域 */}
<View style={styles.iconContainer}>
{showGroupAvatar ? (
<View style={styles.avatarWrapper}>
<Avatar
source={groupAvatar || ''}
size={48}
name={extra_data?.group_name || '群聊'}
/>
<View style={[styles.typeIconBadge, { backgroundColor: bgColor }]}>
<MaterialCommunityIcons name={icon} size={12} color={color} />
</View>
</View>
) : showOperatorAvatar ? (
<View style={styles.avatarWrapper}>
<Avatar
source={operatorAvatar || ''}
size={48}
name={operatorName}
onPress={onAvatarPress}
/>
<View style={[styles.typeIconBadge, { backgroundColor: bgColor }]}>
<MaterialCommunityIcons name={icon} size={12} color={color} />
</View>
</View>
) : (
<View style={[styles.iconWrapper, { backgroundColor: bgColor }]}>
<MaterialCommunityIcons name={icon} size={24} color={color} />
</View>
)}
</View>
{/* 内容区域 */}
<View style={styles.content}>
{/* 标题行 */}
<View style={styles.titleRow}>
<View style={styles.titleLeft}>
<Text
variant="body"
style={[styles.title, ...(isUnread ? [styles.unreadTitle] : [])]}
numberOfLines={1}
>
{title}
</Text>
{getStatusBadge()}
</View>
<Text variant="caption" color={colors.text.hint} style={styles.time}>
{formatChatTime(message.created_at)}
</Text>
</View>
{/* 消息内容 */}
<Text
variant="caption"
color={colors.text.secondary}
numberOfLines={2}
style={styles.messageContent}
>
{message.content}
</Text>
{/* 附加信息 */}
{(extra_data?.target_title || extra_data?.post_title || extra_data?.comment_preview) &&
!['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected'].includes(message.system_type) &&
(() => {
const isCommentType = ['comment', 'reply'].includes(extra_data?.target_type ?? '');
const previewText = extra_data?.target_title || extra_data?.post_title || extra_data?.comment_preview;
const iconName = isCommentType ? 'comment-text-outline' : 'file-document-outline';
return (
<View style={styles.extraInfo}>
<MaterialCommunityIcons name={iconName} size={12} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} numberOfLines={1} style={styles.extraText}>
{previewText}
</Text>
</View>
);
})()}
{/* 操作按钮 */}
{isActionable && (
<View style={styles.actionsRow}>
<TouchableOpacity
style={[styles.actionBtn, styles.rejectBtn]}
onPress={() => onRequestAction?.(false)}
disabled={requestActionLoading}
>
<MaterialCommunityIcons name="close" size={14} color={colors.error.main} />
<Text variant="caption" color={colors.error.main} style={styles.actionBtnText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, styles.approveBtn]}
onPress={() => onRequestAction?.(true)}
disabled={requestActionLoading}
>
<MaterialCommunityIcons name="check" size={14} color={colors.success.main} />
<Text variant="caption" color={colors.success.main} style={styles.actionBtnText}></Text>
</TouchableOpacity>
</View>
)}
</View>
{/* 右侧箭头 */}
{onPress && (
<View style={styles.arrowContainer}>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</View>
)}
</TouchableOpacity>
);
};
export default SystemMessageItem;