Merge branch 'feat/84-whisper-chat' into 'main'

Add whisper chat option

Closes #84

See merge request cs108-fs26/Gruppe-13!123
This commit was merged in pull request #279.
This commit is contained in:
Mathis Ginkel
2026-04-12 16:47:09 +00:00
5 changed files with 191 additions and 43 deletions
@@ -3,11 +3,14 @@ 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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable;
/**
@@ -38,10 +41,15 @@ public class ChatController {
return chatModelMap;
}
private Map<ChatKey, ChatModel> chatModelMap;
private final Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000;
/** List of all users connected to the server, to safe them locally on the client */
private final List<String> localUserList;
private final Logger logger;
/**
* Constructor, adds TimerTask to be sent to the server regularly
*
@@ -52,13 +60,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,
@@ -66,7 +77,7 @@ public class ChatController {
}
/**
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
* Method to be activated, if a lobby has been chosen. It will update the UI and add a new
* ChatModel to hold the Data for the Lobby Chat
*
* @param lobbyId
@@ -78,7 +89,15 @@ public class ChatController {
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
}
/** method to get all messages from the server */
/**
* Method to process the Messages that got polled by the {@link ChatClient}.
*
* <p>Case distinction for every message: - Messages identified with ChatType.LOBBY -> check if
* they belong to the users lobby. - Messages identified with ChatType.GLOBAL -> check if the
* target user of that Message is the user, or the message got sent by the user.
*
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
*/
public void receiveMessage() {
List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) {
@@ -102,14 +121,24 @@ public class ChatController {
}
break;
case ChatType.WHISPER:
if (msg.target.equals(username)) {
if (chatModelMap.containsKey(
new ChatKey(ChatType.WHISPER, msg.sender))) {
chatModelMap
.get(new ChatKey(ChatType.WHISPER, msg.sender))
.addMessage(msg);
if (msg.target.equals(username) || msg.sender.equals(username)) {
ChatKey key;
if (msg.target.equals(username)) {
key = new ChatKey(ChatType.WHISPER, msg.sender);
} else {
chatBoxController.addWhisperChat(msg.sender);
key = new ChatKey(ChatType.WHISPER, msg.target);
}
if (chatModelMap.containsKey(key)) {
chatModelMap.get(key).addMessage(msg);
} else {
ChatModel chatModel =
new ChatModel(
ChatType.WHISPER,
username,
lobbyId,
key.targetUser());
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
chatModel.addMessage(msg);
}
}
break;
@@ -118,6 +147,30 @@ public class ChatController {
}
}
/**
* Method to process the usernames of other users connected to the server, after they got polled
* by the {@link ChatClient}.
*
* <p>Checks for every one of the usernames, if it is already in the list localUserList and if
* not adds them. For every new User added in the List, they get added as an option to start a
* Whisper Chat with.
*/
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
*
@@ -69,4 +69,33 @@ public class ChatClient {
}
return messages;
}
/**
* Method to poll the usernames of all users currently connected to the server, by sending the
* "LIST_USERS" command.
*
* @return List of all usernames retrieved from the server.
*/
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,7 +5,13 @@ 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 javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -13,6 +19,7 @@ import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -28,15 +35,21 @@ public class ChatBoxController {
@FXML private MenuButton addWhisperChatButton;
@FXML private HBox menuBox;
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";
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
this.chatController = chatController;
activeWhisperChats = new ArrayList<>();
usernameTabMap = new HashMap<>();
}
/**
@@ -51,7 +64,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 +74,20 @@ 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 +96,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 +120,56 @@ 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);
}
});
}
/**
* Helper Function to cope with a Threads problem occurring when the main JavaFX Thread performs
* the task to add a new whisper Chat tab, and the task is specified to runLater.
*/
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);
}
}
}
}