Ensure unique timestamps for chat messages to prevent collisions and fix date formatting in chat-box UI.

This commit is contained in:
akastijn 2026-07-19 19:20:38 +02:00
parent 564582a6f9
commit 9e5148fa8c
4 changed files with 23 additions and 5 deletions

View File

@ -2,6 +2,7 @@ package com.alttd.altitudeweb.controllers.chat;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import java.time.Instant; import java.time.Instant;
import java.util.UUID; import java.util.UUID;
@ -11,7 +12,8 @@ import java.util.UUID;
public class ChatMessage { public class ChatMessage {
private final UUID uuid; private final UUID uuid;
private final Instant timestamp; @Setter
private Instant timestamp;
private final String server; private final String server;
//TODO [Stijn] [2026-07-18]: Handle channel types //TODO [Stijn] [2026-07-18]: Handle channel types
//private final String channel; //private final String channel;

View File

@ -4,16 +4,18 @@ import com.alttd.altitudeweb.database.chat.ChatLogDao;
import com.alttd.altitudeweb.model.ChatMessageDto; import com.alttd.altitudeweb.model.ChatMessageDto;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import java.time.Instant;
import java.util.UUID; import java.util.UUID;
@UtilityClass @UtilityClass
public class ChatMessageMapper { public class ChatMessageMapper {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.registerModule(new JavaTimeModule()); .registerModule(new JavaTimeModule())
.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
public ChatMessage fromDto(ChatMessageDto dto) { public ChatMessage fromDto(ChatMessageDto dto) {
return ChatMessage.builder() return ChatMessage.builder()

View File

@ -30,6 +30,7 @@ public class ChatService {
private String[] allowedServers; private String[] allowedServers;
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>(); private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
private final EventPublisher eventPublisher; private final EventPublisher eventPublisher;
private long lastMillis = 0;
@Scheduled(cron = "0 * * * * *") @Scheduled(cron = "0 * * * * *")
public void clearOldMessages() { public void clearOldMessages() {
@ -50,7 +51,8 @@ public class ChatService {
try { try {
sqlSession.getMapper(ChatLogMapper.class) sqlSession.getMapper(ChatLogMapper.class)
.getChatLogs(Instant.now().minus(Duration.ofHours(1)).toEpochMilli()) .getChatLogs(Instant.now().minus(Duration.ofHours(1)).toEpochMilli())
.stream().map(ChatMessageMapper::fromDao) .stream()
.map(ChatMessageMapper::fromDao)
.forEach(this::putChatMessage); .forEach(this::putChatMessage);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to load history count", e); log.error("Failed to load history count", e);
@ -58,7 +60,19 @@ public class ChatService {
}); });
} }
private synchronized void setUniqueTimestamp(ChatMessage chatMessage) {
long timestamp = chatMessage.getTimestamp().toEpochMilli();
if (timestamp == lastMillis) {
timestamp += 1;
} else {
return;
}
lastMillis = timestamp;
chatMessage.setTimestamp(Instant.ofEpochMilli(timestamp));
}
private void putChatMessage(ChatMessage chatMessage) { private void putChatMessage(ChatMessage chatMessage) {
setUniqueTimestamp(chatMessage);
chatMessages.put(chatMessage.getTimestamp(), chatMessage); chatMessages.put(chatMessage.getTimestamp(), chatMessage);
} }

View File

@ -2,7 +2,7 @@
@for (message of messages(); track message.timestamp) { @for (message of messages(); track message.timestamp) {
<p class="message"> <p class="message">
<span class="date-time" [class.blocked]="!message.notBlocked"> <span class="date-time" [class.blocked]="!message.notBlocked">
[{{ message.timestamp * 1000 | date: 'mediumTime' }}] [{{ message.timestamp | date: 'mediumTime' }}]
</span> </span>
<mini-message [node]="message.messageJson"></mini-message> <mini-message [node]="message.messageJson"></mini-message>
</p> </p>