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..3a69a45 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,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 chatModelMap; + private final Map 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 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}. + * + *

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. + * + *

All Messages get added to a particular {@link ChatModel}, if the checks passed. + */ public void receiveMessage() { List 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}. + * + *

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 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..6a88ed9 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 @@ -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 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..e0a62e6 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,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 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 +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 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..9c81ee7 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,10 @@ 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.List; import javafx.application.Application; import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; @@ -13,13 +15,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 +30,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();