fix(message): add robust NaN validation for date handling across components
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 53s
Frontend CI / ota-android (push) Has been cancelled
Frontend CI / build-android-apk (push) Has been cancelled

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:
lafay
2026-04-25 15:25:42 +08:00
parent a6a4198ac5
commit 6b91a7ead1
14 changed files with 68 additions and 40 deletions

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>());

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
// 检查是否是图片消息

View File

@@ -426,10 +426,10 @@ 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()}
{/* 私聊模式:显示已读标记 */}

View File

@@ -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,

View File

@@ -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]);
// 处理输入变化,检测@符号

View File

@@ -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} />

View File

@@ -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);

View File

@@ -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;
});
}