Feat: Add whisper chat functionality

Introduce recording of users connected to server, to show usernames as options for whisper chat.
This commit is contained in:
Mathis Ginkel
2026-04-12 00:19:34 +02:00
parent 48e6b51ad1
commit b2adeb7a56
5 changed files with 159 additions and 50 deletions
@@ -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.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.util.LinkedHashMap;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Timer; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.TimerTask; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
/** /**
@@ -38,10 +39,14 @@ public class ChatController {
return chatModelMap; return chatModelMap;
} }
private Map<ChatKey, ChatModel> chatModelMap; private final Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000; private static final long REFRESH_TIME = 1000;
private List<String> localUserList;
private Logger logger;
/** /**
* Constructor, adds TimerTask to be sent to the server regularly * Constructor, adds TimerTask to be sent to the server regularly
* *
@@ -52,13 +57,16 @@ public class ChatController {
this.username = username; this.username = username;
chatClient = new ChatClient(clientService); chatClient = new ChatClient(clientService);
chatModelMap = new LinkedHashMap<>(); chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this); this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.timer = new Timer(); this.timer = new Timer();
timer.schedule( timer.schedule(
new TimerTask() { new TimerTask() {
@Override @Override
public void run() { public void run() {
receiveMessage(); receiveMessage();
checkWhisperUsers();
} }
}, },
0, 0,
@@ -102,14 +110,21 @@ public class ChatController {
} }
break; break;
case ChatType.WHISPER: case ChatType.WHISPER:
if (msg.target.equals(username) || msg.sender.equals(username)) {
ChatKey key;
if(msg.target.equals(username)) { if(msg.target.equals(username)) {
if (chatModelMap.containsKey( key = new ChatKey(ChatType.WHISPER, msg.sender);
new ChatKey(ChatType.WHISPER, msg.sender))) { } else {
key = new ChatKey(ChatType.WHISPER, msg.target);
}
if (chatModelMap.containsKey(key)) {
chatModelMap chatModelMap
.get(new ChatKey(ChatType.WHISPER, msg.sender)) .get(key)
.addMessage(msg); .addMessage(msg);
} else { } else {
chatBoxController.addWhisperChat(msg.sender); ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, lobbyId, key.targetUser());
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
chatModel.addMessage(msg);
} }
} }
break; break;
@@ -118,6 +133,22 @@ public class ChatController {
} }
} }
public synchronized void checkWhisperUsers() {
List<String> 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 * method to send a message to the server
* *
@@ -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.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -69,4 +70,27 @@ public class ChatClient {
} }
return messages; return messages;
} }
public List<String> getUsers() {
logger.info("Asking server for list of users");
List<String> users = clientService.processCommand("LIST_USERS");
List<String> parameters = new ArrayList<String>();
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;
}
} }
@@ -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 ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; 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.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.MenuButton; import javafx.scene.control.*;
import javafx.scene.control.MenuItem; import javafx.scene.layout.HBox;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@@ -28,15 +34,21 @@ public class ChatBoxController {
@FXML private MenuButton addWhisperChatButton; @FXML private MenuButton addWhisperChatButton;
@FXML private HBox menuBox;
private FXMLLoader fxmlLoader; private FXMLLoader fxmlLoader;
@FXML private List<MenuItem> whisperUsers; private final List<String> activeWhisperChats;
@FXML private final Map<String, Tab> usernameTabMap;
private String ressource = "/ui-structure/components/chatui/chattab.fxml"; private String ressource = "/ui-structure/components/chatui/chattab.fxml";
public ChatBoxController(String username, ChatController chatController) { public ChatBoxController(String username, ChatController chatController) {
this.username = username; this.username = username;
this.chatController = chatController; this.chatController = chatController;
activeWhisperChats = new ArrayList<>();
usernameTabMap = new HashMap<>();
} }
/** /**
@@ -51,7 +63,7 @@ public class ChatBoxController {
.getChatModelMap() .getChatModelMap()
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel); .put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
addChatTab("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. * @param targetUserName The username of the person to be added to the whisper list.
*/ */
public void addWhisperUser(String targetUserName) { public void addWhisperUser(String targetUserName) {
Platform.runLater(
() -> {
MenuItem menuItem = new MenuItem(targetUserName); MenuItem menuItem = new MenuItem(targetUserName);
whisperUsers.add(menuItem);
addWhisperChatButton.getItems().add(menuItem); addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(event -> addWhisperChat(targetUserName)); 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. * @param target The username of the recipient for the private messages.
*/ */
public void addWhisperChat(String target) { public void addWhisperChat(String target, ChatModel chatModel) {
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target); if (!activeWhisperChats.contains(target)) {
activeWhisperChats.add(target);
chatController chatController
.getChatModelMap() .getChatModelMap()
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel); .put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
addChatTab(target, chatModel); addChatTab(target, chatModel);
} else {
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
}
} }
/** /**
@@ -93,6 +111,7 @@ public class ChatBoxController {
public void addChatTab(String title, ChatModel chatModel) { public void addChatTab(String title, ChatModel chatModel) {
URL resource = getClass().getResource(ressource); URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource); FXMLLoader fxmlLoader = new FXMLLoader(resource);
runOnPlatformSynchronized(()-> {
try { try {
ChatViewController chatViewController = ChatViewController chatViewController =
new ChatViewController(this.chatController, chatModel, this.username); new ChatViewController(this.chatController, chatModel, this.username);
@@ -104,9 +123,38 @@ public class ChatBoxController {
vbox.getChildren().add(load); vbox.getChildren().add(load);
chatModel.addListener((msg) -> chatViewController.showMessage(msg)); chatModel.addListener((msg) -> chatViewController.showMessage(msg));
Tab newChat = new Tab(title, vbox); Tab newChat = new Tab(title, vbox);
usernameTabMap.put(title, newChat);
this.chatTabPane.getTabs().add(newChat); this.chatTabPane.getTabs().add(newChat);
this.chatTabPane.getSelectionModel().select(newChat);
return newChat;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
});
}
public static <T> T runOnPlatformSynchronized(Callable<T> code) {
if(Platform.isFxApplicationThread()) {
try {
return code.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
CompletableFuture<T> 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);
}
} }
} }
}
@@ -4,6 +4,6 @@ import javafx.application.Application;
public class Chat { public class Chat {
public static void main(String[] args) { public static void main(String[] args) {
Application.launch(ChatApplication.class); Application.launch(ChatApplication.class, args);
} }
} }
@@ -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.network.CoreClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application; import javafx.application.Application;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; 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_WIDTH = 1200;
private static final int SCENE_HEIGHT = 800; private static final int SCENE_HEIGHT = 800;
String ip = "localhost";
String username = "mathis";
final int port = 5000;
public ChatApplication() {} public ChatApplication() {}
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
List<String> params = getParameters().getRaw();
String ip = params.get(0);
int port = Integer.parseInt(params.get(1));
String username = params.get(2);
FXMLLoader fxmlLoader = FXMLLoader fxmlLoader =
new FXMLLoader( new FXMLLoader(
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml")); getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
@@ -27,10 +33,10 @@ public class ChatApplication extends Application {
CoreClient coreClient = new CoreClient(clientService); CoreClient coreClient = new CoreClient(clientService);
coreClient.login(username); coreClient.login(username);
ChatController chatController = new ChatController(username, clientService); ChatController chatController = new ChatController(username, clientService);
ChatBoxController chatBoxController = new ChatBoxController(username, chatController); ChatBoxController chatBoxController = chatController.getChatBoxController();
fxmlLoader.setController(chatBoxController); fxmlLoader.setController(chatBoxController);
Parent node = fxmlLoader.load();
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); Scene scene = new Scene(node, SCENE_WIDTH, SCENE_HEIGHT);
stage.setTitle("Chat"); stage.setTitle("Chat");
stage.setScene(scene); stage.setScene(scene);
stage.show(); stage.show();