Erweiterungen
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,26 +173,46 @@ 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));
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the messages badge with the current unread count
|
||||||
|
*/
|
||||||
|
private void updateMessagesBadge() {
|
||||||
|
if (messagesNavItem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
if (!securityService.isUserLoggedIn()) {
|
if (!securityService.isUserLoggedIn()) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user