Files
frontend/src/components/business/SystemMessageItem.tsx

496 lines
15 KiB
TypeScript
Raw Normal View History

/**
* SystemMessageItem -
*
*
*/
import React, { useMemo } from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
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
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;
2026-03-16 17:47:10 +08:00
// 判断是否显示操作者头像
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}>
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
{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;