From b2adeb7a5604439133b23c86d18ab4519c64d4b0 Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Sun, 12 Apr 2026 00:19:34 +0200 Subject: [PATCH] Feat: Add whisper chat functionality Introduce recording of users connected to server, to show usernames as options for whisper chat. --- .../casono/client/chat/ChatController.java | 53 +++++++-- .../casono/client/network/ChatClient.java | 28 ++++- .../client/ui/chatui/ChatBoxController.java | 108 +++++++++++++----- .../dbis/cs108/casono/client/chat/Chat.java | 2 +- .../casono/client/chat/ChatApplication.java | 18 ++- 5 files changed, 159 insertions(+), 50 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java index 161397a..5c8b50a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java @@ -3,11 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat; import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; + +import java.util.*; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.jspecify.annotations.Nullable; /** @@ -38,10 +39,14 @@ public class ChatController { return chatModelMap; } - private Map chatModelMap; + private final Map chatModelMap; private static final long REFRESH_TIME = 1000; + private List localUserList; + + private Logger logger; + /** * Constructor, adds TimerTask to be sent to the server regularly * @@ -52,13 +57,16 @@ public class ChatController { this.username = username; chatClient = new ChatClient(clientService); chatModelMap = new LinkedHashMap<>(); + localUserList = new ArrayList<>(); this.chatBoxController = new ChatBoxController(username, this); + this.logger = LogManager.getLogger(ChatController.class); this.timer = new Timer(); timer.schedule( new TimerTask() { @Override public void run() { receiveMessage(); + checkWhisperUsers(); } }, 0, @@ -102,14 +110,21 @@ public class ChatController { } break; case ChatType.WHISPER: - if (msg.target.equals(username)) { - if (chatModelMap.containsKey( - new ChatKey(ChatType.WHISPER, msg.sender))) { + if (msg.target.equals(username) || msg.sender.equals(username)) { + ChatKey key; + if(msg.target.equals(username)) { + key = new ChatKey(ChatType.WHISPER, msg.sender); + } else { + key = new ChatKey(ChatType.WHISPER, msg.target); + } + if (chatModelMap.containsKey(key)) { chatModelMap - .get(new ChatKey(ChatType.WHISPER, msg.sender)) + .get(key) .addMessage(msg); } else { - chatBoxController.addWhisperChat(msg.sender); + ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, lobbyId, key.targetUser()); + chatBoxController.addWhisperChat(key.targetUser(), chatModel); + chatModel.addMessage(msg); } } break; @@ -118,6 +133,22 @@ public class ChatController { } } + public synchronized void checkWhisperUsers() { + List users = chatClient.getUsers(); + logger.info(users); + if (!users.isEmpty()) { + for (String user : users) { + String value = user.split("\\=")[1]; + logger.info(value); + if (!(localUserList.contains(value) || value.equals(username))) { + localUserList.add(value); + logger.info("adding new whisper user"); + chatBoxController.addWhisperUser(value); + } + } + } + } + /** * method to send a message to the server * diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java index f47ea87..e6fe86e 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java @@ -2,8 +2,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; -import java.util.ArrayList; -import java.util.List; + +import java.util.*; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -69,4 +70,27 @@ public class ChatClient { } return messages; } + + public List getUsers() { + logger.info("Asking server for list of users"); + List users = clientService.processCommand("LIST_USERS"); + List parameters = new ArrayList(); + for (int i = 0; i < users.size(); i++) { + String line = users.get(i); + if (line.equals("USERS")) { + continue; + } else if (line.equals("END")) { + break; + } + line = line.replaceFirst("^\t", ""); + + if (line.equals("USER")) { + String username = users.get(i + 1); + username = username.replaceFirst("^\t", ""); + username = username.replaceFirst("^\t", ""); + parameters.add(username); + } + } + return parameters; + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 618d309..3e5c518 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -5,14 +5,20 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -28,15 +34,21 @@ public class ChatBoxController { @FXML private MenuButton addWhisperChatButton; + @FXML private HBox menuBox; + private FXMLLoader fxmlLoader; - @FXML private List whisperUsers; + private final List activeWhisperChats; + + @FXML private final Map usernameTabMap; private String ressource = "/ui-structure/components/chatui/chattab.fxml"; public ChatBoxController(String username, ChatController chatController) { this.username = username; this.chatController = chatController; + activeWhisperChats = new ArrayList<>(); + usernameTabMap = new HashMap<>(); } /** @@ -51,7 +63,7 @@ public class ChatBoxController { .getChatModelMap() .put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel); addChatTab("GLOBAL", globalChatModel); - // TODO: Button to add new Whisper Chat + addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show()); } /** @@ -61,10 +73,12 @@ public class ChatBoxController { * @param targetUserName The username of the person to be added to the whisper list. */ public void addWhisperUser(String targetUserName) { - MenuItem menuItem = new MenuItem(targetUserName); - whisperUsers.add(menuItem); - addWhisperChatButton.getItems().add(menuItem); - menuItem.setOnAction(event -> addWhisperChat(targetUserName)); + Platform.runLater( + () -> { + MenuItem menuItem = new MenuItem(targetUserName); + addWhisperChatButton.getItems().add(menuItem); + menuItem.setOnAction(event -> addWhisperChat(targetUserName, new ChatModel(ChatType.WHISPER, username, -1, targetUserName))); + }); } /** @@ -73,12 +87,16 @@ public class ChatBoxController { * * @param target The username of the recipient for the private messages. */ - public void addWhisperChat(String target) { - ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target); - chatController - .getChatModelMap() - .put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel); - addChatTab(target, chatModel); + public void addWhisperChat(String target, ChatModel chatModel) { + if (!activeWhisperChats.contains(target)) { + activeWhisperChats.add(target); + chatController + .getChatModelMap() + .put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel); + addChatTab(target, chatModel); + } else { + chatTabPane.getSelectionModel().select(usernameTabMap.get(target)); + } } /** @@ -93,20 +111,50 @@ public class ChatBoxController { public void addChatTab(String title, ChatModel chatModel) { URL resource = getClass().getResource(ressource); FXMLLoader fxmlLoader = new FXMLLoader(resource); - try { - ChatViewController chatViewController = - new ChatViewController(this.chatController, chatModel, this.username); - fxmlLoader.setController(chatViewController); - Node load = fxmlLoader.load(); - VBox.setVgrow(load, Priority.ALWAYS); - VBox vbox = new VBox(); - VBox.setVgrow(vbox, Priority.ALWAYS); - vbox.getChildren().add(load); - chatModel.addListener((msg) -> chatViewController.showMessage(msg)); - Tab newChat = new Tab(title, vbox); - this.chatTabPane.getTabs().add(newChat); - } catch (IOException e) { - throw new RuntimeException(e); + runOnPlatformSynchronized(()-> { + try { + ChatViewController chatViewController = + new ChatViewController(this.chatController, chatModel, this.username); + fxmlLoader.setController(chatViewController); + Node load = fxmlLoader.load(); + VBox.setVgrow(load, Priority.ALWAYS); + VBox vbox = new VBox(); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.getChildren().add(load); + chatModel.addListener((msg) -> chatViewController.showMessage(msg)); + Tab newChat = new Tab(title, vbox); + usernameTabMap.put(title, newChat); + this.chatTabPane.getTabs().add(newChat); + this.chatTabPane.getSelectionModel().select(newChat); + return newChat; + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public static T runOnPlatformSynchronized(Callable code) { + if(Platform.isFxApplicationThread()) { + try { + return code.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + CompletableFuture futureLock = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + futureLock.complete(code.call()); + } catch (Exception e) { + futureLock.completeExceptionally(e); + } + }); + try { + return futureLock.get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } } } + } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java index 6088f22..5f533fd 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java @@ -4,6 +4,6 @@ import javafx.application.Application; public class Chat { public static void main(String[] args) { - Application.launch(ChatApplication.class); + Application.launch(ChatApplication.class, args); } } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java index 0eff249..722b859 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java @@ -4,8 +4,13 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import javafx.application.Application; import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; @@ -13,13 +18,14 @@ public class ChatApplication extends Application { private static final int SCENE_WIDTH = 1200; private static final int SCENE_HEIGHT = 800; - String ip = "localhost"; - String username = "mathis"; - final int port = 5000; public ChatApplication() {} public void start(Stage stage) throws IOException { + List params = getParameters().getRaw(); + String ip = params.get(0); + int port = Integer.parseInt(params.get(1)); + String username = params.get(2); FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource("/ui-structure/components/chatui/chatbox.fxml")); @@ -27,10 +33,10 @@ public class ChatApplication extends Application { CoreClient coreClient = new CoreClient(clientService); coreClient.login(username); ChatController chatController = new ChatController(username, clientService); - ChatBoxController chatBoxController = new ChatBoxController(username, chatController); + ChatBoxController chatBoxController = chatController.getChatBoxController(); fxmlLoader.setController(chatBoxController); - - Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); + Parent node = fxmlLoader.load(); + Scene scene = new Scene(node, SCENE_WIDTH, SCENE_HEIGHT); stage.setTitle("Chat"); stage.setScene(scene); stage.show();