From d3e203b575f024c45860581c76aa0d9fe18ab0e4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 14 Apr 2026 19:59:42 -0600 Subject: [PATCH] add desktop chat notifications --- .../petshopdesktop/PetShopApplication.java | 5 ++ .../api/ChatRealtimeClient.java | 19 +++++- .../controllers/ChatController.java | 3 + .../util/DesktopNotificationService.java | 63 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java diff --git a/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java index 21d7d9ef..18296405 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java +++ b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java @@ -5,6 +5,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import org.example.petshopdesktop.util.DesktopNotificationService; import java.io.IOException; import java.util.Objects; @@ -26,5 +27,9 @@ public class PetShopApplication extends Application { stage.setScene(scene); stage.show(); + + DesktopNotificationService.getInstance().init( + stage.getIcons().isEmpty() ? null : stage.getIcons().get(0) + ); } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java index 0462fe6b..d29d3aab 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -4,6 +4,7 @@ import org.example.petshopdesktop.api.dto.chat.ConversationResponse; import org.example.petshopdesktop.api.dto.chat.MessageRequest; import org.example.petshopdesktop.api.dto.chat.MessageResponse; import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.util.DesktopNotificationService; import java.net.URI; import java.net.http.HttpClient; @@ -72,6 +73,16 @@ public class ChatRealtimeClient implements WebSocket.Listener { updateNotificationState(); } + public void markConversationReplied(Long conversationId, Long senderId) { + synchronized (lock) { + ConversationResponse conv = globalConversations.get(conversationId); + if (conv != null) { + conv.setLastSenderId(senderId); + } + } + updateNotificationState(); + } + public boolean hasActionableChats() { synchronized (lock) { UserSession session = UserSession.getInstance(); @@ -333,7 +344,13 @@ public class ChatRealtimeClient implements WebSocket.Listener { if (messageListener != null) { messageListener.accept(message); } - + + Long currentUserId = UserSession.getInstance().getUserId(); + if (message.getSenderId() != null && !message.getSenderId().equals(currentUserId)) { + DesktopNotificationService.getInstance() + .notifyNewMessage(message.getSenderDisplayName(), message.getContent()); + } + // Also update globalConversation last sender if this is the active conversation synchronized (lock) { ConversationResponse conv = globalConversations.get(message.getConversationId()); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index cd9c9c29..6d1082fd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -196,6 +196,7 @@ public class ChatController { MessageResponse response = ChatApi.getInstance().sendMessage(convId, request); Platform.runLater(() -> { btnSend.setDisable(false); + realtimeClient.markConversationReplied(convId, UserSession.getInstance().getUserId()); appendMessageIfSelected(response); if (selectedAttachmentFile != null) { clearLocalAttachment(); @@ -380,6 +381,7 @@ public class ChatController { List response = ChatApi.getInstance().listConversations(); response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); Platform.runLater(() -> { + realtimeClient.initializeState(response); conversations.setAll(response); refreshSections(); restoreSelection(); @@ -471,6 +473,7 @@ public class ChatController { .findFirst() .ifPresent(conversation -> { conversation.setLastMessage(message.getContent()); + conversation.setLastSenderId(message.getSenderId()); conversation.setUpdatedAt(message.getTimestamp()); conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); refreshSections(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java new file mode 100644 index 00000000..4f9da43f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java @@ -0,0 +1,63 @@ +package org.example.petshopdesktop.util; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; + +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.image.BufferedImage; + +public class DesktopNotificationService { + private static final DesktopNotificationService INSTANCE = new DesktopNotificationService(); + + private TrayIcon trayIcon; + private boolean trayInitialized = false; + + private DesktopNotificationService() {} + + public static DesktopNotificationService getInstance() { + return INSTANCE; + } + + public void init(Image appIcon) { + if (!SystemTray.isSupported()) return; + try { + BufferedImage image; + if (appIcon != null) { + image = SwingFXUtils.fromFXImage(appIcon, null); + } else { + image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + } + trayIcon = new TrayIcon(image, "Leon's Pet Store"); + trayIcon.setImageAutoSize(true); + SystemTray.getSystemTray().add(trayIcon); + trayInitialized = true; + } catch (Exception ignored) {} + } + + public void notifyNewMessage(String senderName, String content) { + Thread t = new Thread(() -> { + Toolkit.getDefaultToolkit().beep(); + showNotification(senderName, content); + }); + t.setDaemon(true); + t.start(); + } + + private void showNotification(String senderName, String content) { + String title = senderName != null ? senderName : "New Message"; + String body = content != null + ? (content.length() > 100 ? content.substring(0, 100) + "..." : content) + : "Attachment"; + + if (trayInitialized && trayIcon != null) { + trayIcon.displayMessage(title, body, TrayIcon.MessageType.INFO); + return; + } + + try { + new ProcessBuilder("notify-send", title, body).start(); + } catch (Exception ignored) {} + } +}