Erweiterungen
This commit is contained in:
@@ -7,12 +7,7 @@ public enum MessageOrigin {
|
|||||||
/**
|
/**
|
||||||
* Message received from a client (app user)
|
* Message received from a client (app user)
|
||||||
*/
|
*/
|
||||||
INCOMING,
|
CLIENT,
|
||||||
|
|
||||||
/**
|
|
||||||
* Message sent to a client (app user)
|
|
||||||
*/
|
|
||||||
OUTGOING,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message sent from the server
|
* Message sent from the server
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
private ObjectId jobIdContext;
|
private ObjectId jobIdContext;
|
||||||
private String jobNumberContext;
|
private String jobNumberContext;
|
||||||
private final VerticalLayout contentLayout;
|
private final VerticalLayout contentLayout;
|
||||||
private VerticalLayout messagesContainer;
|
private VerticalLayout messagesContainer; // Container for message components
|
||||||
private Scroller messagesScroller; // Reference to the scroller component
|
private Scroller messagesScroller; // Reference to the scroller component
|
||||||
private Div scrollAnchor; // Marker element at the end of messages for scrolling
|
private Div scrollAnchor; // Anchor element for scrolling to bottom
|
||||||
|
private List<Message> currentMessages; // Current messages being displayed for redrawing
|
||||||
private Registration broadcasterRegistration; // Track listener registration
|
private Registration broadcasterRegistration; // Track listener registration
|
||||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||||
@@ -131,25 +132,23 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
HorizontalLayout headerLayout = createHeaderLayout(clientName, conversationTitle);
|
HorizontalLayout headerLayout = createHeaderLayout(clientName, conversationTitle);
|
||||||
contentLayout.add(headerLayout);
|
contentLayout.add(headerLayout);
|
||||||
|
|
||||||
// Reset scrollAnchor when creating new messagesContainer to ensure it's properly re-added
|
// Store current messages for rendering and future updates
|
||||||
|
this.currentMessages = filteredMessages;
|
||||||
|
|
||||||
|
// Reset scroll anchor for new container
|
||||||
scrollAnchor = null;
|
scrollAnchor = null;
|
||||||
|
|
||||||
|
// Create messages container
|
||||||
messagesContainer = new VerticalLayout();
|
messagesContainer = new VerticalLayout();
|
||||||
messagesContainer.setPadding(true);
|
messagesContainer.setPadding(false);
|
||||||
messagesContainer.setSpacing(true);
|
messagesContainer.setSpacing(false);
|
||||||
messagesContainer.setWidthFull();
|
messagesContainer.setWidthFull();
|
||||||
messagesContainer.getStyle().set("background-color", "#f0f0f0");
|
messagesContainer.getStyle()
|
||||||
messagesContainer.getStyle().set("border-radius", "8px");
|
.set("background-color", "#f0f0f0")
|
||||||
messagesContainer.getStyle().set("padding", "15px");
|
.set("border-radius", "8px")
|
||||||
messagesContainer.getStyle().set("display", "flex");
|
.set("padding", "15px");
|
||||||
messagesContainer.getStyle().set("flex-direction", "column");
|
|
||||||
|
|
||||||
if (!filteredMessages.isEmpty()) {
|
|
||||||
renderMessages(filteredMessages);
|
|
||||||
} else {
|
|
||||||
ensureScrollAnchor();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Wrap messages container in scroller for vertical scrolling
|
||||||
messagesScroller = new Scroller(messagesContainer);
|
messagesScroller = new Scroller(messagesContainer);
|
||||||
messagesScroller.setWidthFull();
|
messagesScroller.setWidthFull();
|
||||||
messagesScroller.setHeightFull();
|
messagesScroller.setHeightFull();
|
||||||
@@ -162,10 +161,142 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
HorizontalLayout inputLayout = createMessageInputArea();
|
HorizontalLayout inputLayout = createMessageInputArea();
|
||||||
contentLayout.add(inputLayout);
|
contentLayout.add(inputLayout);
|
||||||
|
|
||||||
// Scroll to bottom to show the latest messages on initial page load
|
// Render messages using Vaadin components
|
||||||
// This must be called AFTER messagesScroller is created and added to the layout
|
renderMessages();
|
||||||
// Force scroll to ensure user sees the latest messages
|
|
||||||
scrollToBottom(true);
|
// Ensure scroll anchor exists and scroll to bottom
|
||||||
|
ensureScrollAnchor();
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render all messages using Vaadin components
|
||||||
|
*/
|
||||||
|
private void renderMessages() {
|
||||||
|
if (currentMessages == null || messagesContainer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesContainer.removeAll();
|
||||||
|
|
||||||
|
LocalDate currentDate = null;
|
||||||
|
|
||||||
|
for (Message message : currentMessages) {
|
||||||
|
LocalDateTime timestamp = resolveTimestamp(message);
|
||||||
|
LocalDate messageDate = timestamp.toLocalDate();
|
||||||
|
|
||||||
|
// Add date separator if date changed
|
||||||
|
if (currentDate == null || !currentDate.equals(messageDate)) {
|
||||||
|
messagesContainer.add(createDateSeparator(messageDate));
|
||||||
|
currentDate = messageDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message bubble
|
||||||
|
messagesContainer.add(createMessageBubble(message, timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a date separator component
|
||||||
|
*/
|
||||||
|
private Div createDateSeparator(LocalDate date) {
|
||||||
|
Div separator = new Div();
|
||||||
|
separator.getStyle()
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("justify-content", "center")
|
||||||
|
.set("text-align", "center")
|
||||||
|
.set("margin", "20px 0");
|
||||||
|
separator.setWidthFull();
|
||||||
|
|
||||||
|
Span dateLabel = new Span(date.format(DATE_FORMATTER));
|
||||||
|
dateLabel.getStyle()
|
||||||
|
.set("background-color", "#d0d0d0")
|
||||||
|
.set("padding", "4px 10px")
|
||||||
|
.set("border-radius", "12px")
|
||||||
|
.set("font-size", "12px")
|
||||||
|
.set("font-weight", "500")
|
||||||
|
.set("color", "#333333")
|
||||||
|
.set("display", "inline-block");
|
||||||
|
|
||||||
|
separator.add(dateLabel);
|
||||||
|
return separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a message bubble component
|
||||||
|
*/
|
||||||
|
private Div createMessageBubble(Message message, LocalDateTime timestamp) {
|
||||||
|
// Determine alignment based on message origin
|
||||||
|
// CLIENT origin = client messages (left), SERVER origin = server messages (right)
|
||||||
|
boolean isServerMessage = message.getOrigin() == MessageOrigin.SERVER;
|
||||||
|
|
||||||
|
// Container for the message (aligns left or right)
|
||||||
|
Div messageWrapper = new Div();
|
||||||
|
String alignment = isServerMessage ? "right" : "left";
|
||||||
|
|
||||||
|
messageWrapper.getStyle()
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("justify-content", isServerMessage ? "flex-end" : "flex-start")
|
||||||
|
.set("margin", "5px 0")
|
||||||
|
.set("width", "100%");
|
||||||
|
|
||||||
|
// Message bubble
|
||||||
|
Div bubble = new Div();
|
||||||
|
bubble.getStyle()
|
||||||
|
.set("background-color", isServerMessage ? "#dcf8c6" : "#ffffff")
|
||||||
|
.set("padding", "10px 15px")
|
||||||
|
.set("border-radius", "18px")
|
||||||
|
.set("max-width", "70%")
|
||||||
|
.set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)")
|
||||||
|
.set("word-wrap", "break-word")
|
||||||
|
.set("white-space", "pre-wrap")
|
||||||
|
.set("text-align", alignment);
|
||||||
|
|
||||||
|
// Message content
|
||||||
|
Div contentDiv = new Div();
|
||||||
|
String content = Optional.ofNullable(message.getContent()).orElse("(kein Inhalt)");
|
||||||
|
contentDiv.setText(content);
|
||||||
|
contentDiv.getStyle()
|
||||||
|
.set("font-size", "14px")
|
||||||
|
.set("color", "#000000")
|
||||||
|
.set("margin-bottom", "5px")
|
||||||
|
.set("text-align", alignment);
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
||||||
|
timeSpan.getStyle()
|
||||||
|
.set("font-size", "11px")
|
||||||
|
.set("color", isServerMessage ? "#666666" : "#999999")
|
||||||
|
.set("display", "block")
|
||||||
|
.set("text-align", alignment);
|
||||||
|
|
||||||
|
bubble.add(contentDiv, timeSpan);
|
||||||
|
messageWrapper.add(bubble);
|
||||||
|
|
||||||
|
return messageWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure scroll anchor exists at the bottom of messages container
|
||||||
|
*/
|
||||||
|
private void ensureScrollAnchor() {
|
||||||
|
if (scrollAnchor == null && messagesContainer != null) {
|
||||||
|
scrollAnchor = new Div();
|
||||||
|
scrollAnchor.setId("scroll-anchor");
|
||||||
|
scrollAnchor.getStyle()
|
||||||
|
.set("height", "1px")
|
||||||
|
.set("width", "100%");
|
||||||
|
messagesContainer.add(scrollAnchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to the bottom of the messages container
|
||||||
|
*/
|
||||||
|
private void scrollToBottom() {
|
||||||
|
if (scrollAnchor != null) {
|
||||||
|
scrollAnchor.scrollIntoView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HorizontalLayout createHeaderLayout(String clientName, String conversationTitle) {
|
private HorizontalLayout createHeaderLayout(String clientName, String conversationTitle) {
|
||||||
@@ -193,85 +324,6 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Div createDateSeparator(LocalDateTime date) {
|
|
||||||
Div separator = new Div();
|
|
||||||
separator.setWidthFull();
|
|
||||||
separator.getStyle().set("text-align", "center");
|
|
||||||
|
|
||||||
Span dateSpan = new Span(date.format(DATE_FORMATTER));
|
|
||||||
dateSpan.getStyle().set("background-color", "#d0d0d0");
|
|
||||||
dateSpan.getStyle().set("padding", "8px 20px");
|
|
||||||
dateSpan.getStyle().set("border-radius", "20px");
|
|
||||||
dateSpan.getStyle().set("font-size", "12px");
|
|
||||||
dateSpan.getStyle().set("color", "#333333");
|
|
||||||
dateSpan.getStyle().set("font-weight", "500");
|
|
||||||
dateSpan.getStyle().set("box-shadow", "0 1px 3px rgba(0,0,0,0.15)");
|
|
||||||
|
|
||||||
separator.add(dateSpan);
|
|
||||||
return separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Div createIncomingMessage(String content, LocalDateTime timestamp) {
|
|
||||||
Div messageWrapper = new Div();
|
|
||||||
messageWrapper.setWidthFull();
|
|
||||||
messageWrapper.getStyle().set("display", "flex");
|
|
||||||
messageWrapper.getStyle().set("justify-content", "flex-start");
|
|
||||||
|
|
||||||
Div messageBubble = new Div();
|
|
||||||
messageBubble.getStyle().set("max-width", "60%");
|
|
||||||
messageBubble.getStyle().set("background-color", "#ffffff");
|
|
||||||
messageBubble.getStyle().set("padding", "10px 15px");
|
|
||||||
messageBubble.getStyle().set("border-radius", "18px");
|
|
||||||
messageBubble.getStyle().set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)");
|
|
||||||
|
|
||||||
Span contentSpan = new Span(content);
|
|
||||||
contentSpan.getStyle().set("display", "block");
|
|
||||||
contentSpan.getStyle().set("word-wrap", "break-word");
|
|
||||||
|
|
||||||
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
|
||||||
timeSpan.getStyle().set("font-size", "11px");
|
|
||||||
timeSpan.getStyle().set("color", "#999999");
|
|
||||||
timeSpan.getStyle().set("display", "block");
|
|
||||||
timeSpan.getStyle().set("text-align", "right");
|
|
||||||
timeSpan.getStyle().set("margin-top", "5px");
|
|
||||||
|
|
||||||
messageBubble.add(contentSpan, timeSpan);
|
|
||||||
messageWrapper.add(messageBubble);
|
|
||||||
|
|
||||||
return messageWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Div createOutgoingMessage(String content, LocalDateTime timestamp) {
|
|
||||||
Div messageWrapper = new Div();
|
|
||||||
messageWrapper.setWidthFull();
|
|
||||||
messageWrapper.getStyle().set("display", "flex");
|
|
||||||
messageWrapper.getStyle().set("justify-content", "flex-end");
|
|
||||||
messageWrapper.getStyle().set("margin-bottom", "10px");
|
|
||||||
|
|
||||||
Div messageBubble = new Div();
|
|
||||||
messageBubble.getStyle().set("max-width", "60%");
|
|
||||||
messageBubble.getStyle().set("background-color", "#dcf8c6");
|
|
||||||
messageBubble.getStyle().set("padding", "10px 15px");
|
|
||||||
messageBubble.getStyle().set("padding-bottom", "5px");
|
|
||||||
messageBubble.getStyle().set("border-radius", "18px");
|
|
||||||
messageBubble.getStyle().set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)");
|
|
||||||
|
|
||||||
Span contentSpan = new Span(content);
|
|
||||||
contentSpan.getStyle().set("display", "block");
|
|
||||||
contentSpan.getStyle().set("word-wrap", "break-word");
|
|
||||||
|
|
||||||
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
|
||||||
timeSpan.getStyle().set("font-size", "11px");
|
|
||||||
timeSpan.getStyle().set("color", "#666666");
|
|
||||||
timeSpan.getStyle().set("display", "block");
|
|
||||||
timeSpan.getStyle().set("text-align", "right");
|
|
||||||
timeSpan.getStyle().set("margin-top", "5px");
|
|
||||||
|
|
||||||
messageBubble.add(contentSpan, timeSpan);
|
|
||||||
messageWrapper.add(messageBubble);
|
|
||||||
|
|
||||||
return messageWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HorizontalLayout createMessageInputArea() {
|
private HorizontalLayout createMessageInputArea() {
|
||||||
TextArea messageInput = new TextArea();
|
TextArea messageInput = new TextArea();
|
||||||
@@ -316,13 +368,9 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
|
Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
|
||||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||||
|
|
||||||
// Refresh conversation to include the new message and update counters
|
// Refresh conversation to include the new message and redraw canvas
|
||||||
|
// loadMessageDetails already scrolls to bottom after rendering
|
||||||
loadMessageDetails();
|
loadMessageDetails();
|
||||||
|
|
||||||
// Ensure we scroll to the bottom to show the newly sent message
|
|
||||||
// This is called after loadMessageDetails to ensure DOM is fully updated
|
|
||||||
// Force scroll so user always sees their sent message
|
|
||||||
scrollToBottom(true);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Failed to send message to {}: {}", participantKey, ex.getMessage(), ex);
|
log.error("Failed to send message to {}: {}", participantKey, ex.getMessage(), ex);
|
||||||
@@ -412,147 +460,10 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
return "Konversation";
|
return "Konversation";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderMessages(List<Message> messages) {
|
|
||||||
LocalDate currentDate = null;
|
|
||||||
Div lastMessageWrapper = null;
|
|
||||||
for (Message message : messages) {
|
|
||||||
LocalDateTime timestamp = resolveTimestamp(message);
|
|
||||||
LocalDate messageDate = timestamp.toLocalDate();
|
|
||||||
if (!messageDate.equals(currentDate)) {
|
|
||||||
messagesContainer.add(createDateSeparator(timestamp));
|
|
||||||
currentDate = messageDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
String content = Optional.ofNullable(message.getContent()).orElse("(kein Inhalt)");
|
|
||||||
if (message.getOrigin() == MessageOrigin.INCOMING) {
|
|
||||||
lastMessageWrapper = createIncomingMessage(content, timestamp);
|
|
||||||
messagesContainer.add(lastMessageWrapper);
|
|
||||||
} else {
|
|
||||||
lastMessageWrapper = createOutgoingMessage(content, timestamp);
|
|
||||||
messagesContainer.add(lastMessageWrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureScrollAnchor();
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalDateTime resolveTimestamp(Message message) {
|
private LocalDateTime resolveTimestamp(Message message) {
|
||||||
return Optional.ofNullable(message.getCreatedAt()).orElse(LocalDateTime.now());
|
return Optional.ofNullable(message.getCreatedAt()).orElse(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureScrollAnchor() {
|
|
||||||
if (scrollAnchor == null) {
|
|
||||||
scrollAnchor = new Div();
|
|
||||||
scrollAnchor.setId("scroll-anchor");
|
|
||||||
scrollAnchor.getStyle().set("height", "5px");
|
|
||||||
scrollAnchor.getStyle().set("width", "5px");
|
|
||||||
scrollAnchor.getStyle().set("background-color", "red");
|
|
||||||
}
|
|
||||||
if (scrollAnchor.getParent().isEmpty()) {
|
|
||||||
messagesContainer.add(scrollAnchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll the messages scroller to the bottom to show the scrollAnchor element
|
|
||||||
* Waits until all DOM elements are fully rendered before scrolling
|
|
||||||
*
|
|
||||||
* @param forceScroll If true, always scroll to bottom. If false, only scroll if user is near bottom (within 100px)
|
|
||||||
*/
|
|
||||||
private void scrollToBottom(boolean forceScroll) {
|
|
||||||
if (messagesScroller != null && scrollAnchor != null) {
|
|
||||||
// Use beforeClientResponse to ensure all components are rendered and DOM is ready
|
|
||||||
UI.getCurrent().beforeClientResponse(messagesScroller, context -> {
|
|
||||||
// Execute JS on the scroller element for more reliable DOM access
|
|
||||||
messagesScroller.getElement().executeJs(
|
|
||||||
"const scroller = this;" +
|
|
||||||
"const forceScroll = " + forceScroll + ";" +
|
|
||||||
"console.log('[ScrollToBottom] Starting scroll, forceScroll:', forceScroll);" +
|
|
||||||
|
|
||||||
// Function to perform the actual scroll
|
|
||||||
"const performScroll = (scrollContainer, anchor) => {" +
|
|
||||||
" if (forceScroll) {" +
|
|
||||||
" anchor.scrollIntoView({ behavior: 'auto', block: 'end', inline: 'nearest' });" +
|
|
||||||
" console.log('[ScrollToBottom] Force scrolled to anchor');" +
|
|
||||||
" return;" +
|
|
||||||
" }" +
|
|
||||||
|
|
||||||
" const scrollTop = scrollContainer.scrollTop;" +
|
|
||||||
" const scrollHeight = scrollContainer.scrollHeight;" +
|
|
||||||
" const clientHeight = scrollContainer.clientHeight;" +
|
|
||||||
" const distanceFromBottom = scrollHeight - scrollTop - clientHeight;" +
|
|
||||||
|
|
||||||
" if (distanceFromBottom <= 100) {" +
|
|
||||||
" anchor.scrollIntoView({ behavior: 'auto', block: 'end', inline: 'nearest' });" +
|
|
||||||
" console.log('[ScrollToBottom] Scrolled to anchor (was near bottom)');" +
|
|
||||||
" } else {" +
|
|
||||||
" console.log('[ScrollToBottom] User scrolled up, not auto-scrolling');" +
|
|
||||||
" }" +
|
|
||||||
"};" +
|
|
||||||
|
|
||||||
// Function to wait for DOM to be fully rendered by checking scrollHeight stability
|
|
||||||
"const waitForDOMReady = () => {" +
|
|
||||||
" if (!scroller || !scroller.shadowRoot) {" +
|
|
||||||
" console.log('[ScrollToBottom] No scroller or shadowRoot found, retrying...');" +
|
|
||||||
" setTimeout(waitForDOMReady, 50);" +
|
|
||||||
" return;" +
|
|
||||||
" }" +
|
|
||||||
|
|
||||||
" const scrollContainer = scroller.shadowRoot.querySelector('[part=\"content\"]');" +
|
|
||||||
" if (!scrollContainer) {" +
|
|
||||||
" console.log('[ScrollToBottom] No scroll container found, retrying...');" +
|
|
||||||
" setTimeout(waitForDOMReady, 50);" +
|
|
||||||
" return;" +
|
|
||||||
" }" +
|
|
||||||
|
|
||||||
" const anchor = scrollContainer.querySelector('#scroll-anchor');" +
|
|
||||||
" if (!anchor) {" +
|
|
||||||
" console.log('[ScrollToBottom] No anchor found, scrolling container to bottom');" +
|
|
||||||
" scrollContainer.scrollTop = scrollContainer.scrollHeight;" +
|
|
||||||
" return;" +
|
|
||||||
" }" +
|
|
||||||
|
|
||||||
" let lastScrollHeight = 0;" +
|
|
||||||
" let stableCount = 0;" +
|
|
||||||
" const requiredStableChecks = 3;" +
|
|
||||||
" const maxAttempts = 50;" +
|
|
||||||
" let attempts = 0;" +
|
|
||||||
|
|
||||||
" const checkStability = () => {" +
|
|
||||||
" attempts++;" +
|
|
||||||
" const currentScrollHeight = scrollContainer.scrollHeight;" +
|
|
||||||
|
|
||||||
" if (currentScrollHeight === lastScrollHeight) {" +
|
|
||||||
" stableCount++;" +
|
|
||||||
" console.log('[ScrollToBottom] ScrollHeight stable (', stableCount, '/', requiredStableChecks, '), height:', currentScrollHeight);" +
|
|
||||||
" } else {" +
|
|
||||||
" stableCount = 0;" +
|
|
||||||
" console.log('[ScrollToBottom] ScrollHeight changed from', lastScrollHeight, 'to', currentScrollHeight);" +
|
|
||||||
" }" +
|
|
||||||
|
|
||||||
" lastScrollHeight = currentScrollHeight;" +
|
|
||||||
|
|
||||||
" if (stableCount >= requiredStableChecks) {" +
|
|
||||||
" console.log('[ScrollToBottom] DOM is stable, performing scroll');" +
|
|
||||||
" performScroll(scrollContainer, anchor);" +
|
|
||||||
" } else if (attempts >= maxAttempts) {" +
|
|
||||||
" console.log('[ScrollToBottom] Max attempts reached, performing scroll anyway');" +
|
|
||||||
" performScroll(scrollContainer, anchor);" +
|
|
||||||
" } else {" +
|
|
||||||
" requestAnimationFrame(checkStability);" +
|
|
||||||
" }" +
|
|
||||||
" };" +
|
|
||||||
|
|
||||||
" requestAnimationFrame(checkStability);" +
|
|
||||||
"};" +
|
|
||||||
|
|
||||||
// Add initial delay to allow Vaadin component to fully initialize
|
|
||||||
"setTimeout(waitForDOMReady, 100);"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the view is attached to the UI
|
* Called when the view is attached to the UI
|
||||||
* Registers listener for incoming messages
|
* Registers listener for incoming messages
|
||||||
@@ -624,37 +535,21 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
// Update UI in a thread-safe manner using UI.access()
|
// Update UI in a thread-safe manner using UI.access()
|
||||||
ui.access(() -> {
|
ui.access(() -> {
|
||||||
try {
|
try {
|
||||||
if (messagesContainer != null) {
|
if (currentMessages != null) {
|
||||||
LocalDateTime timestamp = resolveTimestamp(message);
|
// Add new message to the list
|
||||||
String content = Optional.ofNullable(message.getContent()).orElse("(kein Inhalt)");
|
currentMessages.add(message);
|
||||||
|
|
||||||
// Create and add the new message component
|
// Re-render all messages with the new message included
|
||||||
Div messageComponent;
|
renderMessages();
|
||||||
if (message.getOrigin() == MessageOrigin.INCOMING) {
|
|
||||||
messageComponent = createIncomingMessage(content, timestamp);
|
|
||||||
} else {
|
|
||||||
messageComponent = createOutgoingMessage(content, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove scroll anchor temporarily
|
// Ensure scroll anchor exists and scroll to show new message
|
||||||
if (scrollAnchor != null && scrollAnchor.getParent().isPresent()) {
|
|
||||||
messagesContainer.remove(scrollAnchor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new message
|
|
||||||
messagesContainer.add(messageComponent);
|
|
||||||
|
|
||||||
// Re-add scroll anchor
|
|
||||||
ensureScrollAnchor();
|
ensureScrollAnchor();
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
// Scroll to show the new message (conditional - only if user is near bottom)
|
log.info("Messages re-rendered with new message");
|
||||||
// This prevents interrupting users who are reading older messages
|
|
||||||
scrollToBottom(false);
|
|
||||||
|
|
||||||
log.info("UI updated with new message");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating UI with new message", e);
|
log.error("Error updating messages with new message", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ public class MessagesView extends Main {
|
|||||||
String preview = Optional.ofNullable(latest.getContent()).filter(s -> !s.isBlank()).orElse("(kein Inhalt)");
|
String preview = Optional.ofNullable(latest.getContent()).filter(s -> !s.isBlank()).orElse("(kein Inhalt)");
|
||||||
int totalMessages = conversation.size();
|
int totalMessages = conversation.size();
|
||||||
int unreadCount = (int) conversation.stream()
|
int unreadCount = (int) conversation.stream()
|
||||||
.filter(msg -> msg.getOrigin() == MessageOrigin.INCOMING && !msg.isRead())
|
.filter(msg -> msg.getOrigin() == MessageOrigin.CLIENT && !msg.isRead())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
summary.setTotalMessages(summary.getTotalMessages() + totalMessages);
|
summary.setTotalMessages(summary.getTotalMessages() + totalMessages);
|
||||||
@@ -243,7 +243,7 @@ public class MessagesView extends Main {
|
|||||||
if (message == null) {
|
if (message == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (message.getOrigin() == MessageOrigin.INCOMING) {
|
if (message.getOrigin() == MessageOrigin.CLIENT) {
|
||||||
return message.getSender();
|
return message.getSender();
|
||||||
}
|
}
|
||||||
return message.getReceiver();
|
return message.getReceiver();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.assecutor.votianlt.pages.view;
|
|||||||
|
|
||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H2;
|
import com.vaadin.flow.component.html.H2;
|
||||||
import com.vaadin.flow.component.html.H3;
|
import com.vaadin.flow.component.html.H3;
|
||||||
@@ -131,7 +130,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
|
|
||||||
Message latest = sortedMessages.isEmpty() ? null : sortedMessages.get(sortedMessages.size() - 1);
|
Message latest = sortedMessages.isEmpty() ? null : sortedMessages.get(sortedMessages.size() - 1);
|
||||||
int unreadCount = (int) sortedMessages.stream()
|
int unreadCount = (int) sortedMessages.stream()
|
||||||
.filter(message -> message.getOrigin() == MessageOrigin.INCOMING && !message.isRead())
|
.filter(message -> message.getOrigin() == MessageOrigin.CLIENT && !message.isRead())
|
||||||
.count();
|
.count();
|
||||||
int messageCount = sortedMessages.size();
|
int messageCount = sortedMessages.size();
|
||||||
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
|
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
|
||||||
@@ -173,7 +172,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
messages.sort(Comparator.comparing(Message::getCreatedAt, Comparator.nullsLast(LocalDateTime::compareTo)));
|
messages.sort(Comparator.comparing(Message::getCreatedAt, Comparator.nullsLast(LocalDateTime::compareTo)));
|
||||||
Message latest = messages.get(messages.size() - 1);
|
Message latest = messages.get(messages.size() - 1);
|
||||||
int unreadCount = (int) messages.stream()
|
int unreadCount = (int) messages.stream()
|
||||||
.filter(message -> message.getOrigin() == MessageOrigin.INCOMING && !message.isRead())
|
.filter(message -> message.getOrigin() == MessageOrigin.CLIENT && !message.isRead())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
String conversationTitle = "Auftrag " + jobKey;
|
String conversationTitle = "Auftrag " + jobKey;
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ public class MessageService {
|
|||||||
Message message;
|
Message message;
|
||||||
if (payload.hasJobContext()) {
|
if (payload.hasJobContext()) {
|
||||||
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
||||||
MessageOrigin.INCOMING, payload.jobId(), payload.jobNumber());
|
MessageOrigin.CLIENT, payload.jobId(), payload.jobNumber());
|
||||||
} else {
|
} else {
|
||||||
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
||||||
MessageOrigin.INCOMING);
|
MessageOrigin.CLIENT);
|
||||||
}
|
}
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user