Erweiterungen
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package de.assecutor.votianlt.controller;
|
package de.assecutor.votianlt.controller;
|
||||||
|
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.service.MessageService;
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -30,7 +31,7 @@ public class MessageApiController {
|
|||||||
/**
|
/**
|
||||||
* Send a general message to a client
|
* Send a general message to a client
|
||||||
* POST /api/messages/send
|
* POST /api/messages/send
|
||||||
* Body: { "content": "message text", "sender": "username", "receiver": "username" }
|
* Body: { "content": "message text", "sender": "username", "receiver": "username", "contentType": "TEXT|IMAGE" }
|
||||||
*/
|
*/
|
||||||
@PostMapping("/send")
|
@PostMapping("/send")
|
||||||
public ResponseEntity<Message> sendGeneralMessage(@RequestBody Map<String, String> request) {
|
public ResponseEntity<Message> sendGeneralMessage(@RequestBody Map<String, String> request) {
|
||||||
@@ -38,6 +39,7 @@ public class MessageApiController {
|
|||||||
String content = request.get("content");
|
String content = request.get("content");
|
||||||
String sender = request.get("sender");
|
String sender = request.get("sender");
|
||||||
String receiver = request.get("receiver");
|
String receiver = request.get("receiver");
|
||||||
|
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||||
|
|
||||||
if (content == null || content.isBlank() ||
|
if (content == null || content.isBlank() ||
|
||||||
sender == null || sender.isBlank() ||
|
sender == null || sender.isBlank() ||
|
||||||
@@ -46,10 +48,13 @@ public class MessageApiController {
|
|||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message message = messageService.sendGeneralMessageToClient(content, sender, receiver);
|
Message message = messageService.sendGeneralMessageToClient(content, sender, receiver, contentType);
|
||||||
log.info("General message sent from {} to {}", sender, receiver);
|
log.info("General message sent from {} to {}", sender, receiver);
|
||||||
return ResponseEntity.ok(message);
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Invalid general message request: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error sending general message: {}", e.getMessage(), e);
|
log.error("Error sending general message: {}", e.getMessage(), e);
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
@@ -60,7 +65,7 @@ public class MessageApiController {
|
|||||||
* Send a job-related message to a client
|
* Send a job-related message to a client
|
||||||
* POST /api/messages/send-job-message
|
* POST /api/messages/send-job-message
|
||||||
* Body: { "content": "message text", "sender": "username", "receiver": "username",
|
* Body: { "content": "message text", "sender": "username", "receiver": "username",
|
||||||
* "jobId": "job id", "jobNumber": "job number" }
|
* "jobId": "job id", "jobNumber": "job number", "contentType": "TEXT|IMAGE" }
|
||||||
*/
|
*/
|
||||||
@PostMapping("/send-job-message")
|
@PostMapping("/send-job-message")
|
||||||
public ResponseEntity<Message> sendJobMessage(@RequestBody Map<String, String> request) {
|
public ResponseEntity<Message> sendJobMessage(@RequestBody Map<String, String> request) {
|
||||||
@@ -70,6 +75,7 @@ public class MessageApiController {
|
|||||||
String receiver = request.get("receiver");
|
String receiver = request.get("receiver");
|
||||||
String jobIdStr = request.get("jobId");
|
String jobIdStr = request.get("jobId");
|
||||||
String jobNumber = request.get("jobNumber");
|
String jobNumber = request.get("jobNumber");
|
||||||
|
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||||
|
|
||||||
if (content == null || content.isBlank() ||
|
if (content == null || content.isBlank() ||
|
||||||
sender == null || sender.isBlank() ||
|
sender == null || sender.isBlank() ||
|
||||||
@@ -80,12 +86,12 @@ public class MessageApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ObjectId jobId = new ObjectId(jobIdStr);
|
ObjectId jobId = new ObjectId(jobIdStr);
|
||||||
Message message = messageService.sendJobMessageToClient(content, sender, receiver, jobId, jobNumber);
|
Message message = messageService.sendJobMessageToClient(content, sender, receiver, contentType, jobId, jobNumber);
|
||||||
log.info("Job-related message sent from {} to {} for job {}", sender, receiver, jobNumber);
|
log.info("Job-related message sent from {} to {} for job {}", sender, receiver, jobNumber);
|
||||||
return ResponseEntity.ok(message);
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.error("Invalid jobId format: {}", e.getMessage());
|
log.warn("Invalid job message request: {}", e.getMessage());
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error sending job message: {}", e.getMessage(), e);
|
log.error("Error sending job message: {}", e.getMessage(), e);
|
||||||
@@ -228,4 +234,16 @@ public class MessageApiController {
|
|||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MessageContentType resolveContentType(String rawValue) {
|
||||||
|
if (rawValue == null || rawValue.isBlank()) {
|
||||||
|
return MessageContentType.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return MessageContentType.valueOf(rawValue.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw new IllegalArgumentException("Unsupported contentType: " + rawValue, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,7 +601,8 @@ public class MessageController {
|
|||||||
* {
|
* {
|
||||||
* "sender": "appUserUsername",
|
* "sender": "appUserUsername",
|
||||||
* "receiver": "systemUserUsername",
|
* "receiver": "systemUserUsername",
|
||||||
* "content": "message text",
|
* "content": "message payload",
|
||||||
|
* "contentType": "TEXT|IMAGE",
|
||||||
* "jobId": "optional job id",
|
* "jobId": "optional job id",
|
||||||
* "jobNumber": "optional job number"
|
* "jobNumber": "optional job number"
|
||||||
* }
|
* }
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package de.assecutor.votianlt.dto;
|
package de.assecutor.votianlt.dto;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalized payload for chat messages sent by mobile clients via MQTT.
|
* Normalized payload for chat messages sent by mobile clients via MQTT.
|
||||||
*/
|
*/
|
||||||
public record ChatMessageInboundPayload(String sender, String receiver, String content, ObjectId jobId, String jobNumber) {
|
public record ChatMessageInboundPayload(String sender, String receiver, String content,
|
||||||
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
|
|
||||||
|
public ChatMessageInboundPayload {
|
||||||
|
contentType = contentType != null ? contentType : MessageContentType.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
public static ChatMessageInboundPayload fromPayload(Map<String, Object> payload) {
|
public static ChatMessageInboundPayload fromPayload(Map<String, Object> payload) {
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
@@ -17,10 +22,11 @@ public record ChatMessageInboundPayload(String sender, String receiver, String c
|
|||||||
String sender = extractRequiredString(payload, "sender");
|
String sender = extractRequiredString(payload, "sender");
|
||||||
String receiver = extractRequiredString(payload, "receiver");
|
String receiver = extractRequiredString(payload, "receiver");
|
||||||
String content = extractRequiredString(payload, "content");
|
String content = extractRequiredString(payload, "content");
|
||||||
|
MessageContentType contentType = extractContentType(payload.get("contentType"));
|
||||||
ObjectId jobId = extractObjectId(payload.get("jobId"), "jobId");
|
ObjectId jobId = extractObjectId(payload.get("jobId"), "jobId");
|
||||||
String jobNumber = extractOptionalString(payload.get("jobNumber"));
|
String jobNumber = extractOptionalString(payload.get("jobNumber"));
|
||||||
|
|
||||||
return new ChatMessageInboundPayload(sender, receiver, content, jobId, jobNumber);
|
return new ChatMessageInboundPayload(sender, receiver, content, contentType, jobId, jobNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasJobContext() {
|
public boolean hasJobContext() {
|
||||||
@@ -56,4 +62,16 @@ public record ChatMessageInboundPayload(String sender, String receiver, String c
|
|||||||
"Field '%s' must be a valid MongoDB ObjectId".formatted(fieldName), ex);
|
"Field '%s' must be a valid MongoDB ObjectId".formatted(fieldName), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MessageContentType extractContentType(Object value) {
|
||||||
|
String candidate = extractOptionalString(value);
|
||||||
|
if (candidate == null) {
|
||||||
|
return MessageContentType.TEXT;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return MessageContentType.valueOf(candidate.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw new IllegalArgumentException("Unsupported contentType '%s'".formatted(candidate), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.assecutor.votianlt.dto;
|
package de.assecutor.votianlt.dto;
|
||||||
|
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.model.MessageType;
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -13,6 +14,7 @@ public record ChatMessageOutboundPayload(
|
|||||||
String sender,
|
String sender,
|
||||||
String receiver,
|
String receiver,
|
||||||
String content,
|
String content,
|
||||||
|
MessageContentType contentType,
|
||||||
MessageOrigin origin,
|
MessageOrigin origin,
|
||||||
MessageType messageType,
|
MessageType messageType,
|
||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
@@ -27,6 +29,7 @@ public record ChatMessageOutboundPayload(
|
|||||||
message.getSender(),
|
message.getSender(),
|
||||||
message.getReceiver(),
|
message.getReceiver(),
|
||||||
message.getContent(),
|
message.getContent(),
|
||||||
|
message.getContentType(),
|
||||||
message.getOrigin(),
|
message.getOrigin(),
|
||||||
message.getMessageType(),
|
message.getMessageType(),
|
||||||
message.getCreatedAt(),
|
message.getCreatedAt(),
|
||||||
|
|||||||
@@ -25,11 +25,17 @@ public class Message {
|
|||||||
private ObjectId id;
|
private ObjectId id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content of the message
|
* Content of the message, either plain text or base64 encoded media
|
||||||
*/
|
*/
|
||||||
@Field("content")
|
@Field("content")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares how to interpret the content payload
|
||||||
|
*/
|
||||||
|
@Field("content_type")
|
||||||
|
private MessageContentType contentType = MessageContentType.TEXT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Username of the sender (app user or system user)
|
* Username of the sender (app user or system user)
|
||||||
*/
|
*/
|
||||||
@@ -88,29 +94,35 @@ public class Message {
|
|||||||
* Constructor for general messages
|
* Constructor for general messages
|
||||||
*/
|
*/
|
||||||
public Message(String content, String sender, String receiver, MessageOrigin origin) {
|
public Message(String content, String sender, String receiver, MessageOrigin origin) {
|
||||||
this.content = content;
|
this(content, sender, receiver, origin, MessageContentType.TEXT);
|
||||||
this.sender = sender;
|
}
|
||||||
this.receiver = receiver;
|
|
||||||
this.origin = origin;
|
/**
|
||||||
|
* Constructor for general messages with explicit content type
|
||||||
|
*/
|
||||||
|
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
||||||
|
MessageContentType contentType) {
|
||||||
|
initializeBaseFields(content, sender, receiver, origin, contentType);
|
||||||
this.messageType = MessageType.GENERAL;
|
this.messageType = MessageType.GENERAL;
|
||||||
this.createdAt = LocalDateTime.now();
|
|
||||||
this.isRead = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for job-related messages
|
* Constructor for job-related messages
|
||||||
*/
|
*/
|
||||||
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
||||||
ObjectId jobId, String jobNumber) {
|
ObjectId jobId, String jobNumber) {
|
||||||
this.content = content;
|
this(content, sender, receiver, origin, MessageContentType.TEXT, jobId, jobNumber);
|
||||||
this.sender = sender;
|
}
|
||||||
this.receiver = receiver;
|
|
||||||
this.origin = origin;
|
/**
|
||||||
|
* Constructor for job-related messages with explicit content type
|
||||||
|
*/
|
||||||
|
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
||||||
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
|
initializeBaseFields(content, sender, receiver, origin, contentType);
|
||||||
this.messageType = MessageType.JOB_RELATED;
|
this.messageType = MessageType.JOB_RELATED;
|
||||||
this.jobId = jobId;
|
this.jobId = jobId;
|
||||||
this.jobNumber = jobNumber;
|
this.jobNumber = jobNumber;
|
||||||
this.createdAt = LocalDateTime.now();
|
|
||||||
this.isRead = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,4 +148,22 @@ public class Message {
|
|||||||
public String getJobIdAsString() {
|
public String getJobIdAsString() {
|
||||||
return jobId != null ? jobId.toString() : null;
|
return jobId != null ? jobId.toString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure callers always receive a non-null content type to simplify rendering
|
||||||
|
*/
|
||||||
|
public MessageContentType getContentType() {
|
||||||
|
return contentType != null ? contentType : MessageContentType.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeBaseFields(String content, String sender, String receiver, MessageOrigin origin,
|
||||||
|
MessageContentType contentType) {
|
||||||
|
this.content = content;
|
||||||
|
this.sender = sender;
|
||||||
|
this.receiver = receiver;
|
||||||
|
this.origin = origin;
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.isRead = false;
|
||||||
|
this.contentType = contentType != null ? contentType : MessageContentType.TEXT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported content variants for chat messages.
|
||||||
|
*/
|
||||||
|
public enum MessageContentType {
|
||||||
|
TEXT,
|
||||||
|
IMAGE
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
package de.assecutor.votianlt.pages.view;
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
import com.vaadin.flow.component.AttachEvent;
|
import com.vaadin.flow.component.AttachEvent;
|
||||||
|
import com.vaadin.flow.component.Component;
|
||||||
import com.vaadin.flow.component.DetachEvent;
|
import com.vaadin.flow.component.DetachEvent;
|
||||||
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.dialog.Dialog;
|
||||||
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.Image;
|
||||||
import com.vaadin.flow.component.html.Main;
|
import com.vaadin.flow.component.html.Main;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.Scroller;
|
import com.vaadin.flow.component.orderedlayout.Scroller;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
|
||||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
|
||||||
import com.vaadin.flow.component.textfield.TextArea;
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.component.upload.Upload;
|
||||||
|
import com.vaadin.flow.component.upload.UploadI18N;
|
||||||
|
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
@@ -23,6 +30,7 @@ import com.vaadin.flow.router.RouteParameters;
|
|||||||
import com.vaadin.flow.shared.Registration;
|
import com.vaadin.flow.shared.Registration;
|
||||||
import de.assecutor.votianlt.model.AppUser;
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.model.MessageType;
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
@@ -33,13 +41,17 @@ import jakarta.annotation.security.RolesAllowed;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Route(value = "message-details/:clientId/:conversationId", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
@Route(value = "message-details/:clientId/:conversationId", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
@PageTitle("Nachrichtenverlauf")
|
@PageTitle("Nachrichtenverlauf")
|
||||||
@@ -63,8 +75,10 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
private Div scrollAnchor; // Anchor element for scrolling to bottom
|
private Div scrollAnchor; // Anchor element for scrolling to bottom
|
||||||
private List<Message> currentMessages; // Current messages being displayed for redrawing
|
private List<Message> currentMessages; // Current messages being displayed for redrawing
|
||||||
private Registration broadcasterRegistration; // Track listener registration
|
private Registration broadcasterRegistration; // Track listener registration
|
||||||
|
private TextArea messageInput;
|
||||||
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");
|
||||||
|
private static final int MAX_IMAGE_FILE_SIZE = 32 * 1024 * 1024; // 32 MB aligns with Spring settings
|
||||||
|
|
||||||
public MessageDetailsView(AppUserService appUserService, MessageService messageService,
|
public MessageDetailsView(AppUserService appUserService, MessageService messageService,
|
||||||
SecurityService securityService, MessageBroadcaster messageBroadcaster) {
|
SecurityService securityService, MessageBroadcaster messageBroadcaster) {
|
||||||
@@ -196,6 +210,141 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openImageUploadDialog() {
|
||||||
|
Dialog dialog = new Dialog();
|
||||||
|
dialog.setHeaderTitle("Bild anhängen");
|
||||||
|
dialog.setCloseOnEsc(true);
|
||||||
|
dialog.setCloseOnOutsideClick(true);
|
||||||
|
dialog.setWidth("480px");
|
||||||
|
|
||||||
|
MemoryBuffer buffer = new MemoryBuffer();
|
||||||
|
Upload upload = new Upload(buffer);
|
||||||
|
upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/gif", "image/webp");
|
||||||
|
upload.setMaxFiles(1);
|
||||||
|
upload.setMaxFileSize(MAX_IMAGE_FILE_SIZE);
|
||||||
|
upload.setI18n(createGermanUploadI18n());
|
||||||
|
upload.setWidthFull();
|
||||||
|
|
||||||
|
Span helper = new Span("Unterstützte Formate: PNG, JPG, GIF, WebP (max. 32 MB)");
|
||||||
|
helper.getStyle()
|
||||||
|
.set("font-size", "12px")
|
||||||
|
.set("color", "#666666");
|
||||||
|
|
||||||
|
Image preview = new Image();
|
||||||
|
preview.setAlt("Vorschau des ausgewählten Bildes");
|
||||||
|
preview.setVisible(false);
|
||||||
|
preview.setWidth(null);
|
||||||
|
preview.setHeight(null);
|
||||||
|
preview.getStyle()
|
||||||
|
.set("max-width", "100%")
|
||||||
|
.set("max-height", "320px")
|
||||||
|
.set("height", "auto")
|
||||||
|
.set("border-radius", "12px")
|
||||||
|
.set("display", "inline-block");
|
||||||
|
|
||||||
|
Div previewWrapper = new Div(preview);
|
||||||
|
previewWrapper.setWidthFull();
|
||||||
|
previewWrapper.getStyle()
|
||||||
|
.set("text-align", "center")
|
||||||
|
.set("margin-top", "10px");
|
||||||
|
|
||||||
|
AtomicReference<String> base64Ref = new AtomicReference<>();
|
||||||
|
|
||||||
|
Button confirmButton = new Button("Senden");
|
||||||
|
confirmButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
|
||||||
|
Button cancelButton = new Button("Abbrechen", event -> dialog.close());
|
||||||
|
|
||||||
|
upload.addSucceededListener(event -> {
|
||||||
|
try (InputStream inputStream = buffer.getInputStream()) {
|
||||||
|
byte[] bytes = inputStream.readAllBytes();
|
||||||
|
if (bytes.length == 0) {
|
||||||
|
base64Ref.set(null);
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
Notification.show("Die Bilddatei ist leer.", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String base64 = Base64.getEncoder().encodeToString(bytes);
|
||||||
|
base64Ref.set(base64);
|
||||||
|
|
||||||
|
String mimeType = Optional.ofNullable(event.getMIMEType()).filter(value -> !value.isBlank())
|
||||||
|
.orElseGet(() -> detectImageMimeType(bytes));
|
||||||
|
String dataUrl = createDataUrlFromContent(base64, mimeType);
|
||||||
|
if (dataUrl == null) {
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
Notification.show("Das Bild konnte nicht verarbeitet werden.", 3000,
|
||||||
|
Notification.Position.MIDDLE).addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.setSrc(dataUrl);
|
||||||
|
preview.setVisible(true);
|
||||||
|
confirmButton.setEnabled(true);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("Fehler beim Verarbeiten der Bilddatei", ex);
|
||||||
|
base64Ref.set(null);
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
Notification.show("Das Bild konnte nicht gelesen werden.", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
upload.addFileRejectedListener(event -> {
|
||||||
|
base64Ref.set(null);
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
Notification.show(event.getErrorMessage(), 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
upload.addFailedListener(event -> {
|
||||||
|
base64Ref.set(null);
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
Notification.show("Der Upload ist fehlgeschlagen.", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
upload.addFileRemovedListener(event -> {
|
||||||
|
base64Ref.set(null);
|
||||||
|
preview.setVisible(false);
|
||||||
|
confirmButton.setEnabled(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmButton.addClickListener(event -> {
|
||||||
|
String base64 = base64Ref.get();
|
||||||
|
if (base64 == null || base64.isBlank()) {
|
||||||
|
Notification.show("Bitte zuerst ein Bild auswählen.", 2500, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialog.close();
|
||||||
|
sendMessageToParticipant(base64, MessageContentType.IMAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
VerticalLayout dialogContent = new VerticalLayout(upload, helper, previewWrapper);
|
||||||
|
dialogContent.setSpacing(true);
|
||||||
|
dialogContent.setPadding(false);
|
||||||
|
dialogContent.setAlignItems(FlexComponent.Alignment.STRETCH);
|
||||||
|
dialogContent.setWidthFull();
|
||||||
|
|
||||||
|
HorizontalLayout buttonBar = new HorizontalLayout(cancelButton, confirmButton);
|
||||||
|
buttonBar.setWidthFull();
|
||||||
|
buttonBar.setSpacing(true);
|
||||||
|
buttonBar.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||||
|
|
||||||
|
dialog.add(dialogContent);
|
||||||
|
dialog.getFooter().add(buttonBar);
|
||||||
|
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a date separator component
|
* Create a date separator component
|
||||||
*/
|
*/
|
||||||
@@ -252,15 +401,8 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
.set("white-space", "pre-wrap")
|
.set("white-space", "pre-wrap")
|
||||||
.set("text-align", alignment);
|
.set("text-align", alignment);
|
||||||
|
|
||||||
// Message content
|
// Message content component (text or media)
|
||||||
Div contentDiv = new Div();
|
Component contentComponent = createContentComponent(message, alignment);
|
||||||
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
|
// Timestamp
|
||||||
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
||||||
@@ -270,12 +412,147 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
.set("display", "block")
|
.set("display", "block")
|
||||||
.set("text-align", alignment);
|
.set("text-align", alignment);
|
||||||
|
|
||||||
bubble.add(contentDiv, timeSpan);
|
bubble.add(contentComponent, timeSpan);
|
||||||
messageWrapper.add(bubble);
|
messageWrapper.add(bubble);
|
||||||
|
|
||||||
return messageWrapper;
|
return messageWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Component createContentComponent(Message message, String alignment) {
|
||||||
|
MessageContentType contentType = message.getContentType();
|
||||||
|
return switch (contentType) {
|
||||||
|
case IMAGE -> createImageContent(message.getContent(), alignment);
|
||||||
|
case TEXT -> createTextContent(message.getContent(), alignment);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Component createTextContent(String contentValue, String alignment) {
|
||||||
|
Div contentDiv = new Div();
|
||||||
|
String content = Optional.ofNullable(contentValue).filter(value -> !value.isBlank())
|
||||||
|
.orElse("(kein Inhalt)");
|
||||||
|
contentDiv.setText(content);
|
||||||
|
contentDiv.getStyle()
|
||||||
|
.set("font-size", "14px")
|
||||||
|
.set("color", "#000000")
|
||||||
|
.set("margin-bottom", "5px")
|
||||||
|
.set("text-align", alignment);
|
||||||
|
return contentDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Component createImageContent(String base64Value, String alignment) {
|
||||||
|
Div wrapper = new Div();
|
||||||
|
wrapper.getStyle()
|
||||||
|
.set("margin-bottom", "5px")
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("justify-content", "right".equals(alignment) ? "flex-end" : "flex-start");
|
||||||
|
|
||||||
|
String trimmed = Optional.ofNullable(base64Value).map(String::trim).orElse("");
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
wrapper.setText("(kein Bildinhalt)");
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dataUrl = createDataUrlFromContent(trimmed, null);
|
||||||
|
if (dataUrl == null) {
|
||||||
|
wrapper.setText("(Bild konnte nicht geladen werden)");
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image image = new Image(dataUrl, "Nachrichtenbild");
|
||||||
|
image.getStyle()
|
||||||
|
.set("max-width", "100%")
|
||||||
|
.set("border-radius", "12px")
|
||||||
|
.set("display", "block")
|
||||||
|
.set("max-height", "320px")
|
||||||
|
.set("height", "auto");
|
||||||
|
image.getElement().setAttribute("loading", "lazy");
|
||||||
|
|
||||||
|
wrapper.add(image);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createDataUrlFromContent(String contentValue, String mimeTypeHint) {
|
||||||
|
if (contentValue == null || contentValue.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = contentValue.trim();
|
||||||
|
if (trimmed.startsWith("data:")) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] imageBytes;
|
||||||
|
try {
|
||||||
|
imageBytes = Base64.getDecoder().decode(trimmed);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.warn("Ungültiger Base64-Inhalt für Bildnachricht", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mimeType = Optional.ofNullable(mimeTypeHint).filter(value -> !value.isBlank())
|
||||||
|
.orElseGet(() -> detectImageMimeType(imageBytes));
|
||||||
|
return "data:" + mimeType + ";base64," + trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectImageMimeType(byte[] bytes) {
|
||||||
|
if (bytes == null || bytes.length < 4) {
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bytes[0] & 0xFF) == 0xFF && (bytes[1] & 0xFF) == 0xD8) {
|
||||||
|
return "image/jpeg";
|
||||||
|
}
|
||||||
|
if ((bytes[0] & 0xFF) == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) {
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x38) {
|
||||||
|
return "image/gif";
|
||||||
|
}
|
||||||
|
if (bytes.length >= 12 && bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46
|
||||||
|
&& bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50) {
|
||||||
|
return "image/webp";
|
||||||
|
}
|
||||||
|
if (bytes[0] == 0x42 && bytes[1] == 0x4D) {
|
||||||
|
return "image/bmp";
|
||||||
|
}
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
private UploadI18N createGermanUploadI18n() {
|
||||||
|
UploadI18N i18n = new UploadI18N();
|
||||||
|
|
||||||
|
UploadI18N.AddFiles addFiles = new UploadI18N.AddFiles();
|
||||||
|
addFiles.setOne("Bild auswählen");
|
||||||
|
addFiles.setMany("Bilder auswählen");
|
||||||
|
i18n.setAddFiles(addFiles);
|
||||||
|
|
||||||
|
UploadI18N.DropFiles dropFiles = new UploadI18N.DropFiles();
|
||||||
|
dropFiles.setOne("Bild hierher ziehen");
|
||||||
|
dropFiles.setMany("Bilder hierher ziehen");
|
||||||
|
i18n.setDropFiles(dropFiles);
|
||||||
|
|
||||||
|
UploadI18N.Error error = new UploadI18N.Error();
|
||||||
|
error.setFileIsTooBig("Die Datei ist größer als 32 MB.");
|
||||||
|
error.setTooManyFiles("Es kann nur ein Bild gleichzeitig hochgeladen werden.");
|
||||||
|
i18n.setError(error);
|
||||||
|
|
||||||
|
UploadI18N.Uploading uploading = new UploadI18N.Uploading();
|
||||||
|
uploading.setStatus(new UploadI18N.Uploading.Status()
|
||||||
|
.setConnecting("Verbindung wird aufgebaut...")
|
||||||
|
.setStalled("Upload pausiert")
|
||||||
|
.setProcessing("Verarbeitung...")
|
||||||
|
.setHeld("Warten auf Upload..."));
|
||||||
|
uploading.setRemainingTime(new UploadI18N.Uploading.RemainingTime()
|
||||||
|
.setPrefix("Verbleibende Zeit: ")
|
||||||
|
.setUnknown("Verbleibende Zeit unbekannt"));
|
||||||
|
uploading.setError(new UploadI18N.Uploading.Error()
|
||||||
|
.setServerUnavailable("Server nicht erreichbar")
|
||||||
|
.setUnexpectedServerError("Unerwarteter Serverfehler")
|
||||||
|
.setForbidden("Upload nicht erlaubt"));
|
||||||
|
i18n.setUploading(uploading);
|
||||||
|
|
||||||
|
return i18n;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure scroll anchor exists at the bottom of messages container
|
* Ensure scroll anchor exists at the bottom of messages container
|
||||||
*/
|
*/
|
||||||
@@ -326,43 +603,64 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
|
|
||||||
|
|
||||||
private HorizontalLayout createMessageInputArea() {
|
private HorizontalLayout createMessageInputArea() {
|
||||||
TextArea messageInput = new TextArea();
|
messageInput = new TextArea();
|
||||||
messageInput.setPlaceholder("Nachricht eingeben...");
|
messageInput.setPlaceholder("Nachricht eingeben...");
|
||||||
messageInput.setWidthFull();
|
messageInput.setWidthFull();
|
||||||
messageInput.getStyle().set("min-height", "60px");
|
messageInput.getStyle().set("min-height", "60px");
|
||||||
messageInput.getStyle().set("max-height", "120px");
|
messageInput.getStyle().set("max-height", "120px");
|
||||||
|
|
||||||
|
Button attachButton = new Button(VaadinIcon.PAPERCLIP.create());
|
||||||
|
attachButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_TERTIARY,
|
||||||
|
com.vaadin.flow.component.button.ButtonVariant.LUMO_ICON);
|
||||||
|
attachButton.getStyle().set("height", "60px");
|
||||||
|
attachButton.getElement().setProperty("title", "Bild anhängen");
|
||||||
|
attachButton.getElement().setAttribute("aria-label", "Bild anhängen");
|
||||||
|
attachButton.addClickListener(event -> openImageUploadDialog());
|
||||||
|
|
||||||
Button sendButton = new Button("Senden", VaadinIcon.PAPERPLANE.create());
|
Button sendButton = new Button("Senden", VaadinIcon.PAPERPLANE.create());
|
||||||
sendButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
sendButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
||||||
sendButton.getStyle().set("height", "60px");
|
sendButton.getStyle().set("height", "60px");
|
||||||
|
|
||||||
sendButton.addClickListener(e -> {
|
sendButton.addClickListener(e -> {
|
||||||
String message = messageInput.getValue();
|
String message = messageInput.getValue();
|
||||||
if (message != null && !message.trim().isEmpty()) {
|
if (message != null && !message.trim().isEmpty()) {
|
||||||
sendMessageToParticipant(message.trim());
|
sendMessageToParticipant(message, MessageContentType.TEXT);
|
||||||
messageInput.clear();
|
messageInput.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
HorizontalLayout layout = new HorizontalLayout(messageInput, sendButton);
|
HorizontalLayout layout = new HorizontalLayout(attachButton, messageInput, sendButton);
|
||||||
layout.setWidthFull();
|
layout.setWidthFull();
|
||||||
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.END);
|
layout.setAlignItems(FlexComponent.Alignment.END);
|
||||||
|
layout.setSpacing(true);
|
||||||
layout.expand(messageInput);
|
layout.expand(messageInput);
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessageToParticipant(String content) {
|
private void sendMessageToParticipant(String content, MessageContentType contentType) {
|
||||||
|
String payload = Optional.ofNullable(content).orElse("");
|
||||||
|
if (contentType == MessageContentType.TEXT) {
|
||||||
|
payload = payload.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.isEmpty()) {
|
||||||
|
Notification.show("Nachricht enthält keinen Inhalt.", 2500, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String sender = Optional.ofNullable(securityService.getCurrentUsername()).filter(name -> !name.isBlank())
|
String sender = Optional.ofNullable(securityService.getCurrentUsername()).filter(name -> !name.isBlank())
|
||||||
.orElse("System");
|
.orElse("System");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Message saved;
|
Message saved;
|
||||||
if (jobConversation) {
|
if (jobConversation) {
|
||||||
saved = messageService.sendJobMessageToClient(content, sender, participantKey, jobIdContext,
|
saved = messageService.sendJobMessageToClient(payload, sender, participantKey,
|
||||||
jobNumberContext);
|
contentType, jobIdContext, jobNumberContext);
|
||||||
} else {
|
} else {
|
||||||
saved = messageService.sendGeneralMessageToClient(content, sender, participantKey);
|
saved = messageService.sendGeneralMessageToClient(payload, sender, participantKey,
|
||||||
|
contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
|
Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.vaadin.flow.router.Route;
|
|||||||
import de.assecutor.votianlt.dto.ClientMessageSummary;
|
import de.assecutor.votianlt.dto.ClientMessageSummary;
|
||||||
import de.assecutor.votianlt.model.AppUser;
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
import de.assecutor.votianlt.service.MessageService;
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
@@ -207,7 +208,7 @@ public class MessagesView extends Main {
|
|||||||
.orElse(conversation.get(0));
|
.orElse(conversation.get(0));
|
||||||
|
|
||||||
LocalDateTime lastDate = latest.getCreatedAt();
|
LocalDateTime lastDate = latest.getCreatedAt();
|
||||||
String preview = Optional.ofNullable(latest.getContent()).filter(s -> !s.isBlank()).orElse("(kein Inhalt)");
|
String preview = resolvePreview(latest);
|
||||||
int totalMessages = conversation.size();
|
int totalMessages = conversation.size();
|
||||||
int unreadCount = (int) conversation.stream()
|
int unreadCount = (int) conversation.stream()
|
||||||
.filter(msg -> msg.getOrigin() == MessageOrigin.CLIENT && !msg.isRead())
|
.filter(msg -> msg.getOrigin() == MessageOrigin.CLIENT && !msg.isRead())
|
||||||
@@ -239,6 +240,20 @@ public class MessagesView extends Main {
|
|||||||
return summaries;
|
return summaries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolvePreview(Message message) {
|
||||||
|
if (message == null) {
|
||||||
|
return "(kein Inhalt)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getContentType() == MessageContentType.IMAGE) {
|
||||||
|
return "[Bildnachricht]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.ofNullable(message.getContent())
|
||||||
|
.filter(content -> !content.isBlank())
|
||||||
|
.orElse("(kein Inhalt)");
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveParticipantKey(Message message) {
|
private String resolveParticipantKey(Message message) {
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.vaadin.flow.router.PageTitle;
|
|||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import de.assecutor.votianlt.model.AppUser;
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.model.MessageType;
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
@@ -134,7 +135,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
.count();
|
.count();
|
||||||
int messageCount = sortedMessages.size();
|
int messageCount = sortedMessages.size();
|
||||||
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
|
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
|
||||||
String preview = latest != null ? latest.getContent() : null;
|
String preview = resolvePreview(latest);
|
||||||
|
|
||||||
section.add(createMessageCard(
|
section.add(createMessageCard(
|
||||||
"Allgemeine Unterhaltung",
|
"Allgemeine Unterhaltung",
|
||||||
@@ -178,7 +179,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
String conversationTitle = "Auftrag " + jobKey;
|
String conversationTitle = "Auftrag " + jobKey;
|
||||||
section.add(createMessageCard(
|
section.add(createMessageCard(
|
||||||
conversationTitle,
|
conversationTitle,
|
||||||
Optional.ofNullable(latest.getContent()).orElse(""),
|
resolvePreview(latest),
|
||||||
latest.getCreatedAt(),
|
latest.getCreatedAt(),
|
||||||
messages.size(),
|
messages.size(),
|
||||||
unreadCount,
|
unreadCount,
|
||||||
@@ -189,6 +190,20 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolvePreview(Message message) {
|
||||||
|
if (message == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getContentType() == MessageContentType.IMAGE) {
|
||||||
|
return "[Bildnachricht]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.ofNullable(message.getContent())
|
||||||
|
.map(String::trim)
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
private Div createMessageCard(String conversationTitle, String lastMessagePreview,
|
private Div createMessageCard(String conversationTitle, String lastMessagePreview,
|
||||||
LocalDateTime lastMessageTime, int messageCount,
|
LocalDateTime lastMessageTime, int messageCount,
|
||||||
int unreadCount, String conversationId) {
|
int unreadCount, String conversationId) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.assecutor.votianlt.service;
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
import de.assecutor.votianlt.model.Message;
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageContentType;
|
||||||
import de.assecutor.votianlt.model.MessageOrigin;
|
import de.assecutor.votianlt.model.MessageOrigin;
|
||||||
import de.assecutor.votianlt.model.MessageType;
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
import de.assecutor.votianlt.dto.ChatMessageInboundPayload;
|
import de.assecutor.votianlt.dto.ChatMessageInboundPayload;
|
||||||
@@ -44,7 +45,12 @@ public class MessageService {
|
|||||||
* Send a general message to a client via MQTT
|
* Send a general message to a client via MQTT
|
||||||
*/
|
*/
|
||||||
public Message sendGeneralMessageToClient(String content, String sender, String receiver) {
|
public Message sendGeneralMessageToClient(String content, String sender, String receiver) {
|
||||||
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER);
|
return sendGeneralMessageToClient(content, sender, receiver, MessageContentType.TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message sendGeneralMessageToClient(String content, String sender, String receiver,
|
||||||
|
MessageContentType contentType) {
|
||||||
|
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType);
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
publishMessageToMqtt(message, receiver);
|
publishMessageToMqtt(message, receiver);
|
||||||
return message;
|
return message;
|
||||||
@@ -53,9 +59,14 @@ public class MessageService {
|
|||||||
/**
|
/**
|
||||||
* Send a job-related message to a client via MQTT
|
* Send a job-related message to a client via MQTT
|
||||||
*/
|
*/
|
||||||
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
||||||
ObjectId jobId, String jobNumber) {
|
ObjectId jobId, String jobNumber) {
|
||||||
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, jobId, jobNumber);
|
return sendJobMessageToClient(content, sender, receiver, MessageContentType.TEXT, jobId, jobNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
||||||
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
|
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType, jobId, jobNumber);
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
publishMessageToMqtt(message, receiver);
|
publishMessageToMqtt(message, receiver);
|
||||||
return message;
|
return message;
|
||||||
@@ -66,12 +77,13 @@ public class MessageService {
|
|||||||
*/
|
*/
|
||||||
public Message receiveMessageFromClient(ChatMessageInboundPayload payload) {
|
public Message receiveMessageFromClient(ChatMessageInboundPayload payload) {
|
||||||
Message message;
|
Message message;
|
||||||
|
MessageContentType contentType = payload.contentType();
|
||||||
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.CLIENT, payload.jobId(), payload.jobNumber());
|
MessageOrigin.CLIENT, contentType, 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.CLIENT);
|
MessageOrigin.CLIENT, contentType);
|
||||||
}
|
}
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user