This commit is contained in:
@@ -31,25 +31,23 @@ 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", "contentType": "TEXT|IMAGE" }
|
* Body: { "content": "message text", "receiver": "appUserId", "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) {
|
||||||
try {
|
try {
|
||||||
String content = request.get("content");
|
String content = request.get("content");
|
||||||
String sender = request.get("sender");
|
|
||||||
String receiver = request.get("receiver");
|
String receiver = request.get("receiver");
|
||||||
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||||
|
|
||||||
if (content == null || content.isBlank() ||
|
if (content == null || content.isBlank() ||
|
||||||
sender == null || sender.isBlank() ||
|
|
||||||
receiver == null || receiver.isBlank()) {
|
receiver == null || receiver.isBlank()) {
|
||||||
log.warn("Invalid message request: missing required fields");
|
log.warn("Invalid message request: missing required fields");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message message = messageService.sendGeneralMessageToClient(content, sender, receiver, contentType);
|
Message message = messageService.sendGeneralMessageToClient(content, receiver, contentType);
|
||||||
log.info("General message sent from {} to {}", sender, receiver);
|
log.info("General message sent to AppUser '{}'", receiver);
|
||||||
return ResponseEntity.ok(message);
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -64,21 +62,19 @@ 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", "receiver": "appUserId",
|
||||||
* "jobId": "job id", "jobNumber": "job number", "contentType": "TEXT|IMAGE" }
|
* "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) {
|
||||||
try {
|
try {
|
||||||
String content = request.get("content");
|
String content = request.get("content");
|
||||||
String sender = request.get("sender");
|
|
||||||
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"));
|
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||||
|
|
||||||
if (content == null || content.isBlank() ||
|
if (content == null || content.isBlank() ||
|
||||||
sender == null || sender.isBlank() ||
|
|
||||||
receiver == null || receiver.isBlank() ||
|
receiver == null || receiver.isBlank() ||
|
||||||
jobIdStr == null || jobIdStr.isBlank()) {
|
jobIdStr == null || jobIdStr.isBlank()) {
|
||||||
log.warn("Invalid job message request: missing required fields");
|
log.warn("Invalid job message request: missing required fields");
|
||||||
@@ -86,8 +82,8 @@ public class MessageApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ObjectId jobId = new ObjectId(jobIdStr);
|
ObjectId jobId = new ObjectId(jobIdStr);
|
||||||
Message message = messageService.sendJobMessageToClient(content, sender, receiver, contentType, jobId, jobNumber);
|
Message message = messageService.sendJobMessageToClient(content, receiver, contentType, jobId, jobNumber);
|
||||||
log.info("Job-related message sent from {} to {} for job {}", sender, receiver, jobNumber);
|
log.info("Job-related message sent to AppUser '{}' for job {}", receiver, jobNumber);
|
||||||
return ResponseEntity.ok(message);
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
|||||||
@@ -613,27 +613,19 @@ public class MessageController {
|
|||||||
* Handle incoming message from a client via MQTT.
|
* Handle incoming message from a client via MQTT.
|
||||||
* Client sends to /server/{clientId}/message with payload:
|
* Client sends to /server/{clientId}/message with payload:
|
||||||
* {
|
* {
|
||||||
* "sender": "appUserUsername",
|
|
||||||
* "receiver": "systemUserUsername",
|
|
||||||
* "content": "message payload",
|
* "content": "message payload",
|
||||||
* "contentType": "TEXT|IMAGE",
|
* "contentType": "TEXT|IMAGE",
|
||||||
* "jobId": "optional job id",
|
* "jobId": "optional job id",
|
||||||
* "jobNumber": "optional job number",
|
* "jobNumber": "optional job number"
|
||||||
* "clientId": "extracted from topic"
|
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Logic:
|
* The clientId is extracted from the MQTT topic and represents the AppUser ID.
|
||||||
* 1. Extract clientId from topic (this is the AppUser ID)
|
* This clientId is stored as the receiver field in the message.
|
||||||
* 2. Find AppUser by ID in database
|
|
||||||
* 3. Get owner (User) from AppUser.owner field
|
|
||||||
* 4. Set receiver = User ID, sender = AppUser ID
|
|
||||||
*/
|
*/
|
||||||
public void handleIncomingMessage(Map<String, Object> payload) {
|
public void handleIncomingMessage(Map<String, Object> payload) {
|
||||||
log.info("MQTT Endpoint '/server/{clientId}/message' called with data: {}", payload);
|
log.info("MQTT Endpoint '/server/{clientId}/message' called with data: {}", payload);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
|
|
||||||
|
|
||||||
// Extract clientId from payload (added by MqttV5ClientManager from topic)
|
// Extract clientId from payload (added by MqttV5ClientManager from topic)
|
||||||
// The clientId IS the AppUser ID
|
// The clientId IS the AppUser ID
|
||||||
String clientId = payload.get("clientId") != null ? payload.get("clientId").toString() : null;
|
String clientId = payload.get("clientId") != null ? payload.get("clientId").toString() : null;
|
||||||
@@ -643,53 +635,15 @@ public class MessageController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert clientId (AppUser ID) to ObjectId
|
// Add clientId as receiver to the payload
|
||||||
ObjectId appUserObjectId;
|
payload.put("receiver", clientId);
|
||||||
try {
|
|
||||||
appUserObjectId = new ObjectId(clientId);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.warn("Invalid clientId/AppUser ID '{}': {}", clientId, e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find AppUser by ID
|
// Parse the payload
|
||||||
AppUser appUser = appUserService.findById(appUserObjectId);
|
ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
|
||||||
if (appUser == null) {
|
|
||||||
log.warn("AppUser not found for clientId '{}'", clientId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get owner (User) of AppUser from the owner field
|
// Save the message with receiver = AppUser ID (clientId)
|
||||||
ObjectId ownerId = appUser.getOwner();
|
messageService.receiveMessageFromClient(inboundPayload);
|
||||||
if (ownerId == null) {
|
log.info("Successfully saved incoming message for AppUser '{}'", clientId);
|
||||||
log.warn("AppUser '{}' has no owner, cannot determine receiver", clientId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that owner exists
|
|
||||||
de.assecutor.votianlt.model.User owner = userService.findById(ownerId);
|
|
||||||
if (owner == null) {
|
|
||||||
log.warn("Owner User not found for AppUser '{}'", clientId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert owner ID to string for receiver field
|
|
||||||
String ownerIdString = ownerId.toHexString();
|
|
||||||
|
|
||||||
// Create payload with:
|
|
||||||
// - sender = AppUser ID (clientId)
|
|
||||||
// - receiver = User ID (owner's ID as string)
|
|
||||||
ChatMessageInboundPayload resolvedPayload = new ChatMessageInboundPayload(
|
|
||||||
clientId, // sender = AppUser ID
|
|
||||||
ownerIdString, // receiver = User ID
|
|
||||||
inboundPayload.content(),
|
|
||||||
inboundPayload.contentType(),
|
|
||||||
inboundPayload.jobId(),
|
|
||||||
inboundPayload.jobNumber()
|
|
||||||
);
|
|
||||||
|
|
||||||
messageService.receiveMessageFromClient(resolvedPayload);
|
|
||||||
log.info("Successfully saved incoming message from AppUser '{}' to User '{}'", clientId, ownerIdString);
|
|
||||||
} catch (IllegalArgumentException validationError) {
|
} catch (IllegalArgumentException validationError) {
|
||||||
log.warn("Incoming chat message rejected: {}", validationError.getMessage());
|
log.warn("Incoming chat message rejected: {}", validationError.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ 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.
|
||||||
|
* receiver = AppUser ID (clientId) extracted from MQTT topic
|
||||||
*/
|
*/
|
||||||
public record ChatMessageInboundPayload(String sender, String receiver, String content,
|
public record ChatMessageInboundPayload(String receiver, String content,
|
||||||
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
|
|
||||||
public ChatMessageInboundPayload {
|
public ChatMessageInboundPayload {
|
||||||
@@ -19,14 +20,13 @@ public record ChatMessageInboundPayload(String sender, String receiver, String c
|
|||||||
throw new IllegalArgumentException("payload must not be null");
|
throw new IllegalArgumentException("payload must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
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"));
|
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, contentType, jobId, jobNumber);
|
return new ChatMessageInboundPayload(receiver, content, contentType, jobId, jobNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasJobContext() {
|
public boolean hasJobContext() {
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Outbound chat message payload published to MQTT subscribers.
|
* Outbound chat message payload published to MQTT subscribers.
|
||||||
|
* The receiver is implicit from the MQTT topic (/client/{appUserId}/message)
|
||||||
*/
|
*/
|
||||||
public record ChatMessageOutboundPayload(
|
public record ChatMessageOutboundPayload(
|
||||||
String messageId,
|
String messageId,
|
||||||
String sender,
|
|
||||||
String receiver,
|
|
||||||
String content,
|
String content,
|
||||||
MessageContentType contentType,
|
MessageContentType contentType,
|
||||||
MessageOrigin origin,
|
MessageOrigin origin,
|
||||||
@@ -26,8 +25,6 @@ public record ChatMessageOutboundPayload(
|
|||||||
public static ChatMessageOutboundPayload fromMessage(Message message) {
|
public static ChatMessageOutboundPayload fromMessage(Message message) {
|
||||||
return new ChatMessageOutboundPayload(
|
return new ChatMessageOutboundPayload(
|
||||||
message.getIdAsString(),
|
message.getIdAsString(),
|
||||||
message.getSender(),
|
|
||||||
message.getReceiver(),
|
|
||||||
message.getContent(),
|
message.getContent(),
|
||||||
message.getContentType(),
|
message.getContentType(),
|
||||||
message.getOrigin(),
|
message.getOrigin(),
|
||||||
|
|||||||
@@ -37,13 +37,7 @@ public class Message {
|
|||||||
private MessageContentType contentType = MessageContentType.TEXT;
|
private MessageContentType contentType = MessageContentType.TEXT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Username of the sender (app user or system user)
|
* AppUser ID (clientId) - the AppUser to whom this message belongs
|
||||||
*/
|
|
||||||
@Field("sender")
|
|
||||||
private String sender;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Username of the receiver (app user or system user)
|
|
||||||
*/
|
*/
|
||||||
@Field("receiver")
|
@Field("receiver")
|
||||||
private String receiver;
|
private String receiver;
|
||||||
@@ -93,33 +87,33 @@ 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 receiver, MessageOrigin origin) {
|
||||||
this(content, sender, receiver, origin, MessageContentType.TEXT);
|
this(content, receiver, origin, MessageContentType.TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for general messages with explicit content type
|
* Constructor for general messages with explicit content type
|
||||||
*/
|
*/
|
||||||
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
public Message(String content, String receiver, MessageOrigin origin,
|
||||||
MessageContentType contentType) {
|
MessageContentType contentType) {
|
||||||
initializeBaseFields(content, sender, receiver, origin, contentType);
|
initializeBaseFields(content, receiver, origin, contentType);
|
||||||
this.messageType = MessageType.GENERAL;
|
this.messageType = MessageType.GENERAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 receiver, MessageOrigin origin,
|
||||||
ObjectId jobId, String jobNumber) {
|
ObjectId jobId, String jobNumber) {
|
||||||
this(content, sender, receiver, origin, MessageContentType.TEXT, jobId, jobNumber);
|
this(content, receiver, origin, MessageContentType.TEXT, jobId, jobNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for job-related messages with explicit content type
|
* Constructor for job-related messages with explicit content type
|
||||||
*/
|
*/
|
||||||
public Message(String content, String sender, String receiver, MessageOrigin origin,
|
public Message(String content, String receiver, MessageOrigin origin,
|
||||||
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
initializeBaseFields(content, sender, receiver, origin, contentType);
|
initializeBaseFields(content, 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;
|
||||||
@@ -156,10 +150,9 @@ public class Message {
|
|||||||
return contentType != null ? contentType : MessageContentType.TEXT;
|
return contentType != null ? contentType : MessageContentType.TEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeBaseFields(String content, String sender, String receiver, MessageOrigin origin,
|
private void initializeBaseFields(String content, String receiver, MessageOrigin origin,
|
||||||
MessageContentType contentType) {
|
MessageContentType contentType) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.sender = sender;
|
|
||||||
this.receiver = receiver;
|
this.receiver = receiver;
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
this.createdAt = LocalDateTime.now();
|
this.createdAt = LocalDateTime.now();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.vaadin.flow.server.menu.MenuEntry;
|
|||||||
import com.vaadin.flow.shared.Registration;
|
import com.vaadin.flow.shared.Registration;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
import de.assecutor.votianlt.model.UserInvoiceData;
|
import de.assecutor.votianlt.model.UserInvoiceData;
|
||||||
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
||||||
import de.assecutor.votianlt.pages.view.EditProfileView;
|
import de.assecutor.votianlt.pages.view.EditProfileView;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
@@ -34,9 +35,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -47,6 +47,7 @@ public final class MainLayout extends AppLayout {
|
|||||||
private final UserInvoiceDataService userInvoiceDataService;
|
private final UserInvoiceDataService userInvoiceDataService;
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
private final MessageBadgeUpdateService messageBadgeUpdateService;
|
private final MessageBadgeUpdateService messageBadgeUpdateService;
|
||||||
|
private final AppUserService appUserService;
|
||||||
private Div headerRef;
|
private Div headerRef;
|
||||||
private Scroller navRef;
|
private Scroller navRef;
|
||||||
private Component userMenuRef;
|
private Component userMenuRef;
|
||||||
@@ -55,11 +56,13 @@ public final class MainLayout extends AppLayout {
|
|||||||
private Registration badgeUpdateRegistration; // Track badge update listener registration
|
private Registration badgeUpdateRegistration; // Track badge update listener registration
|
||||||
|
|
||||||
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
|
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
|
||||||
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService) {
|
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
|
||||||
|
AppUserService appUserService) {
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
this.userInvoiceDataService = userInvoiceDataService;
|
this.userInvoiceDataService = userInvoiceDataService;
|
||||||
this.messageService = messageService;
|
this.messageService = messageService;
|
||||||
this.messageBadgeUpdateService = messageBadgeUpdateService;
|
this.messageBadgeUpdateService = messageBadgeUpdateService;
|
||||||
|
this.appUserService = appUserService;
|
||||||
setPrimarySection(Section.DRAWER);
|
setPrimarySection(Section.DRAWER);
|
||||||
|
|
||||||
// Always build the drawer; keep references and toggle visibility on attach and
|
// Always build the drawer; keep references and toggle visibility on attach and
|
||||||
@@ -218,47 +221,28 @@ public final class MainLayout extends AppLayout {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> candidateReceivers = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
User currentUser = securityService.getCurrentDatabaseUser();
|
// Get all AppUsers for the current user
|
||||||
|
List<de.assecutor.votianlt.model.AppUser> appUsers = appUserService.findByCurrentUser();
|
||||||
|
|
||||||
if (currentUser != null) {
|
if (appUsers == null || appUsers.isEmpty()) {
|
||||||
// Add User ID (ObjectId as string) - this is now the primary receiver identifier
|
return 0;
|
||||||
if (currentUser.getId() != null) {
|
}
|
||||||
candidateReceivers.add(currentUser.getId().toHexString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add email for backward compatibility with old messages
|
// Count unread messages for all AppUsers (receiver = AppUser ID)
|
||||||
String email = Optional.ofNullable(currentUser.getEmail()).map(String::trim).orElse("");
|
long unread = 0;
|
||||||
if (!email.isBlank()) {
|
for (de.assecutor.votianlt.model.AppUser appUser : appUsers) {
|
||||||
candidateReceivers.add(email);
|
if (appUser != null && appUser.getId() != null) {
|
||||||
}
|
String appUserId = appUser.getId().toHexString();
|
||||||
|
unread += messageService.getUnreadMessageCount(appUserId);
|
||||||
// Also add full name for backward compatibility
|
|
||||||
String fullName = ((Optional.ofNullable(currentUser.getFirstname()).orElse("") + " "
|
|
||||||
+ Optional.ofNullable(currentUser.getName()).orElse(""))).trim();
|
|
||||||
if (!fullName.isBlank()) {
|
|
||||||
candidateReceivers.add(fullName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (RuntimeException ignored) {
|
|
||||||
// Fallback to username only
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional.ofNullable(securityService.getCurrentUsername()).map(String::trim).filter(name -> !name.isBlank())
|
return unread;
|
||||||
.ifPresent(candidateReceivers::add);
|
} catch (RuntimeException e) {
|
||||||
|
log.error("Error resolving unread message count: {}", e.getMessage(), e);
|
||||||
if (candidateReceivers.isEmpty()) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
long unread = 0;
|
|
||||||
for (String receiver : candidateReceivers) {
|
|
||||||
unread += messageService.getUnreadMessageCount(receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unread;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Component createUserMenu() {
|
private Component createUserMenu() {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
clientName = Optional.ofNullable(client).map(AppUser::getBezeichnung).orElse("Unbekannter Teilnehmer");
|
clientName = Optional.ofNullable(client).map(AppUser::getBezeichnung).orElse("Unbekannter Teilnehmer");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Message> allMessages = messageService.getMessagesForParticipantAscending(participantKey);
|
List<Message> allMessages = messageService.getMessagesForAppUserAscending(participantKey);
|
||||||
List<Message> filteredMessages = filterMessagesForConversation(allMessages, conversationId);
|
List<Message> filteredMessages = filterMessagesForConversation(allMessages, conversationId);
|
||||||
|
|
||||||
this.jobConversation = conversationId != null && conversationId.startsWith("job-");
|
this.jobConversation = conversationId != null && conversationId.startsWith("job-");
|
||||||
@@ -808,10 +808,12 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
try {
|
try {
|
||||||
Message saved;
|
Message saved;
|
||||||
if (jobConversation) {
|
if (jobConversation) {
|
||||||
saved = messageService.sendJobMessageToClient(payload, sender, participantKey,
|
// participantKey = AppUser ID (receiver)
|
||||||
|
saved = messageService.sendJobMessageToClient(payload, participantKey,
|
||||||
contentType, jobIdContext, jobNumberContext);
|
contentType, jobIdContext, jobNumberContext);
|
||||||
} else {
|
} else {
|
||||||
saved = messageService.sendGeneralMessageToClient(payload, sender, participantKey,
|
// participantKey = AppUser ID (receiver)
|
||||||
|
saved = messageService.sendGeneralMessageToClient(payload, participantKey,
|
||||||
contentType);
|
contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,12 +1020,11 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if message involves the current participant
|
// Check if message belongs to the current participant (receiver = AppUser ID)
|
||||||
boolean involvesParticipant = participantKey.equals(message.getSender())
|
boolean involvesParticipant = participantKey.equals(message.getReceiver());
|
||||||
|| participantKey.equals(message.getReceiver());
|
|
||||||
|
|
||||||
if (!involvesParticipant) {
|
if (!involvesParticipant) {
|
||||||
log.debug("Message does not involve current participant, ignoring");
|
log.debug("Message does not belong to current participant, ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,9 +258,7 @@ public class MessagesView extends Main {
|
|||||||
if (message == null) {
|
if (message == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (message.getOrigin() == MessageOrigin.CLIENT) {
|
// All messages now have receiver = AppUser ID (clientId)
|
||||||
return message.getSender();
|
|
||||||
}
|
|
||||||
return message.getReceiver();
|
return message.getReceiver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
|||||||
HorizontalLayout headerLayout = createHeaderLayout(clientName);
|
HorizontalLayout headerLayout = createHeaderLayout(clientName);
|
||||||
contentLayout.add(headerLayout);
|
contentLayout.add(headerLayout);
|
||||||
|
|
||||||
List<Message> conversation = messageService.getMessagesForParticipantAscending(participantKey);
|
List<Message> conversation = messageService.getMessagesForAppUserAscending(participantKey);
|
||||||
Map<MessageType, List<Message>> messagesByType = conversation.stream()
|
Map<MessageType, List<Message>> messagesByType = conversation.stream()
|
||||||
.collect(Collectors.groupingBy(message -> Optional.ofNullable(message.getMessageType()).orElse(MessageType.GENERAL)));
|
.collect(Collectors.groupingBy(message -> Optional.ofNullable(message.getMessageType()).orElse(MessageType.GENERAL)));
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ import java.util.List;
|
|||||||
public interface MessageRepository extends MongoRepository<Message, ObjectId> {
|
public interface MessageRepository extends MongoRepository<Message, ObjectId> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages for a specific receiver, ordered by creation time descending (newest first)
|
* Find all messages for a specific receiver (AppUser ID), ordered by creation time ascending (oldest first)
|
||||||
|
*/
|
||||||
|
List<Message> findByReceiverOrderByCreatedAtAsc(String receiver);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages for a specific receiver (AppUser ID), ordered by creation time descending (newest first)
|
||||||
*/
|
*/
|
||||||
List<Message> findByReceiverOrderByCreatedAtDesc(String receiver);
|
List<Message> findByReceiverOrderByCreatedAtDesc(String receiver);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages sent by a specific sender, ordered by creation time descending
|
* Find all unread messages for a specific receiver (AppUser ID)
|
||||||
*/
|
|
||||||
List<Message> findBySenderOrderByCreatedAtDesc(String sender);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all unread messages for a specific receiver
|
|
||||||
*/
|
*/
|
||||||
List<Message> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(String receiver);
|
List<Message> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(String receiver);
|
||||||
|
|
||||||
@@ -33,27 +33,22 @@ public interface MessageRepository extends MongoRepository<Message, ObjectId> {
|
|||||||
List<Message> findByJobIdOrderByCreatedAtDesc(ObjectId jobId);
|
List<Message> findByJobIdOrderByCreatedAtDesc(ObjectId jobId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages of a specific type for a receiver
|
* Find all messages of a specific type for a receiver (AppUser ID)
|
||||||
*/
|
*/
|
||||||
List<Message> findByReceiverAndMessageTypeOrderByCreatedAtDesc(String receiver, MessageType messageType);
|
List<Message> findByReceiverAndMessageTypeOrderByCreatedAtDesc(String receiver, MessageType messageType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages by origin (incoming/outgoing/server)
|
* Find all messages by origin (CLIENT/SERVER)
|
||||||
*/
|
*/
|
||||||
List<Message> findByOriginOrderByCreatedAtDesc(MessageOrigin origin);
|
List<Message> findByOriginOrderByCreatedAtDesc(MessageOrigin origin);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages between two users (in both directions)
|
* Find all unread messages count for a specific receiver (AppUser ID)
|
||||||
*/
|
|
||||||
List<Message> findBySenderAndReceiverOrderByCreatedAtAsc(String sender, String receiver);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all unread messages count for a specific receiver
|
|
||||||
*/
|
*/
|
||||||
long countByReceiverAndIsReadFalse(String receiver);
|
long countByReceiverAndIsReadFalse(String receiver);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all messages for a specific receiver and job
|
* Find all messages for a specific receiver (AppUser ID) and job
|
||||||
*/
|
*/
|
||||||
List<Message> findByReceiverAndJobIdOrderByCreatedAtDesc(String receiver, ObjectId jobId);
|
List<Message> findByReceiverAndJobIdOrderByCreatedAtDesc(String receiver, ObjectId jobId);
|
||||||
|
|
||||||
@@ -61,16 +56,4 @@ public interface MessageRepository extends MongoRepository<Message, ObjectId> {
|
|||||||
* Find all messages (for admin/overview), ordered by creation time descending
|
* Find all messages (for admin/overview), ordered by creation time descending
|
||||||
*/
|
*/
|
||||||
List<Message> findAllByOrderByCreatedAtDesc();
|
List<Message> findAllByOrderByCreatedAtDesc();
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all messages where the sender or receiver matches the provided value,
|
|
||||||
* ordered by creation time ascending.
|
|
||||||
*/
|
|
||||||
List<Message> findBySenderOrReceiverOrderByCreatedAtAsc(String sender, String receiver);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all messages where the sender or receiver matches the provided value,
|
|
||||||
* ordered by creation time descending.
|
|
||||||
*/
|
|
||||||
List<Message> findBySenderOrReceiverOrderByCreatedAtDesc(String sender, String receiver);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ public class MessageBroadcaster {
|
|||||||
@EventListener
|
@EventListener
|
||||||
public void onMessageReceived(MessageReceivedEvent event) {
|
public void onMessageReceived(MessageReceivedEvent event) {
|
||||||
Message message = event.getMessage();
|
Message message = event.getMessage();
|
||||||
log.info("MessageBroadcaster received event for message from: {}", message.getSender());
|
log.info("MessageBroadcaster received event for message with origin {} for receiver {}",
|
||||||
|
message.getOrigin(), message.getReceiver());
|
||||||
broadcast(message);
|
broadcast(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,20 +41,22 @@ public class MessageService {
|
|||||||
* Save a message to the database
|
* Save a message to the database
|
||||||
*/
|
*/
|
||||||
public Message saveMessage(Message message) {
|
public Message saveMessage(Message message) {
|
||||||
log.info("Saving message from {} to {}", message.getSender(), message.getReceiver());
|
log.info("Saving message with origin {} for receiver {}", message.getOrigin(), message.getReceiver());
|
||||||
return messageRepository.save(message);
|
return messageRepository.save(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a general message to a client via MQTT
|
* Send a general message to a client via MQTT
|
||||||
|
* @param content Message content
|
||||||
|
* @param receiver AppUser ID (clientId)
|
||||||
*/
|
*/
|
||||||
public Message sendGeneralMessageToClient(String content, String sender, String receiver) {
|
public Message sendGeneralMessageToClient(String content, String receiver) {
|
||||||
return sendGeneralMessageToClient(content, sender, receiver, MessageContentType.TEXT);
|
return sendGeneralMessageToClient(content, receiver, MessageContentType.TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message sendGeneralMessageToClient(String content, String sender, String receiver,
|
public Message sendGeneralMessageToClient(String content, String receiver,
|
||||||
MessageContentType contentType) {
|
MessageContentType contentType) {
|
||||||
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType);
|
Message message = new Message(content, receiver, MessageOrigin.SERVER, contentType);
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
publishMessageToMqtt(message, receiver);
|
publishMessageToMqtt(message, receiver);
|
||||||
return message;
|
return message;
|
||||||
@@ -62,16 +64,20 @@ public class MessageService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a job-related message to a client via MQTT
|
* Send a job-related message to a client via MQTT
|
||||||
|
* @param content Message content
|
||||||
|
* @param receiver AppUser ID (clientId)
|
||||||
|
* @param jobId Job ObjectId
|
||||||
|
* @param jobNumber Job number
|
||||||
*/
|
*/
|
||||||
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
public Message sendJobMessageToClient(String content, String receiver,
|
||||||
ObjectId jobId, String jobNumber) {
|
ObjectId jobId, String jobNumber) {
|
||||||
return sendJobMessageToClient(content, sender, receiver, MessageContentType.TEXT, jobId, jobNumber);
|
return sendJobMessageToClient(content, receiver, MessageContentType.TEXT, jobId, jobNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
public Message sendJobMessageToClient(String content, String receiver,
|
||||||
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
MessageContentType contentType, ObjectId jobId, String jobNumber) {
|
||||||
JobContext context = resolveJobContext(jobId, jobNumber);
|
JobContext context = resolveJobContext(jobId, jobNumber);
|
||||||
Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType,
|
Message message = new Message(content, receiver, MessageOrigin.SERVER, contentType,
|
||||||
context.jobId(), context.jobNumber());
|
context.jobId(), context.jobNumber());
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
publishMessageToMqtt(message, receiver);
|
publishMessageToMqtt(message, receiver);
|
||||||
@@ -80,24 +86,28 @@ public class MessageService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming message from a client
|
* Handle incoming message from a client
|
||||||
|
* @param payload Inbound message payload where receiver = AppUser ID (clientId)
|
||||||
*/
|
*/
|
||||||
public Message receiveMessageFromClient(ChatMessageInboundPayload payload) {
|
public Message receiveMessageFromClient(ChatMessageInboundPayload payload) {
|
||||||
Message message;
|
Message message;
|
||||||
MessageContentType contentType = payload.contentType();
|
MessageContentType contentType = payload.contentType();
|
||||||
if (payload.hasJobContext()) {
|
if (payload.hasJobContext()) {
|
||||||
JobContext context = resolveJobContext(payload.jobId(), payload.jobNumber());
|
JobContext context = resolveJobContext(payload.jobId(), payload.jobNumber());
|
||||||
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
// receiver = AppUser ID (clientId)
|
||||||
|
message = new Message(payload.content(), payload.receiver(),
|
||||||
MessageOrigin.CLIENT, contentType, context.jobId(), context.jobNumber());
|
MessageOrigin.CLIENT, contentType, context.jobId(), context.jobNumber());
|
||||||
} else {
|
} else {
|
||||||
message = new Message(payload.content(), payload.sender(), payload.receiver(),
|
// receiver = AppUser ID (clientId)
|
||||||
|
message = new Message(payload.content(), payload.receiver(),
|
||||||
MessageOrigin.CLIENT, contentType);
|
MessageOrigin.CLIENT, contentType);
|
||||||
}
|
}
|
||||||
message = saveMessage(message);
|
message = saveMessage(message);
|
||||||
|
|
||||||
// Publish event to notify UI components about the new message
|
// Publish event to notify UI components about the new message
|
||||||
log.info("Publishing MessageReceivedEvent for message from {}", message.getSender());
|
log.info("Publishing MessageReceivedEvent for message with origin {} for receiver {}",
|
||||||
|
message.getOrigin(), message.getReceiver());
|
||||||
eventPublisher.publishEvent(new MessageReceivedEvent(this, message));
|
eventPublisher.publishEvent(new MessageReceivedEvent(this, message));
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,25 +154,25 @@ public class MessageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all messages a participant sent or received (oldest first) to reconstruct
|
* Get all messages for a specific AppUser (by receiver field), ordered by creation time ascending (oldest first)
|
||||||
* the conversation timeline.
|
* @param appUserId AppUser ID (clientId)
|
||||||
*/
|
*/
|
||||||
public List<Message> getMessagesForParticipantAscending(String participant) {
|
public List<Message> getMessagesForAppUserAscending(String appUserId) {
|
||||||
if (participant == null || participant.isBlank()) {
|
if (appUserId == null || appUserId.isBlank()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return messageRepository.findBySenderOrReceiverOrderByCreatedAtAsc(participant, participant);
|
return messageRepository.findByReceiverOrderByCreatedAtAsc(appUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all messages a participant sent or received (newest first) for
|
* Get all messages for a specific AppUser (by receiver field), ordered by creation time descending
|
||||||
* quick summaries.
|
* @param appUserId AppUser ID (clientId)
|
||||||
*/
|
*/
|
||||||
public List<Message> getMessagesForParticipantDescending(String participant) {
|
public List<Message> getMessagesForAppUserDescending(String appUserId) {
|
||||||
if (participant == null || participant.isBlank()) {
|
if (appUserId == null || appUserId.isBlank()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return messageRepository.findBySenderOrReceiverOrderByCreatedAtDesc(participant, participant);
|
return messageRepository.findByReceiverOrderByCreatedAtDesc(appUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user