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 [isDeleting, setIsDeleting] = useState(false);
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (dateString: string): string => {
|
const formatTime = (dateString: string): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
return formatDistanceToNow(new Date(dateString), {
|
const date = new Date(dateString);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return formatDistanceToNow(date, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: zhCN,
|
locale: zhCN,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -331,8 +331,11 @@ const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
|||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (dateString: string): string => {
|
const formatTime = (dateString: string): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
return formatDistanceToNow(new Date(dateString), {
|
const date = new Date(dateString);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return formatDistanceToNow(date, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: zhCN,
|
locale: zhCN,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -274,8 +274,11 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
|||||||
<View style={styles.metaItem}>
|
<View style={styles.metaItem}>
|
||||||
<MaterialCommunityIcons name="calendar-outline" size={14} color={colors.text.hint} />
|
<MaterialCommunityIcons name="calendar-outline" size={14} color={colors.text.hint} />
|
||||||
<Text style={styles.metaText}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -661,7 +661,10 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
{announcements[0].content}
|
{announcements[0].content}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="caption" color={colors.text.hint} style={styles.announcementTime}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -245,8 +245,10 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
|
|
||||||
// 格式化时间(稳定引用,配合会话行 memo)
|
// 格式化时间(稳定引用,配合会话行 memo)
|
||||||
const formatTime = useCallback((dateString: string): string => {
|
const formatTime = useCallback((dateString: string): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
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 aPinned = a.is_pinned ? 1 : 0;
|
||||||
const bPinned = b.is_pinned ? 1 : 0;
|
const bPinned = b.is_pinned ? 1 : 0;
|
||||||
if (aPinned !== bPinned) return bPinned - aPinned;
|
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>());
|
const listConvRef = useRef(new Map<string, ConversationResponse>());
|
||||||
|
|||||||
@@ -241,7 +241,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
<View style={styles.noticeBox}>
|
<View style={styles.noticeBox}>
|
||||||
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
||||||
<Text style={styles.noticeTime}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -311,7 +314,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
<Text style={styles.infoLabel}>创建时间</Text>
|
<Text style={styles.infoLabel}>创建时间</Text>
|
||||||
<Text style={styles.infoValue}>
|
<Text style={styles.infoValue}>
|
||||||
{groupInfo?.created_at
|
{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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -246,7 +246,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
<View style={styles.noticeBox}>
|
<View style={styles.noticeBox}>
|
||||||
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
<Text style={styles.noticeText}>{announcements[0].content}</Text>
|
||||||
<Text style={styles.noticeTime}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -316,7 +319,10 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
|||||||
<Text style={styles.infoLabel}>创建时间</Text>
|
<Text style={styles.infoLabel}>创建时间</Text>
|
||||||
<Text style={styles.infoValue}>
|
<Text style={styles.infoValue}>
|
||||||
{groupInfo?.created_at
|
{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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -78,10 +78,12 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
|||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
|
|
||||||
const isMyMessage = message.sender_id === currentUserId;
|
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 now = Date.now();
|
||||||
const canRecall = isMyMessage &&
|
const canRecall = isMyMessage &&
|
||||||
message.status !== 'recalled' &&
|
message.status !== 'recalled' &&
|
||||||
|
messageTime > 0 &&
|
||||||
(now - messageTime) < RECALL_TIME_LIMIT;
|
(now - messageTime) < RECALL_TIME_LIMIT;
|
||||||
|
|
||||||
// 检查是否是图片消息
|
// 检查是否是图片消息
|
||||||
|
|||||||
@@ -426,10 +426,10 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
|||||||
|
|
||||||
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
||||||
{/* 群聊模式:显示发送者昵称 */}
|
{/* 群聊模式:显示发送者昵称 */}
|
||||||
{isGroupChat && !isMe && senderInfo && (
|
{isGroupChat && senderInfo && (
|
||||||
<Text style={styles.senderName}>{senderInfo.nickname}</Text>
|
<Text style={[styles.senderName, isMe && styles.mySenderName]}>{senderInfo.nickname}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderMessageContent()}
|
{renderMessageContent()}
|
||||||
|
|
||||||
{/* 私聊模式:显示已读标记 */}
|
{/* 私聊模式:显示已读标记 */}
|
||||||
|
|||||||
@@ -262,14 +262,13 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
messageRow: {
|
messageRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
myMessageRow: {
|
myMessageRow: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
},
|
},
|
||||||
theirMessageRow: {
|
theirMessageRow: {
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
alignItems: 'flex-start',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 头像 - 更大一点
|
// 头像 - 更大一点
|
||||||
@@ -303,6 +302,11 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
mySenderName: {
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 4,
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
},
|
||||||
|
|
||||||
// 消息气泡 - 动态圆角支持
|
// 消息气泡 - 动态圆角支持
|
||||||
messageBubbleOuter: {
|
messageBubbleOuter: {
|
||||||
@@ -315,7 +319,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
elevation: 1,
|
elevation: 1,
|
||||||
},
|
},
|
||||||
myBubbleOuter: {
|
myBubbleOuter: {
|
||||||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3), // 微信的小尾巴更尖
|
borderTopRightRadius: Math.max(4, messageRadius * 0.3), // 箭头在右上
|
||||||
},
|
},
|
||||||
theirBubbleOuter: {
|
theirBubbleOuter: {
|
||||||
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
|
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
|
||||||
@@ -330,7 +334,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
},
|
},
|
||||||
myBubbleInner: {
|
myBubbleInner: {
|
||||||
backgroundColor: myBubbleBg,
|
backgroundColor: myBubbleBg,
|
||||||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
|
borderTopRightRadius: Math.max(4, messageRadius * 0.3),
|
||||||
},
|
},
|
||||||
theirBubbleInner: {
|
theirBubbleInner: {
|
||||||
backgroundColor: theirBubbleBg,
|
backgroundColor: theirBubbleBg,
|
||||||
@@ -394,7 +398,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
elevation: 4,
|
elevation: 4,
|
||||||
},
|
},
|
||||||
myImageBubble: {
|
myImageBubble: {
|
||||||
borderBottomRightRadius: 8,
|
borderTopRightRadius: 8,
|
||||||
},
|
},
|
||||||
theirImageBubble: {
|
theirImageBubble: {
|
||||||
borderTopLeftRadius: 8,
|
borderTopLeftRadius: 8,
|
||||||
|
|||||||
@@ -657,8 +657,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
|||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = useCallback((dateString: string): string => {
|
const formatTime = useCallback((dateString: string): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
@@ -680,9 +682,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
|||||||
if (index === 0) return true;
|
if (index === 0) return true;
|
||||||
const currentMsg = messages[index];
|
const currentMsg = messages[index];
|
||||||
const prevMsg = messages[index - 1];
|
const prevMsg = messages[index - 1];
|
||||||
const currentTime = new Date(currentMsg.created_at).getTime();
|
const currentDate = new Date(currentMsg.created_at);
|
||||||
const prevTime = new Date(prevMsg.created_at).getTime();
|
const prevDate = new Date(prevMsg.created_at);
|
||||||
return (currentTime - prevTime) > TIME_SEPARATOR_INTERVAL;
|
if (Number.isNaN(currentDate.getTime()) || Number.isNaN(prevDate.getTime())) return false;
|
||||||
|
return (currentDate.getTime() - prevDate.getTime()) > TIME_SEPARATOR_INTERVAL;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// 处理输入变化,检测@符号
|
// 处理输入变化,检测@符号
|
||||||
|
|||||||
@@ -223,14 +223,7 @@ function createConversationRowStyles(colors: AppColors) {
|
|||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
},
|
},
|
||||||
groupAvatarPlaceholder: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
borderRadius: borderRadius.md,
|
|
||||||
backgroundColor: colors.primary.main,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
timeText: {
|
timeText: {
|
||||||
color: colors.text.hint,
|
color: colors.text.hint,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -324,13 +317,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
) : isGroupChat ? (
|
) : isGroupChat ? (
|
||||||
<View style={styles.groupAvatar}>
|
<View style={styles.groupAvatar}>
|
||||||
{displayAvatar ? (
|
<Avatar source={displayAvatar} size={56} name={displayNameRaw || '群'} />
|
||||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.groupAvatarPlaceholder}>
|
|
||||||
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||||
|
|||||||
@@ -137,7 +137,9 @@ export class WSMessageHandler implements IWSMessageHandler {
|
|||||||
if (message.created_at) {
|
if (message.created_at) {
|
||||||
const store = useMessageStore.getState();
|
const store = useMessageStore.getState();
|
||||||
const currentLast = store.lastSystemMessageAt;
|
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);
|
store.setLastSystemMessageAt(message.created_at);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +337,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
|||||||
content: textContent,
|
content: textContent,
|
||||||
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
|
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
|
||||||
isRead: isActiveConversation,
|
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,
|
seq,
|
||||||
status: 'normal',
|
status: 'normal',
|
||||||
segments,
|
segments,
|
||||||
@@ -451,7 +453,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
|||||||
: [],
|
: [],
|
||||||
status: 'normal',
|
status: 'normal',
|
||||||
category: 'notification',
|
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);
|
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 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();
|
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;
|
return bTime - aTime;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user