How to get submit event timestamp from frontend?

Good morning! I need to capture the exact time a MessageInput is submitted to handle scenarios where a user’s device is temporarily offline, causing delays with the backend.

What I Tried

This is the only solution I could find that works consistently, but it feels a bit awkward to me. Maybe there’s a more elegant way, such as using getElement().setAttribute()?

private void handleMessageSubmit(MessageInput.SubmitEvent event, Scroller messageScroller) {
    String jsCode = "const now = new Date();"
            + "const timestamp = now.toISOString();"
            + "const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;"
            + "return { timestamp, timezone };";

    event.getSource().getElement().executeJs(jsCode).then(jsonString -> {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode jsonNode = objectMapper.readTree(jsonString.toJson());

            String timestamp = jsonNode.get("timestamp").asText();
            String timezone = jsonNode.get("timezone").asText();

            processSubmission(event.getValue(), timestamp, timezone, messageScroller);
        } catch (Exception e) {
            log.error("Failed to parse timestamp and timezone from JSON: {}", e.getMessage());
        }
    });
}

Other Posts I Looked At

According to this post, I can get the timezone offset using Page.getCurrent().getWebBrowser().getTimezoneOffset(), but that doesn’t give me the time of the message’s submit event.

From this 2012 post, it seems like WebBrowser.getCurrentDate() could get the user’s local date, which makes me wonder if there’s a way to get the current time of the browser. However, this still doesn’t fully solve my problem.

Docs Assistant

I also asked the Docs Assistant for a solution. It suggested:

messageInput.addSubmitListener(event -> {
    String messageText = event.getValue();
    String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    items.add(new Message(messageText, timestamp, "User", "U", 1));
    messageList.setItems(items);
    Notification.show("Message sent at: " + timestamp, 3000, Notification.Position.MIDDLE);
});

However, this captures the time on the server side, not on the front end, right?

Docs Assistant Error

By the way, I’ve noticed that the Flow/Hilla select dropdown doesn’t seem to work correctly. Even when I have Flow selected, the assistant keeps giving me Hilla-like solutions unless I specifically include “with Vaadin Flow” in my query.

Are you really sure you want to use the client-side timestamp? The problem is that it can lead to weird results in cases when the client’s clock is out of sync. Modern operating systems have NTP enabled by default so this rarely happens by accident but there’s also the possibility of someone maliciously manipulating their clock to exploit the system.

The solution you show doesn’t do what you ask for since it does a separate round trip to fetch the client-side timestamp after the server has already received the original event. Instead, you need to instruct the client-side logic to extract the timestamp when it’s about to send the original submission to the server. To do this, you need to define a subclass of the regular submit event with an additional @EventData expression (the one for value is copied from the super class constructor).

public class TimestampedSubmitEvent extends SubmitEvent {
    private String timestamp;

    public TimestampedSubmitEvent(MessageInput source, boolean fromClient,
            @EventData("event.detail.value") String value,
            @EventData("new Date().toISOString()") String timestamp) {
        super(source, fromClient, value);
        this.timestamp = timestamp;
    }

    public String getTimestamp() {
        return timestamp;
    }
}

And then you need to use ComponentUtil to add a listener for that more specific event instead of the regular submit event.

MessageInput input = new MessageInput();
ComponentUtil.addListener(input, TimestampedSubmitEvent.class, event -> {
    Notification.show(event.getValue() + " @ " + event.getTimestamp());
});
add(input);

If you use the client time only for ordering events, then you don’t need to fetch the time zone since the ISO string is always converted to UTC (which can be seen from the Z at the end of the string).

1 Like

Leif, thanks so much for your response!

Yes. An important feature of my app is that the employee can clock out even if their device is temporarily offline. Therefore, I need to know when they actually submitted the event, not when it was received.

Ah, is that why in that post from 2012 he is trying to compare the client and server time?

TBH, I had not considered that and I’m not sure how to handle it.

In another part of the app, I ask chatGPT to convert the user message to LocalDateTime. My assumption is that chatGPT doesn’t anything about the timezone of the user, but it can convert text like 8am today into LocalDateTime. Then I use the ZoneId to convert that to an Instant.

I’m working on testing your solution right now. Will report back soon! :slight_smile:

Ok, it worked! Thanks so much for your help!

Final code:

public class TimestampedSubmitEvent extends SubmitEvent {
    private final String timestamp;
    private final String timezone;

    public TimestampedSubmitEvent(MessageInput source, boolean fromClient,
                                  @EventData("event.detail.value") String value,
                                  @EventData("new Date().toISOString()") String timestamp,
                                  @EventData("Intl.DateTimeFormat().resolvedOptions().timeZone") String timezone) {
        super(source, fromClient, value);
        this.timestamp = timestamp;
        this.timezone = timezone;
    }

    public String getTimestamp() {
        return timestamp;
    }

    public String getTimezone() {
        return timezone;
    }
}
private MessageInput createMessageInput(Scroller messageScroller) {
        MessageInput messageInput = new MessageInput();
        messageInput.setWidthFull();
        ComponentUtil.addListener(messageInput, TimestampedSubmitEvent.class, event -> handleMessageSubmit(event, messageScroller));
        return messageInput;
    }

    private void handleMessageSubmit(TimestampedSubmitEvent event, Scroller messageScroller) {
        String userMessageText = event.getValue();
        String timestamp = event.getTimestamp();
        String timezone = event.getTimezone();  // Leaving this as a string for now since it's only used by the AI
        log.info("Timestamp : {}, Timezone : {}", timestamp, timezone);
        Instant creationTime = timestamp != null ? Instant.parse(timestamp) : Instant.now();
        String userName = "Nathan";
        MessageListItem userMessage = new MessageListItem(userMessageText, creationTime, userName);
        UserMessageDto userMessageDto = new UserMessageDto(creationTime, userName, userMessageText, chatId, timezone);
        appendMessageAndReply(userMessage, messageScroller, userMessageDto);
    }