fix(message): add robust NaN validation for date handling across components
Add comprehensive date validation using Number.isNaN() checks to prevent crashes when timestamps are invalid or empty. Apply defensive formatting across CommentItem, SystemMessageItem, MessageBubble, LongPressMenu, and store utilities. Refine message bubble styling: move sender name display to both sides in group chat with distinct `mySenderName` styling, adjust bubble corner radius to top-right for consistent arrow placement, and align message rows to flex-start for improved layout. Simplify group avatar rendering in ConversationListRow.
This commit is contained in:
@@ -223,8 +223,11 @@ const CommentItem: React.FC<CommentItemProps> = ({
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
|
||||
@@ -331,8 +331,11 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
|
||||
@@ -274,8 +274,11 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
<View style={styles.metaItem}>
|
||||
<MaterialCommunityIcons name="calendar-outline" size={14} color={colors.text.hint} />
|
||||
<Text style={styles.metaText}>
|
||||
加入于 {new Date(user.created_at || Date.now()).getFullYear()}年
|
||||
{new Date(user.created_at || Date.now()).getMonth() + 1}月
|
||||
{(() => {
|
||||
const d = new Date(user.created_at || Date.now());
|
||||
if (Number.isNaN(d.getTime())) return '加入于 未知时间';
|
||||
return `加入于 ${d.getFullYear()}年${d.getMonth() + 1}月`;
|
||||
})()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -661,7 +661,10 @@ const GroupInfoScreen: React.FC = () => {
|
||||
{announcements[0].content}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.announcementTime}>
|
||||
{new Date(announcements[0].created_at).toLocaleDateString()}
|
||||
{(() => {
|
||||
const d = new Date(announcements[0].created_at);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
||||
})()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -245,8 +245,10 @@ export const MessageListScreen: React.FC = () => {
|
||||
|
||||
// 格式化时间(稳定引用,配合会话行 memo)
|
||||
const formatTime = useCallback((dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
@@ -483,7 +485,10 @@ export const MessageListScreen: React.FC = () => {
|
||||
const aPinned = a.is_pinned ? 1 : 0;
|
||||
const bPinned = b.is_pinned ? 1 : 0;
|
||||
if (aPinned !== bPinned) return bPinned - aPinned;
|
||||
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
||||
const aTime = new Date(a.updated_at || 0).getTime();
|
||||
const bTime = new Date(b.updated_at || 0).getTime();
|
||||
if (Number.isNaN(aTime) || Number.isNaN(bTime)) return 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
const listConvRef = useRef(new Map<string, ConversationResponse>());
|
||||
|
||||
@@ -241,7 +241,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
<View style={styles.noticeBox}>
|
||||
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
||||
<Text style={styles.noticeTime}>
|
||||
{new Date(announcements[0].created_at).toLocaleDateString()}
|
||||
{(() => {
|
||||
const d = new Date(announcements[0].created_at);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
||||
})()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -311,7 +314,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
<Text style={styles.infoLabel}>创建时间</Text>
|
||||
<Text style={styles.infoValue}>
|
||||
{groupInfo?.created_at
|
||||
? new Date(groupInfo.created_at).toLocaleDateString('zh-CN')
|
||||
? (() => {
|
||||
const d = new Date(groupInfo.created_at);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleDateString('zh-CN');
|
||||
})()
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -246,7 +246,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
<View style={styles.noticeBox}>
|
||||
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
||||
<Text style={styles.noticeTime}>
|
||||
{new Date(announcements[0].created_at).toLocaleDateString()}
|
||||
{(() => {
|
||||
const d = new Date(announcements[0].created_at);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
||||
})()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -316,7 +319,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
<Text style={styles.infoLabel}>创建时间</Text>
|
||||
<Text style={styles.infoValue}>
|
||||
{groupInfo?.created_at
|
||||
? new Date(groupInfo.created_at).toLocaleDateString('zh-CN')
|
||||
? (() => {
|
||||
const d = new Date(groupInfo.created_at);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleDateString('zh-CN');
|
||||
})()
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -78,10 +78,12 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
||||
if (!message) return null;
|
||||
|
||||
const isMyMessage = message.sender_id === currentUserId;
|
||||
const messageTime = new Date(message.created_at).getTime();
|
||||
const messageDate = new Date(message.created_at);
|
||||
const messageTime = Number.isNaN(messageDate.getTime()) ? 0 : messageDate.getTime();
|
||||
const now = Date.now();
|
||||
const canRecall = isMyMessage &&
|
||||
message.status !== 'recalled' &&
|
||||
messageTime > 0 &&
|
||||
(now - messageTime) < RECALL_TIME_LIMIT;
|
||||
|
||||
// 检查是否是图片消息
|
||||
|
||||
@@ -426,8 +426,8 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
|
||||
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
||||
{/* 群聊模式:显示发送者昵称 */}
|
||||
{isGroupChat && !isMe && senderInfo && (
|
||||
<Text style={styles.senderName}>{senderInfo.nickname}</Text>
|
||||
{isGroupChat && senderInfo && (
|
||||
<Text style={[styles.senderName, isMe && styles.mySenderName]}>{senderInfo.nickname}</Text>
|
||||
)}
|
||||
|
||||
{renderMessageContent()}
|
||||
|
||||
@@ -262,14 +262,13 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: spacing.md,
|
||||
alignItems: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
myMessageRow: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
theirMessageRow: {
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
|
||||
// 头像 - 更大一点
|
||||
@@ -303,6 +302,11 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
marginLeft: 4,
|
||||
fontWeight: '600',
|
||||
},
|
||||
mySenderName: {
|
||||
marginLeft: 0,
|
||||
marginRight: 4,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
|
||||
// 消息气泡 - 动态圆角支持
|
||||
messageBubbleOuter: {
|
||||
@@ -315,7 +319,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
elevation: 1,
|
||||
},
|
||||
myBubbleOuter: {
|
||||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3), // 微信的小尾巴更尖
|
||||
borderTopRightRadius: Math.max(4, messageRadius * 0.3), // 箭头在右上
|
||||
},
|
||||
theirBubbleOuter: {
|
||||
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
|
||||
@@ -330,7 +334,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
},
|
||||
myBubbleInner: {
|
||||
backgroundColor: myBubbleBg,
|
||||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
|
||||
borderTopRightRadius: Math.max(4, messageRadius * 0.3),
|
||||
},
|
||||
theirBubbleInner: {
|
||||
backgroundColor: theirBubbleBg,
|
||||
@@ -394,7 +398,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
elevation: 4,
|
||||
},
|
||||
myImageBubble: {
|
||||
borderBottomRightRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
},
|
||||
theirImageBubble: {
|
||||
borderTopLeftRadius: 8,
|
||||
|
||||
@@ -657,8 +657,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = useCallback((dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
@@ -680,9 +682,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
if (index === 0) return true;
|
||||
const currentMsg = messages[index];
|
||||
const prevMsg = messages[index - 1];
|
||||
const currentTime = new Date(currentMsg.created_at).getTime();
|
||||
const prevTime = new Date(prevMsg.created_at).getTime();
|
||||
return (currentTime - prevTime) > TIME_SEPARATOR_INTERVAL;
|
||||
const currentDate = new Date(currentMsg.created_at);
|
||||
const prevDate = new Date(prevMsg.created_at);
|
||||
if (Number.isNaN(currentDate.getTime()) || Number.isNaN(prevDate.getTime())) return false;
|
||||
return (currentDate.getTime() - prevDate.getTime()) > TIME_SEPARATOR_INTERVAL;
|
||||
}, [messages]);
|
||||
|
||||
// 处理输入变化,检测@符号
|
||||
|
||||
@@ -223,14 +223,7 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
timeText: {
|
||||
color: colors.text.hint,
|
||||
fontSize: 12,
|
||||
@@ -324,13 +317,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
</View>
|
||||
) : isGroupChat ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
{displayAvatar ? (
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
) : (
|
||||
<View style={styles.groupAvatarPlaceholder}>
|
||||
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
||||
</View>
|
||||
)}
|
||||
<Avatar source={displayAvatar} size={56} name={displayNameRaw || '群'} />
|
||||
</View>
|
||||
) : (
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
|
||||
@@ -137,7 +137,9 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
if (message.created_at) {
|
||||
const store = useMessageStore.getState();
|
||||
const currentLast = store.lastSystemMessageAt;
|
||||
if (!currentLast || new Date(message.created_at) > new Date(currentLast)) {
|
||||
const msgDate = new Date(message.created_at);
|
||||
if (Number.isNaN(msgDate.getTime())) return;
|
||||
if (!currentLast || msgDate > new Date(currentLast)) {
|
||||
store.setLastSystemMessageAt(message.created_at);
|
||||
}
|
||||
}
|
||||
@@ -335,7 +337,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
content: textContent,
|
||||
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
|
||||
isRead: isActiveConversation,
|
||||
createdAt: new Date(created_at).toISOString(),
|
||||
createdAt: (() => { const d = new Date(created_at); return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString(); })(),
|
||||
seq,
|
||||
status: 'normal',
|
||||
segments,
|
||||
@@ -451,7 +453,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
: [],
|
||||
status: 'normal',
|
||||
category: 'notification',
|
||||
created_at: new Date(timestamp || Date.now()).toISOString(),
|
||||
created_at: (() => { const d = new Date(timestamp || Date.now()); return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString(); })(),
|
||||
};
|
||||
|
||||
const updatedMessages = [...existingMessages, systemNoticeMessage].sort((a, b) => a.seq - b.seq);
|
||||
|
||||
@@ -134,6 +134,7 @@ function sortConversationList(conversations: Map<string, ConversationResponse>):
|
||||
}
|
||||
const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime();
|
||||
const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime();
|
||||
if (Number.isNaN(aTime) || Number.isNaN(bTime)) return 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user