From 4687566816fe6522cb5e69eddac057a6c3ab0120 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Wed, 15 Apr 2026 21:06:05 +0200 Subject: [PATCH] Feat: Added client-side USERNAME_CHANGED event handling with whisper migration --- .../casono/client/chat/ChatController.java | 144 ++++++++++++++++-- .../cs108/casono/client/chat/ChatModel.java | 6 +- .../client/ui/chatui/ChatBoxController.java | 59 +++++++ 3 files changed, 196 insertions(+), 13 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 d3ac3c0..b07c712 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 @@ -1,15 +1,20 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat; +import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp; 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 ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.WeakHashMap; +import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jspecify.annotations.Nullable; @@ -24,7 +29,7 @@ public class ChatController { private static final Map ACTIVE_CONTROLLERS = new WeakHashMap<>(); - private final String username; + private volatile String username; private final ClientService clientService; @@ -37,6 +42,7 @@ public class ChatController { private final ChatBoxController chatBoxController; private int lobbyId = -1; private final Timer timer; + private final Consumer> serverEventListener; public record ChatKey(ChatType type, @Nullable String targetUser) { public ChatKey(ChatType type) { @@ -71,8 +77,10 @@ public class ChatController { localUserList = new ArrayList<>(); this.chatBoxController = new ChatBoxController(username, this); this.logger = LogManager.getLogger(ChatController.class); + this.serverEventListener = this::handleServerEvent; registerAsActiveController(clientService); + clientService.addEventListener(serverEventListener); this.timer = new Timer(true); timer.schedule( @@ -131,6 +139,7 @@ public class ChatController { *

All Messages get added to a particular {@link ChatModel}, if the checks passed. */ public void receiveMessage() { + String currentUsername = getCurrentUsername(); List newMessages = chatClient.getMessages(); if (!newMessages.isEmpty()) { for (Message msg : newMessages) { @@ -146,16 +155,17 @@ public class ChatController { (_key) -> new ChatModel( ChatType.LOBBY, - username, + currentUsername, msg.lobbyId, null)) .addMessage(msg); } break; case ChatType.WHISPER: - if (msg.target.equals(username) || msg.sender.equals(username)) { + if (msg.target.equals(currentUsername) + || msg.sender.equals(currentUsername)) { ChatKey key; - if (msg.target.equals(username)) { + if (msg.target.equals(currentUsername)) { key = new ChatKey(ChatType.WHISPER, msg.sender); } else { key = new ChatKey(ChatType.WHISPER, msg.target); @@ -166,7 +176,7 @@ public class ChatController { ChatModel chatModel = new ChatModel( ChatType.WHISPER, - username, + currentUsername, lobbyId, key.targetUser()); chatBoxController.addWhisperChat(key.targetUser(), chatModel); @@ -203,13 +213,29 @@ 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); - addWhisperUser(value); + Set remoteUsers = new HashSet<>(); + String currentUsername = getCurrentUsername(); + + for (String user : users) { + if (user == null || !user.contains("=")) { + continue; } + String value = user.split("\\=", 2)[1].trim(); + if (value.isBlank() || value.equals(currentUsername)) { + continue; + } + remoteUsers.add(value); + } + + for (String known : new ArrayList<>(localUserList)) { + if (!remoteUsers.contains(known)) { + localUserList.remove(known); + chatBoxController.removeWhisperUser(known); + } + } + + for (String remote : remoteUsers) { + addWhisperUser(remote); } } @@ -222,16 +248,110 @@ public class ChatController { if (user == null || user.isBlank()) { return; } - if (!(localUserList.contains(user) || user.equals(username))) { + if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) { localUserList.add(user); logger.info("adding new whisper user"); chatBoxController.addWhisperUser(user); } } + public synchronized void updateUsername(String newUsername) { + if (newUsername == null || newUsername.isBlank()) { + return; + } + String oldUsername = this.username; + this.username = newUsername.trim(); + chatBoxController.setUsername(this.username); + + if (oldUsername != null && !oldUsername.equals(this.username)) { + localUserList.remove(oldUsername); + chatBoxController.removeWhisperUser(oldUsername); + } + } + + private void handleServerEvent(List lines) { + if (lines == null || lines.isEmpty()) { + return; + } + + List params; + try { + params = ClientService.convertToRequestParameters(lines); + } catch (RuntimeException e) { + return; + } + String event = null; + String oldUsername = null; + String newUsername = null; + + for (RequestParameter p : params) { + if ("EVENT".equalsIgnoreCase(p.key())) { + event = p.value(); + } else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) { + oldUsername = p.value(); + } else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) { + newUsername = p.value(); + } + } + + if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) { + return; + } + + applyUsernameMigration(oldUsername, newUsername); + } + + private synchronized void applyUsernameMigration(String oldUsername, String newUsername) { + if (oldUsername == null + || newUsername == null + || oldUsername.isBlank() + || newUsername.isBlank() + || oldUsername.equals(newUsername)) { + return; + } + + String currentUsername = getCurrentUsername(); + if (oldUsername.equals(currentUsername)) { + updateUsername(newUsername); + } + + ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername); + ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername); + + ChatModel oldModel = chatModelMap.remove(oldKey); + ChatModel existingNewModel = chatModelMap.get(newKey); + if (oldModel != null) { + oldModel.setTarget(newUsername); + if (existingNewModel == null) { + chatModelMap.put(newKey, oldModel); + } else { + // Merge possible parallel history into the already existing new-key model. + for (Message msg : oldModel.messages) { + existingNewModel.addMessage(msg); + } + } + chatBoxController.renameWhisperUser(oldUsername, newUsername); + } + + localUserList.remove(oldUsername); + chatBoxController.removeWhisperUser(oldUsername); + if (!newUsername.equals(getCurrentUsername())) { + addWhisperUser(newUsername); + } + } + + public String getCurrentUsername() { + String shared = ClientApp.getSharedUsername(); + if (shared != null && !shared.isBlank()) { + return shared.trim(); + } + return username; + } + /** Stops polling background tasks for this chat controller instance. */ public void shutdown() { timer.cancel(); + clientService.removeEventListener(serverEventListener); synchronized (ACTIVE_CONTROLLERS) { if (ACTIVE_CONTROLLERS.get(clientService) == this) { ACTIVE_CONTROLLERS.remove(clientService); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java index 69cdecb..6cbca3c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java @@ -22,7 +22,7 @@ public class ChatModel { public final String username; /** The person to send the message to If the chat is a whisper chat */ - private final String target; + private String target; private final IntegerProperty count; @@ -82,4 +82,8 @@ public class ChatModel { public String getTarget() { return target; } + + public void setTarget(String target) { + this.target = target; + } } 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 addd9fa..1864601 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 @@ -60,6 +60,12 @@ public class ChatBoxController { usernameTabMap = new HashMap<>(); } + public void setUsername(String username) { + if (username != null && !username.isBlank()) { + this.username = username.trim(); + } + } + /** * Initializes the chat interface by creating the global chat model and adding the corresponding * "GLOBAL" tab to the interface. It also registers the global chat in the {@link @@ -84,6 +90,11 @@ public class ChatBoxController { public void addWhisperUser(String targetUserName) { Platform.runLater( () -> { + for (MenuItem existing : addWhisperChatButton.getItems()) { + if (targetUserName.equals(existing.getText())) { + return; + } + } MenuItem menuItem = new MenuItem(targetUserName); addWhisperChatButton.getItems().add(menuItem); menuItem.setOnAction( @@ -98,6 +109,54 @@ public class ChatBoxController { }); } + public void removeWhisperUser(String targetUserName) { + Platform.runLater( + () -> + addWhisperChatButton + .getItems() + .removeIf(item -> targetUserName.equals(item.getText()))); + } + + public void renameWhisperUser(String oldUsername, String newUsername) { + if (oldUsername == null + || newUsername == null + || oldUsername.isBlank() + || newUsername.isBlank() + || oldUsername.equals(newUsername)) { + return; + } + + Platform.runLater( + () -> { + for (MenuItem item : addWhisperChatButton.getItems()) { + if (oldUsername.equals(item.getText())) { + item.setText(newUsername); + item.setOnAction( + event -> + addWhisperChat( + newUsername, + new ChatModel( + ChatType.WHISPER, + username, + -1, + newUsername))); + break; + } + } + + Tab tab = usernameTabMap.remove(oldUsername); + if (tab != null) { + tab.setText(newUsername); + usernameTabMap.put(newUsername, tab); + } + + int idx = activeWhisperChats.indexOf(oldUsername); + if (idx >= 0) { + activeWhisperChats.set(idx, newUsername); + } + }); + } + /** * Creates a new private (whisper) chat model for a specific target user, registers it within * the chat system, and opens a new chat tab.