This commit is contained in:
2025-10-21 10:02:48 +02:00
parent c15b054151
commit 98974dcc2a
12 changed files with 109 additions and 192 deletions

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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(),

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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;
} }

View File

@@ -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();
} }

View File

@@ -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)));

View File

@@ -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);
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
/** /**