expo-popcore-app/components/message/MessageCard.tsx

269 lines
6.7 KiB
TypeScript

import React from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
Image,
} from 'react-native'
export interface MessageData {
subType?: string
templateId?: string
templateTitle?: string
generationId?: string
resultUrl?: string[]
webpPreviewUrl?: string
likerId?: string
likerName?: string
likerAvatar?: string
commentId?: string
credits?: number
balanceAfter?: number
originalComment?: string
}
export interface Message {
id: string
type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING'
title: string
content: string
data?: MessageData
link?: string
priority?: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW'
isRead: boolean
readAt?: string
createdAt: Date | string
}
interface MessageCardProps {
message: Message
onPress: (message: Message) => void
onDelete?: (id: string) => void
}
const MESSAGE_TYPE_CONFIG: Record<string, {
icon: string
showPreview: boolean
showAvatar: boolean
showQuote: boolean
}> = {
TEMPLATE_GENERATION_SUCCESS: { icon: '🎉', showPreview: true, showAvatar: false, showQuote: false },
TEMPLATE_GENERATION_FAILED: { icon: '❌', showPreview: false, showAvatar: false, showQuote: false },
TEMPLATE_LIKED: { icon: '👍', showPreview: false, showAvatar: true, showQuote: false },
TEMPLATE_FAVORITED: { icon: '⭐', showPreview: false, showAvatar: true, showQuote: false },
TEMPLATE_COMMENTED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: false },
COMMENT_REPLIED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: true },
CREDITS_DEDUCTED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
CREDITS_REFUNDED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
CREDITS_RECHARGED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
ANNOUNCEMENT: { icon: '📢', showPreview: false, showAvatar: false, showQuote: false },
}
const DEFAULT_CONFIG = { icon: '📩', showPreview: false, showAvatar: false, showQuote: false }
export const getMessageIcon = (subType?: string): string => {
if (!subType) return DEFAULT_CONFIG.icon
return MESSAGE_TYPE_CONFIG[subType]?.icon ?? DEFAULT_CONFIG.icon
}
export const getMessageConfig = (subType?: string) => {
if (!subType) return DEFAULT_CONFIG
return MESSAGE_TYPE_CONFIG[subType] ?? DEFAULT_CONFIG
}
export const formatRelativeTime = (date: Date | string): string => {
const now = new Date()
const targetDate = typeof date === 'string' ? new Date(date) : date
const diffMs = now.getTime() - targetDate.getTime()
const diffMinutes = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMinutes < 1) {
return '刚刚'
}
if (diffMinutes < 60) {
return `${diffMinutes}分钟前`
}
if (diffHours < 24) {
return `${diffHours}小时前`
}
if (diffDays < 2) {
const hours = String(targetDate.getHours()).padStart(2, '0')
const minutes = String(targetDate.getMinutes()).padStart(2, '0')
return `昨天 ${hours}:${minutes}`
}
const year = targetDate.getFullYear()
const month = String(targetDate.getMonth() + 1).padStart(2, '0')
const day = String(targetDate.getDate()).padStart(2, '0')
const hours = String(targetDate.getHours()).padStart(2, '0')
const minutes = String(targetDate.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
export const MessageCard: React.FC<MessageCardProps> = ({
message,
onPress,
onDelete,
}) => {
const config = getMessageConfig(message.data?.subType)
const showQuote = config.showQuote && message.data?.originalComment
const handlePress = () => {
onPress(message)
}
const handleDelete = () => {
if (onDelete) {
onDelete(message.id)
}
}
return (
<Pressable
testID="message-card"
style={styles.container}
onPress={handlePress}
>
{/* Unread Indicator */}
{!message.isRead && (
<View testID="unread-indicator" style={styles.unreadIndicatorContainer}>
<View style={styles.unreadIndicator} />
</View>
)}
<View style={styles.contentWrapper}>
{/* Middle: Content */}
<View style={styles.middleSection}>
<Text style={styles.title}>{message.title}</Text>
<Text style={styles.content} numberOfLines={2}>
{message.content}
</Text>
{/* Quote */}
{showQuote && (
<View testID="quote-container" style={styles.quoteContainer}>
<Text style={styles.quoteText} numberOfLines={2}>
{message.data?.originalComment}
</Text>
</View>
)}
<Text style={styles.time}>
{formatRelativeTime(message.createdAt)}
</Text>
</View>
{/* Right: Delete Button (if onDelete provided) */}
{onDelete && (
<Pressable
testID="delete-button"
style={styles.deleteButton}
onPress={handleDelete}
>
<Text style={styles.deleteText}></Text>
</Pressable>
)}
</View>
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#16181B',
borderRadius: 16,
padding: 16,
marginBottom: 12,
marginHorizontal: 4,
position: 'relative',
},
unreadIndicatorContainer: {
position: 'absolute',
top: -4,
right: -2,
width: 16,
height: 16,
borderWidth: 4,
borderColor: '#090A0B',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
unreadIndicator: {
width: 6,
height: 6,
borderRadius: 4,
backgroundColor: '#00FF66',
},
contentWrapper: {
flexDirection: 'row',
alignItems: 'flex-start',
},
leftSection: {
marginRight: 12,
},
icon: {
fontSize: 24,
},
avatar: {
width: 40,
height: 40,
borderRadius: 20,
},
middleSection: {
flex: 1,
},
title: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '600',
marginBottom: 4,
},
content: {
color: '#ABABAB',
fontSize: 13,
marginBottom: 8,
lineHeight: 18,
},
previewImage: {
width: 80,
height: 80,
borderRadius: 8,
marginBottom: 8,
},
quoteContainer: {
backgroundColor: '#26292E',
borderRadius: 8,
padding: 8,
marginBottom: 8,
},
quoteText: {
color: '#8A8A8A',
fontSize: 12,
},
time: {
color: '#8A8A8A',
fontSize: 11,
},
deleteButton: {
backgroundColor: '#FF3B30',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
marginLeft: 8,
},
deleteText: {
color: '#FFFFFF',
fontSize: 12,
},
})
export default MessageCard