Erweiterungen

This commit is contained in:
2025-10-14 19:28:20 +02:00
parent 4440825024
commit b391dbec8d
9 changed files with 354 additions and 62 deletions

View File

@@ -9,6 +9,7 @@ import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.task.BaseTask; import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.pages.service.UserService;
import de.assecutor.votianlt.repository.AppUserRepository; import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.CargoItemRepository; import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.JobRepository;
@@ -49,6 +50,9 @@ public class MessageController {
// Map to store userId -> clientId mapping for active sessions // Map to store userId -> clientId mapping for active sessions
private final Map<String, String> userClientIdMapping = new ConcurrentHashMap<>(); private final Map<String, String> userClientIdMapping = new ConcurrentHashMap<>();
// Map to store clientId -> userId mapping for active sessions (reverse lookup)
private final Map<String, String> clientIdUserMapping = new ConcurrentHashMap<>();
private final MqttPublisher mqttPublisher; private final MqttPublisher mqttPublisher;
private final AppUserRepository appUserRepository; private final AppUserRepository appUserRepository;
@@ -67,12 +71,13 @@ public class MessageController {
private final JobHistoryService jobHistoryService; private final JobHistoryService jobHistoryService;
private final EmailService emailService; private final EmailService emailService;
private final MessageService messageService; private final MessageService messageService;
private final UserService userService;
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository,
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService, SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService,
EmailService emailService, MessageService messageService) { EmailService emailService, MessageService messageService, UserService userService) {
this.mqttPublisher = mqttPublisher; this.mqttPublisher = mqttPublisher;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
@@ -86,6 +91,7 @@ public class MessageController {
this.jobHistoryService = jobHistoryService; this.jobHistoryService = jobHistoryService;
this.emailService = emailService; this.emailService = emailService;
this.messageService = messageService; this.messageService = messageService;
this.userService = userService;
} }
/** /**
@@ -585,7 +591,8 @@ public class MessageController {
*/ */
private void storeClientIdMapping(String userId, String clientId) { private void storeClientIdMapping(String userId, String clientId) {
userClientIdMapping.put(userId, clientId); userClientIdMapping.put(userId, clientId);
log.debug("Stored clientId mapping: userId={} -> clientId={}", userId, clientId); clientIdUserMapping.put(clientId, userId);
log.debug("Stored clientId mapping: userId={} <-> clientId={}", userId, clientId);
} }
/** /**
@@ -595,6 +602,13 @@ public class MessageController {
return userClientIdMapping.get(userId); return userClientIdMapping.get(userId);
} }
/**
* Get the userId (AppUser ID) for a given clientId
*/
private String getUserIdForClientId(String clientId) {
return clientIdUserMapping.get(clientId);
}
/** /**
* 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:
@@ -604,21 +618,83 @@ public class MessageController {
* "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:
* 1. Extract clientId from topic (this is the AppUser ID)
* 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); ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
messageService.receiveMessageFromClient(inboundPayload);
log.info("Successfully saved incoming message from {} to {}", inboundPayload.sender(), // Extract clientId from payload (added by MqttV5ClientManager from topic)
inboundPayload.receiver()); // The clientId IS the AppUser ID
String clientId = payload.get("clientId") != null ? payload.get("clientId").toString() : null;
if (clientId == null || clientId.isBlank()) {
log.warn("No clientId found in message payload, cannot process message");
return;
}
// Convert clientId (AppUser ID) to ObjectId
ObjectId appUserObjectId;
try {
appUserObjectId = new ObjectId(clientId);
} catch (IllegalArgumentException e) {
log.warn("Invalid clientId/AppUser ID '{}': {}", clientId, e.getMessage());
return;
}
// Find AppUser by ID
AppUser appUser = appUserService.findById(appUserObjectId);
if (appUser == null) {
log.warn("AppUser not found for clientId '{}'", clientId);
return;
}
// Get owner (User) of AppUser from the owner field
ObjectId ownerId = appUser.getOwner();
if (ownerId == null) {
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) {
log.error("Error handling incoming message: {}", e.getMessage(), e); log.error("Error handling incoming message: {}", e.getMessage(), e);
} }
} }
} }

View File

@@ -0,0 +1,15 @@
package de.assecutor.votianlt.event;
import org.springframework.context.ApplicationEvent;
/**
* Event published when message read status changes (e.g., messages marked as read)
* This allows UI components like the sidebar badge to update accordingly
*/
public class MessageReadStatusChangedEvent extends ApplicationEvent {
public MessageReadStatusChangedEvent(Object source) {
super(source);
}
}

View File

@@ -165,7 +165,14 @@ public class MqttV5ClientManager implements SmartLifecycle {
messageController.handleAppLogin(req); messageController.handleAppLogin(req);
} else if (topic.matches("/server/.+/message")) { } else if (topic.matches("/server/.+/message")) {
try { try {
// Handle incoming message from client // Extract clientId from topic: /server/{clientId}/message
String[] parts = topic.split("/");
String clientId = parts.length > 2 ? parts[2] : null;
if (clientId != null && !clientId.isBlank()) {
payload.put("clientId", clientId);
} else {
log.warn("Couldn't extract clientId from topic {} for message", topic);
}
messageController.handleIncomingMessage(payload); messageController.handleIncomingMessage(payload);
} catch (Exception e) { } catch (Exception e) {
log.error("Error handling incoming message on {}: {}", topic, e.getMessage(), e); log.error("Error handling incoming message on {}: {}", topic, e.getMessage(), e);

View File

@@ -1,6 +1,8 @@
package de.assecutor.votianlt.pages.base.ui.view; package de.assecutor.votianlt.pages.base.ui.view;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI; import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.avatar.Avatar;
@@ -20,12 +22,15 @@ import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.server.menu.MenuConfiguration; import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry; import com.vaadin.flow.server.menu.MenuEntry;
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.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;
import de.assecutor.votianlt.service.MessageBadgeUpdateService;
import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import static com.vaadin.flow.theme.lumo.LumoUtility.*; import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@@ -34,21 +39,27 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
@AnonymousAllowed @AnonymousAllowed
@Slf4j
@Layout("main") @Layout("main")
public final class MainLayout extends AppLayout { public final class MainLayout extends AppLayout {
private final SecurityService securityService; private final SecurityService securityService;
private final UserInvoiceDataService userInvoiceDataService; private final UserInvoiceDataService userInvoiceDataService;
private final MessageService messageService; private final MessageService messageService;
private final MessageBadgeUpdateService messageBadgeUpdateService;
private Div headerRef; private Div headerRef;
private Scroller navRef; private Scroller navRef;
private Component userMenuRef; private Component userMenuRef;
private Span messagesBadge; // Reference to the messages badge for dynamic updates
private SideNavItem messagesNavItem; // Reference to the messages nav item
private Registration badgeUpdateRegistration; // Track badge update listener registration
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService, MessageService messageService) { public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService) {
this.securityService = securityService; this.securityService = securityService;
this.userInvoiceDataService = userInvoiceDataService; this.userInvoiceDataService = userInvoiceDataService;
this.messageService = messageService; this.messageService = messageService;
this.messageBadgeUpdateService = messageBadgeUpdateService;
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
@@ -162,24 +173,44 @@ public final class MainLayout extends AppLayout {
item = new SideNavItem(menuEntry.title(), menuEntry.path()); item = new SideNavItem(menuEntry.title(), menuEntry.path());
} }
if ("Nachrichten".equals(menuEntry.title())) { if ("Nachrichten".equals(menuEntry.title())) {
long unreadCount = resolveUnreadMessageCount(); messagesNavItem = item;
if (unreadCount > 0) { }
Span badge = new Span(String.valueOf(unreadCount)); return item;
badge.getElement().getThemeList().add("badge"); }
badge.getStyle().set("background-color", "var(--lumo-primary-color)");
badge.getStyle().set("color", "#ffffff");
badge.getStyle().set("border-radius", "12px");
badge.getStyle().set("padding", "2px 8px");
badge.getStyle().set("font-size", "12px");
badge.getStyle().set("font-weight", "bold");
badge.getStyle().set("min-width", "20px");
badge.getStyle().set("text-align", "center");
item.setSuffixComponent(badge); /**
} * Updates the messages badge with the current unread count
*/
private void updateMessagesBadge() {
if (messagesNavItem == null) {
return;
} }
return item; long unreadCount = resolveUnreadMessageCount();
if (unreadCount > 0) {
if (messagesBadge == null) {
messagesBadge = new Span(String.valueOf(unreadCount));
messagesBadge.getElement().getThemeList().add("badge");
messagesBadge.getStyle().set("background-color", "var(--lumo-primary-color)");
messagesBadge.getStyle().set("color", "#ffffff");
messagesBadge.getStyle().set("border-radius", "12px");
messagesBadge.getStyle().set("padding", "2px 8px");
messagesBadge.getStyle().set("font-size", "12px");
messagesBadge.getStyle().set("font-weight", "bold");
messagesBadge.getStyle().set("min-width", "20px");
messagesBadge.getStyle().set("text-align", "center");
messagesNavItem.setSuffixComponent(messagesBadge);
} else {
messagesBadge.setText(String.valueOf(unreadCount));
messagesBadge.setVisible(true);
}
} else {
if (messagesBadge != null) {
messagesNavItem.setSuffixComponent(null);
messagesBadge = null;
}
}
} }
private long resolveUnreadMessageCount() { private long resolveUnreadMessageCount() {
@@ -191,6 +222,7 @@ public final class MainLayout extends AppLayout {
try { try {
User currentUser = securityService.getCurrentDatabaseUser(); User currentUser = securityService.getCurrentDatabaseUser();
if (currentUser != null) { if (currentUser != null) {
String email = Optional.ofNullable(currentUser.getEmail()).map(String::trim).orElse(""); String email = Optional.ofNullable(currentUser.getEmail()).map(String::trim).orElse("");
if (!email.isBlank()) { if (!email.isBlank()) {
@@ -217,6 +249,7 @@ public final class MainLayout extends AppLayout {
for (String receiver : candidateReceivers) { for (String receiver : candidateReceivers) {
unread += messageService.getUnreadMessageCount(receiver); unread += messageService.getUnreadMessageCount(receiver);
} }
return unread; return unread;
} }
@@ -269,4 +302,28 @@ public final class MainLayout extends AppLayout {
return false; return false;
} }
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
UI ui = attachEvent.getUI();
// Update badge immediately when layout is attached
updateMessagesBadge();
// Register listener for badge updates
badgeUpdateRegistration = messageBadgeUpdateService.register(() -> {
ui.access(() -> {
updateMessagesBadge();
});
});
}
@Override
protected void onDetach(DetachEvent detachEvent) {
if (badgeUpdateRegistration != null) {
badgeUpdateRegistration.remove();
badgeUpdateRegistration = null;
}
super.onDetach(detachEvent);
}
} }

View File

@@ -59,6 +59,14 @@ public class AppUserService {
return appUserRepository.findById(id).orElse(null); return appUserRepository.findById(id).orElse(null);
} }
public AppUser findByEmail(String email) {
return appUserRepository.findByEmail(email);
}
public AppUser findByBezeichnung(String bezeichnung) {
return appUserRepository.findByBezeichnung(bezeichnung);
}
public AppUser updateAppUser(AppUser appUser) { public AppUser updateAppUser(AppUser appUser) {
// Hash the password if it's being updated and not empty // Hash the password if it's being updated and not empty
if (appUser.getPassword() != null && !appUser.getPassword().isEmpty()) { if (appUser.getPassword() != null && !appUser.getPassword().isEmpty()) {

View File

@@ -2,6 +2,7 @@ package de.assecutor.votianlt.pages.service;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.UserRepository; import de.assecutor.votianlt.repository.UserRepository;
import org.bson.types.ObjectId;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,6 +23,10 @@ public class UserService {
return userRepository.findByEmail(email).orElse(null); return userRepository.findByEmail(email).orElse(null);
} }
public User findById(ObjectId id) {
return userRepository.findById(id).orElse(null);
}
public User save(User user) { public User save(User user) {
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")) { if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")) {
// Passwort verschlüsseln, falls noch nicht verschlüsselt // Passwort verschlüsseln, falls noch nicht verschlüsselt

View File

@@ -38,6 +38,8 @@ import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.service.MessageBroadcaster; import de.assecutor.votianlt.service.MessageBroadcaster;
import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.event.MessageReadStatusChangedEvent;
import org.springframework.context.ApplicationEventPublisher;
import jakarta.annotation.security.RolesAllowed; 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;
@@ -75,6 +77,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
private final MessageService messageService; private final MessageService messageService;
private final SecurityService securityService; private final SecurityService securityService;
private final MessageBroadcaster messageBroadcaster; private final MessageBroadcaster messageBroadcaster;
private final ApplicationEventPublisher eventPublisher;
private String participantKey; private String participantKey;
private String conversationId; private String conversationId;
@@ -95,11 +98,13 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
private static final float JPEG_COMPRESSION_QUALITY = 0.8f; private static final float JPEG_COMPRESSION_QUALITY = 0.8f;
public MessageDetailsView(AppUserService appUserService, MessageService messageService, public MessageDetailsView(AppUserService appUserService, MessageService messageService,
SecurityService securityService, MessageBroadcaster messageBroadcaster) { SecurityService securityService, MessageBroadcaster messageBroadcaster,
ApplicationEventPublisher eventPublisher) {
this.appUserService = appUserService; this.appUserService = appUserService;
this.messageService = messageService; this.messageService = messageService;
this.securityService = securityService; this.securityService = securityService;
this.messageBroadcaster = messageBroadcaster; this.messageBroadcaster = messageBroadcaster;
this.eventPublisher = eventPublisher;
// Set height to 100% to prevent page from growing beyond viewport // Set height to 100% to prevent page from growing beyond viewport
setHeightFull(); setHeightFull();
@@ -226,6 +231,38 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
// Add message bubble // Add message bubble
messagesContainer.add(createMessageBubble(message, timestamp)); messagesContainer.add(createMessageBubble(message, timestamp));
} }
// After rendering, mark any unread messages directed to the current user as read
markVisibleMessagesAsRead();
}
/**
* Marks all currently visible messages that are addressed to the logged-in user as read.
* This is triggered after (re)rendering the conversation and will also update the in-memory
* message objects to keep UI state consistent.
*/
private void markVisibleMessagesAsRead() {
try {
if (currentMessages == null || currentMessages.isEmpty()) {
return;
}
boolean anyMarked = false;
for (Message msg : currentMessages) {
if (!msg.isRead() && msg.getId() != null) {
// Update persistence
messageService.markAsRead(msg.getId());
// Update in-memory object so UI reflects read state immediately
msg.markAsRead();
anyMarked = true;
}
}
// Publish event to update badge in sidebar
if (anyMarked) {
eventPublisher.publishEvent(new MessageReadStatusChangedEvent(this));
}
} catch (Exception e) {
log.warn("Failed to mark messages as read: {}", e.getMessage());
}
} }
private void openImageUploadDialog() { private void openImageUploadDialog() {
@@ -778,6 +815,16 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
contentType); contentType);
} }
// Mark own outgoing message as read immediately
if (saved != null && saved.getId() != null) {
try {
messageService.markAsRead(saved.getId());
saved.markAsRead(); // keep UI state consistent
} catch (Exception ignore) {
// non-fatal
}
}
Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END) Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);

View File

@@ -21,6 +21,6 @@ public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
// Find AppUser by appCode for task completion notifications // Find AppUser by appCode for task completion notifications
java.util.Optional<AppUser> findByAppCode(String appCode); java.util.Optional<AppUser> findByAppCode(String appCode);
// Custom query methods can be added here if needed // Find AppUser by bezeichnung
// List<AppUser> findByBezeichnung(String bezeichnung); AppUser findByBezeichnung(String bezeichnung);
} }

View File

@@ -0,0 +1,77 @@
package de.assecutor.votianlt.service;
import com.vaadin.flow.shared.Registration;
import de.assecutor.votianlt.event.MessageReadStatusChangedEvent;
import de.assecutor.votianlt.event.MessageReceivedEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Service that listens for message-related events and notifies registered UI components
* to update their message badges (e.g., in the sidebar navigation)
*/
@Service
@Slf4j
public class MessageBadgeUpdateService {
private final Executor executor = Executors.newSingleThreadExecutor();
private final LinkedHashSet<Runnable> listeners = new LinkedHashSet<>();
/**
* Register a listener that will be called when message badge should be updated
*
* @param listener Runnable that will be called when badge update is needed
* @return Registration object that can be used to unregister the listener
*/
public synchronized Registration register(Runnable listener) {
listeners.add(listener);
log.debug("Registered badge update listener. Total listeners: {}", listeners.size());
return () -> {
synchronized (MessageBadgeUpdateService.this) {
listeners.remove(listener);
log.debug("Unregistered badge update listener. Total listeners: {}", listeners.size());
}
};
}
/**
* Notify all registered listeners that badge should be updated
*/
private synchronized void notifyListeners() {
log.debug("Notifying {} badge update listeners", listeners.size());
for (Runnable listener : listeners) {
executor.execute(() -> {
try {
listener.run();
} catch (Exception e) {
log.error("Error notifying badge update listener", e);
}
});
}
}
/**
* Spring event listener for message read status changes
*/
@EventListener
public void onMessageReadStatusChanged(MessageReadStatusChangedEvent event) {
log.debug("MessageBadgeUpdateService received MessageReadStatusChangedEvent");
notifyListeners();
}
/**
* Spring event listener for new messages received
*/
@EventListener
public void onMessageReceived(MessageReceivedEvent event) {
log.debug("MessageBadgeUpdateService received MessageReceivedEvent");
notifyListeners();
}
}