From edc61ccb96ea7416953cbb7803323e382e423d63 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sat, 30 Aug 2025 19:43:58 +0330 Subject: [PATCH 1/3] Work on send message Real time in UI --- .../Client/ActionHandler.java | 15 +- .../Client/IncomingMessageListener.java | 150 +++++++++++--- .../Client/TelegramClient.java | 49 ++++- .../Models/ChatEntry.java | 3 + .../Server/ClientHandler.java | 7 +- .../UI/ChatPageController.java | 184 +++++++++++++++++- .../UI/MainController.java | 133 +++++++++++-- 7 files changed, 481 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 5ce10d6..b2b16ed 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -42,10 +42,21 @@ public class ActionHandler { +// private void handleRealTime(JSONObject json) throws IOException { +// IncomingMessageListener listener = new IncomingMessageListener(this.in); +// listener.handleRealTimeEvent (json); +// } + + // ActionHandler.java private void handleRealTime(JSONObject json) throws IOException { - IncomingMessageListener listener = new IncomingMessageListener(this.in); - listener.handleRealTimeEvent (json); + IncomingMessageListener listener = TelegramClient.getInstance().getListener(); + if (listener != null) { + listener.handleRealTimeEvent(json); + } else { + System.err.println("[RT] Listener not ready; dropping RT event: " + json); + } } + public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { this.out = out; this.in = in; diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index e6882a2..cd8c6e0 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -1,21 +1,36 @@ package org.to.telegramfinalproject.Client; +import javafx.application.Platform; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.UI.MainController; import java.io.BufferedReader; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; public class IncomingMessageListener implements Runnable { private final BufferedReader in; + public enum UIMode { CONSOLE, UI } + private final UIMode uiMode; // runtime mode - public IncomingMessageListener(BufferedReader in) { + private final java.util.Set seenMessageIds = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + + + public IncomingMessageListener(BufferedReader in, UIMode uiMode) { this.in = in; + this.uiMode = uiMode; } +// public IncomingMessageListener(BufferedReader in) { +// this.in = in; +// } + @Override public void run() { try { @@ -79,7 +94,7 @@ public class IncomingMessageListener implements Runnable { "update_group_or_channel", "chat_deleted", "blocked_by_user", "unblocked_by_user", "message_seen", "removed_from_group", "removed_from_channel", - "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true; + "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true; default -> false; }; } @@ -88,13 +103,15 @@ public class IncomingMessageListener implements Runnable { String action = response.getString("action"); JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject(); - + JSONObject finalMsg = msg; + JSONObject finalMsg1 = msg; switch (action) { case "added_to_group", "added_to_channel", - "removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> { + "removed_from_group", "removed_from_channel", + "chat_deleted", "created_private_chat" -> { + // این قسمت مستقل از UI/کنسول است System.out.println("🔄 Chat list changed. Updating..."); Session.forceRefreshChatList = true; - System.out.println("🧪 Calling requestChatList() after being added"); String chatId = msg.getString("chat_id"); String chatType = msg.getString("chat_type"); @@ -106,36 +123,67 @@ public class IncomingMessageListener implements Runnable { } } - case "chat_updated" -> { - System.out.println("\n🔄 Chat info updated."); +// case "chat_updated" -> { +// if (uiMode == UIMode.UI) { +// bumpChatListFromUpdate(msg); // برای UI (سایدبار و سورت) +// } else { +// updateLastMessageTime(msg); // برای کنسول (لیست‌های Session) +// } +// } - if (msg.has("last_message_time")) { - updateLastMessageTime(msg); - } else { - new Thread(() -> { - try { - handleAdminRoleChanged(msg); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); - } - } - - - case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> { + case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { try { - handleAdminRoleChanged(msg); //new thread + handleAdminRoleChanged(finalMsg1); } catch (IOException e) { e.printStackTrace(); } }).start(); } + case "new_message" -> { + JSONObject data = response.optJSONObject("data"); + if (data == null) break; - default -> displayRealTimeMessage(action, msg); + String mid = data.optString("id", data.optString("message_id","")); + if (mid.isEmpty()) break; + if (!seenMessageIds.add(mid)) break; + + bumpChatListFromMessage(data); + + JSONObject uiMsg = new JSONObject(data.toString()); + if (!uiMsg.has("message_id") && uiMsg.has("id")) { + uiMsg.put("message_id", uiMsg.getString("id")); + } + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + var chatCtl = (mc != null) ? mc.getChatPageController() : null; + if (chatCtl != null) { + chatCtl.onRealTimeNewMessage(uiMsg); + } + }); + } + + + case "chat_updated" -> { + var data = response.getJSONObject("data"); + bumpChatListFromUpdate(data); + } + + + + + case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted", + "user_status_changed", "blocked_by_user", "unblocked_by_user", "message_seen" -> { + displayRealTimeMessage(action, msg); + } + + default -> { + System.out.println("\n❓ Unknown real-time action: " + action); + System.out.println(msg.toString(2)); + } } System.out.print(">> "); @@ -362,4 +410,58 @@ public class IncomingMessageListener implements Runnable { } } } + + + + private static LocalDateTime parseIsoFlexible(String iso) { + if (iso == null || iso.isBlank()) return null; + try { return LocalDateTime.parse(iso); } catch (Exception ignore) {} + try { return OffsetDateTime.parse(iso).toLocalDateTime(); } catch (Exception ignore) {} + return null; + } + + + + private void bumpChatListFromUpdate(JSONObject data) { + try { + UUID chatId = UUID.fromString(data.optString("chat_id","")); + String chatType = data.optString("chat_type",""); + LocalDateTime ts = parseIsoFlexible(data.optString("last_message_time", null)); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ false, null); + }); + } catch (Exception e) { System.err.println("[RT] bumpChatListFromUpdate: " + e.getMessage()); } + } + + + + private String previewOf(String type, String content) { + String t = type == null ? "" : type.trim().toUpperCase(); + return switch (t) { + case "IMAGE" -> "[Image]"; + case "AUDIO" -> "[Audio]"; + case "VIDEO" -> "[Video]"; + case "FILE" -> "[File]"; + default -> (content == null ? "" : content); + }; + } + + private void bumpChatListFromMessage(JSONObject m) { + try { + UUID chatId = UUID.fromString(m.optString("receiver_id","")); + String chatType = m.optString("receiver_type",""); + LocalDateTime ts = parseIsoFlexible(m.optString("send_at", null)); + + String preview = previewOf(m.optString("message_type","TEXT"), + m.optString("content","")); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ true, preview); + }); + } catch (Exception e) { System.err.println("[RT] bumpChatListFromMessage: " + e.getMessage()); } + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 5b5efb6..eb68156 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -171,11 +171,22 @@ public class TelegramClient { return instance; } +// public void startConsole() { +// try { +// connectIfNeeded(); +// initHandlerIfNeeded(); +// startListenerOnce(); +// showMainMenu(); +// } catch (IOException e) { +// System.err.println("❌ Error connecting to server: " + e.getMessage()); +// } +// } + public void startConsole() { try { connectIfNeeded(); initHandlerIfNeeded(); - startListenerOnce(); + startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول showMainMenu(); } catch (IOException e) { System.err.println("❌ Error connecting to server: " + e.getMessage()); @@ -185,14 +196,23 @@ public class TelegramClient { public void start() { startConsole(); } //UI only +// public static synchronized TelegramClient getOrInitForUI() throws IOException { +// TelegramClient cli = getInstance(); +// cli.connectIfNeeded(); +// cli.initHandlerIfNeeded(); +// cli.startListenerOnce(); +// return cli; +// } + public static synchronized TelegramClient getOrInitForUI() throws IOException { TelegramClient cli = getInstance(); cli.connectIfNeeded(); cli.initHandlerIfNeeded(); - cli.startListenerOnce(); + cli.startListenerOnce(IncomingMessageListener.UIMode.UI); // ← UI return cli; } + private synchronized void connectIfNeeded() throws IOException { if (socket != null && socket.isConnected() && !socket.isClosed()) return; @@ -208,10 +228,23 @@ public class TelegramClient { } } - private void startListenerOnce() { +// private void startListenerOnce() { +// if (listenerStarted) return; +// listenerStarted = true; +// Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener"); +// listenerThread.setDaemon(true); +// listenerThread.start(); +// } + + + private void startListenerOnce(IncomingMessageListener.UIMode mode) { if (listenerStarted) return; listenerStarted = true; - Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener"); + + Thread listenerThread = new Thread( + new IncomingMessageListener(in, mode), + "socket-listener" + ); listenerThread.setDaemon(true); listenerThread.start(); } @@ -269,5 +302,13 @@ public class TelegramClient { public static void main(String[] args) { new TelegramClient().startConsole(); } + + // TelegramClient.java + private IncomingMessageListener listener; + + public IncomingMessageListener getListener() { + return listener; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 9275b96..38e079e 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -173,4 +173,7 @@ public class ChatEntry { public UUID getLastMessageSenderId() { return lastMessageSenderId; } public void setLastMessageSenderId(UUID lastMessageSenderId) { this.lastMessageSenderId = lastMessageSenderId; } + + public void setUnread(int unreadCount){this.unreadCount = unreadCount;} + public int getUnread(){return unreadCount;} } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 31fc039..5f8f068 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1616,21 +1616,16 @@ public class ClientHandler implements Runnable { break; } - // دریافت شناسه چت و نوع حذف (یک‌طرفه یا دوطرفه) UUID targetId = UUID.fromString(requestJson.getString("chat_id")); boolean both = requestJson.getBoolean("both"); - // Real-Time Event Dispatch (اطلاع‌رسانی ریل تایم) if (both) { - // حذف دوطرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); RealTimeEventDispatcher.notifyChatDeleted("private", currentUser.getInternal_uuid(), List.of(targetId)); } else { - // حذف یک‌طرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); } - // فراخوانی متد حذف چت response = handleDeleteChat(requestJson); break; } @@ -2910,6 +2905,8 @@ public class ClientHandler implements Runnable { RealTimeEventDispatcher.sendToUser(receiver, chatPayload); } + RealTimeEventDispatcher.sendToUser(senderId, chatPayload); + JSONObject data = new JSONObject(); data.put("message_id", messageId.toString()); return new ResponseModel("success", "Message sent successfully.", data); diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index aee726f..6a1269b 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -80,6 +80,8 @@ public class ChatPageController { // ===== state ===== private String chatName; private final ThemeManager themeManager = ThemeManager.getInstance(); + private final java.util.Set pendingClientMsgIds = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); // Where your icons live private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; @@ -97,6 +99,7 @@ public class ChatPageController { } + private void initCurrentUserId() { try { String meStr = org.to.telegramfinalproject.Client.Session @@ -332,15 +335,128 @@ public class ChatPageController { // ----- actions ----- +// private void sendMessage() { +// String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); +// if (!text.isEmpty()) { +// addMessage("You", text); +// messageInput.clear(); +// // TODO: send to server +// } +// } + + private void sendMessage() { - String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); - if (!text.isEmpty()) { - addMessage("You", text); - messageInput.clear(); - // TODO: send to server + // 0) Read and validate input + String raw = messageInput.getText(); + String text = (raw == null) ? "" : raw.trim(); + if (text.isEmpty()) return; + + if (currentChat == null) { + addSystemMessage("No chat is selected."); + return; } + + // 1) Clear input immediately for good UX + messageInput.clear(); + + // 2) Snapshot chat info (must be final for lambdas) + final UUID targetChatId = currentChat.getId(); + final String targetType = currentChat.getType(); // "private" | "group" | "channel" + final String contentToSend = text; // effectively final + + // 3) Build the SAME JSON as your console method (for TEXT only) + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "send_message"); + req.put("receiver_type", targetType); + req.put("receiver_id", targetChatId.toString()); + req.put("content", contentToSend); + req.put("message_type", "TEXT"); + + // 4) Send on a background thread + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + Platform.runLater(() -> addSystemMessage("Send failed: " + ex.getMessage())); + return; + } + + // 5) Check status like your console method + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + String err = (resp != null) ? resp.optString("message", "No response") : "No response"; + Platform.runLater(() -> addSystemMessage("Send failed: " + err)); + return; + } + + // 6) Extract fields (your console reads data.message_id; handle both shapes) + org.json.JSONObject data = resp.optJSONObject("data"); + String messageId = null; + String sendAtIso = null; + if (data != null) { + // If server returns { data: { message_id, send_at, ... } } + messageId = data.optString("message_id", null); + + // Some servers nest: { data: { message: {...} } } + if (messageId == null) { + org.json.JSONObject msgObj = data.optJSONObject("message"); + if (msgObj != null) { + messageId = msgObj.optString("message_id", null); + sendAtIso = msgObj.optString("send_at", null); + } + } else { + sendAtIso = data.optString("send_at", null); + } + } + if (messageId == null) messageId = java.util.UUID.randomUUID().toString(); + + final java.time.LocalDateTime ts = + (sendAtIso != null && !sendAtIso.isBlank()) ? parseWhen(sendAtIso) + : java.time.LocalDateTime.now(); + + final String fMessageId = messageId; + final java.time.LocalDateTime fTs = ts; + + // 7) Update UI on FX thread (render outgoing bubble + index for reply previews) + Platform.runLater(() -> { + // If user switched chats while sending, don’t render here + if (currentChat == null || !currentChat.getId().equals(targetChatId)) return; + + addBubble( + true, // outgoing + "You", // display name + "TEXT", // message type + contentToSend, // content + fTs, // timestamp + fMessageId, // message_id + "", "", "", // forwarded_from, forwarded_by, reply_to_id + false, // edited + null // reactions + ); + + //Real time + var mc = MainController.getInstance(); + if (mc != null) { + String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend); + mc.onChatUpdated(targetChatId, targetType, fTs, /*isIncoming*/ false, preview); + } + + // Keep it in msgIndex for reply previews + org.json.JSONObject idx = new org.json.JSONObject(); + idx.put("message_id", fMessageId); + idx.put("message_type", "TEXT"); + idx.put("content", contentToSend); + idx.put("sender_name", "You"); + idx.put("sender_id", (me != null) ? me.toString() : ""); + idx.put("send_at", fTs.toString()); + msgIndex.put(fMessageId, idx); + }); + }).start(); } + + private void openFileChooser() { FileChooser fc = new FileChooser(); fc.setTitle("Select a file to send"); @@ -706,7 +822,6 @@ public class ChatPageController { row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT : javafx.geometry.Pos.CENTER_LEFT); - // کمی padding اطراف هر پیام برای کاهش فاصله‌های ناخوشایند row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6)); messageContainer.getChildren().add(row); @@ -799,9 +914,64 @@ public class ChatPageController { } - private static boolean notBlank(String s) { return s != null && !s.isBlank(); } private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "…" : s; } + + public boolean isSameChat(UUID chatId, String type) { + return currentChat != null + && currentChat.getId().equals(chatId) + && currentChat.getType().equalsIgnoreCase(type); + } + + public void onRealTimeNewMessage(JSONObject m) { + try { + String chatIdStr = str(m,"receiver_id"); + String chatType = str(m,"receiver_type"); + if (chatIdStr.isEmpty() || chatType.isEmpty()) return; + + UUID chatId = UUID.fromString(chatIdStr); + if (!isSameChat(chatId, chatType)) { + System.out.println("[UI] RT msg for another chat: " + chatId); + return; + } + + // id → message_id fallback + if (!m.has("message_id") && m.has("id")) { + m.put("message_id", m.getString("id")); + } + + String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") + : (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown"); + + String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT"; + String content = str(m,"content"); + String whenIso = str(m,"send_at"); + String msgId = str(m,"message_id"); + + LocalDateTime ts = parseWhen(whenIso); + if (ts == null) ts = LocalDateTime.now(); + + addBubble(false, senderName, type, content, ts, msgId, + str(m,"forwarded_from"), str(m,"forwarded_by"), str(m,"reply_to_id"), + bool(m,"is_edited"), arr(m,"reactions")); + + if (hasVal(msgId)) msgIndex.put(msgId, m); + + if (currentChat != null) markAsRead(currentChat); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + + + + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 132b5bc..08fb599 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -9,10 +9,12 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; @@ -75,6 +77,62 @@ public class MainController { private final Map itemControllers = new HashMap<>(); + //For realtime handling + + public void onChatUpdated(UUID chatId, + String chatType, + LocalDateTime lastTs, + boolean isIncoming, + String lastPreview) { + var opt = java.util.stream.Stream + .of(Session.chatList, Session.activeChats, Session.archivedChats) + .flatMap(java.util.Collection::stream) + .filter(c -> c.getId().equals(chatId)) + .findFirst(); + if (opt.isEmpty()) return; + + ChatEntry chat = opt.get(); + + if (lastTs != null) chat.setLastMessageTime(String.valueOf(lastTs)); // ← نوع: LocalDateTime + if (lastPreview != null) chat.setLastMessagePreview(lastPreview); + + boolean isOpen = Session.currentChatId != null && + Session.currentChatId.equals(chatId.toString()); + if (isIncoming && !isOpen) { + chat.setUnread(chat.getUnread() + 1); + } + + java.util.Comparator byTimeDesc = (c1, c2) -> { + var t1 = c1.getLastMessageTime(); + var t2 = c2.getLastMessageTime(); + if (t1 == null && t2 == null) return 0; + if (t1 == null) return 1; + if (t2 == null) return -1; + return t2.compareTo(t1); + }; + Session.chatList.sort(byTimeDesc); + Session.activeChats.sort(byTimeDesc); + Session.archivedChats.sort(byTimeDesc); + + refreshChatListUI(); + refreshBadgesIfAny(); + } + + + private void refreshChatListUI() { + Platform.runLater(() -> { + chatListContainer.getChildren().clear(); + itemControllers.clear(); + populateChatListFromSession(); // همون متدی که نودها رو می‌سازه و می‌چسبونه + }); + } + + + private void refreshBadgesIfAny() { + // آپدیت نشان‌ها اگر لازم است + } + + @FXML public void initialize() { // addSampleChats(); @@ -270,6 +328,30 @@ public class MainController { // } // } +// private void addChatNode(ChatEntry chat) { +// try { +// FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node item = fx.load(); +// ChatItemController cc = fx.getController(); +// +// String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); +// String timeText = formatChatTime(chat.getLastMessageTime()); +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node chatItem = loader.load(); +// ChatItemController controller = loader.getController(); +// +// cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); +// item.setOnMouseClicked(e -> openChat(chat)); +// chatListContainer.getChildren().add(item); +// +// itemControllers.put(chat.getId(), cc); +// +// } catch (Exception ex) { +// ex.printStackTrace(); +// } +// } + + private void addChatNode(ChatEntry chat) { try { FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); @@ -277,22 +359,30 @@ public class MainController { ChatItemController cc = fx.getController(); String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); - String timeText = formatChatTime(chat.getLastMessageTime()); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); - Node chatItem = loader.load(); - ChatItemController controller = loader.getController(); - cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); + // اگر lastMessageTime از نوع LocalDateTime است: + String timeText = chat.getLastMessageTime() == null + ? "" + : formatChatTime(chat.getLastMessageTime()); + + cc.setChatData( + chat.getName(), + preview, + timeText, + chat.getUnreadCount(), + "/org/to/telegramfinalproject/Avatars/default_profile.png" + ); + item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); itemControllers.put(chat.getId(), cc); - } catch (Exception ex) { ex.printStackTrace(); } } + private String mapTypeToLabel(String t) { switch (t.toUpperCase()) { case "IMAGE": return "[Image]"; @@ -325,6 +415,9 @@ public class MainController { ChatPageController controller = loader.getController(); controller.showChat(chat); + this.chatPageController = controller; + Session.currentChatId = chat.getId().toString(); + chatDisplayArea.getChildren().setAll(chatPage); ChatItemController item = itemControllers.get(chat.getId()); @@ -463,6 +556,17 @@ public class MainController { // ===== Search UI state ===== private final java.util.List searchBacking = new java.util.ArrayList<>(); + @FXML private AnchorPane chatHost; // کانتینر مناسب در مین برای قرار دادن ChatPage + + private ChatPageController chatPageController; + + + + public ChatPageController getChatPageController() { + return chatPageController; + } + + private enum SRType { USER, GROUP, CHANNEL, MESSAGE } private static class SearchResult { @@ -483,7 +587,6 @@ public class MainController { } String toDisplay() { - // رشته‌ای که داخل ListView نشان می‌دهیم switch (type) { case MESSAGE: String left = (title == null || title.isBlank()) ? "Message" : title; @@ -512,7 +615,6 @@ public class MainController { showSearchPanel(); - // درخواست به سرور (مثل کنسول) org.json.JSONObject req = new org.json.JSONObject(); req.put("action", "search"); req.put("keyword", keyword); @@ -582,7 +684,7 @@ public class MainController { String subtitle = (ctx.isBlank()? "" : ctx) + (content.isBlank()? "" : (subtitleSep(ctx)+content)); tmp.add(new SearchResult( SRType.MESSAGE, senderName, subtitle, rType, rUuid, - it.optString("receiver_display_id", ""), // اگر داشتی + it.optString("receiver_display_id", ""), it.optString("message_id", null), time )); @@ -612,11 +714,10 @@ public class MainController { private void openSearchResult(SearchResult r) { switch (r.type) { case USER: { - // مثل کنسول: اگر چت private با این یوزر داریم بازش کن؛ - // وگرنه chat_id را از سرور بگیر/بساز، بعد باز کن. + java.util.UUID chatId = findExistingPrivateChatId(r.uuid); if (chatId == null) { - chatId = fetchOrCreatePrivateChat(r.uuid); // نیاز به API سمت سرور + chatId = fetchOrCreatePrivateChat(r.uuid); if (chatId == null) { System.out.println("❌ Failed to create/find private chat."); return; @@ -629,13 +730,12 @@ public class MainController { ce.setName(r.title); // profile_name ce.setType("private"); - openChat(ce); // همون متد فعلی MainController + openChat(ce); break; } case GROUP: case CHANNEL: { - // اگر تو لیست هست بازش کن، وگرنه با همون uuid باز کن org.to.telegramfinalproject.Models.ChatEntry existing = findExistingChat(r.uuid, r.receiverType); if (existing != null) { @@ -652,7 +752,6 @@ public class MainController { } case MESSAGE: { - // مثل کنسول: چتِ پیام را باز کن (اسکرول به پیام را بعداً اضافه کن) org.to.telegramfinalproject.Models.ChatEntry existing = findExistingChat(r.uuid, r.receiverType); if (existing != null) { @@ -661,7 +760,7 @@ public class MainController { org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); ce.setId(r.uuid.toString()); // receiver internal_uuid ce.setType(r.receiverType); - ce.setName(guessNameForReceiver(r)); // اگر خواستی از subtitle استفاده کن + ce.setName(guessNameForReceiver(r)); ce.setDisplayId(r.displayId); openChat(ce); } @@ -684,14 +783,12 @@ public class MainController { } private java.util.UUID findExistingPrivateChatId(java.util.UUID otherUserUuid) { - // اگر در لیست چت‌ها private با همین طرف داری و internal_id همان chat_id است، برش گردان var list = (org.to.telegramfinalproject.Client.Session.chatList==null) ? java.util.Collections.emptyList() : org.to.telegramfinalproject.Client.Session.chatList; for (var c : list) { if ("private".equalsIgnoreCase(c.getType())) { - // اگر مدل‌ات otherUserId در ChatEntry دارد، از آن استفاده کن if (otherUserUuid.equals(c.getOtherUserId())) { try { return java.util.UUID.fromString(c.getId().toString()); } catch (Exception ignored) {} } From 46216d336861eb174dcd5c217113a0686f953b0f Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sat, 30 Aug 2025 23:04:59 +0330 Subject: [PATCH 2/3] Work on send message Real time in UI(update chat list) --- .../Models/ChatEntry.java | 8 ++- .../UI/ChatPageController.java | 3 +- .../UI/MainController.java | 65 +++++++++++-------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 38e079e..3fefe32 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -128,8 +128,6 @@ public class ChatEntry { this.archived = archived; } -// public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); -// } public void setLastMessageTime(String newTime) { @@ -176,4 +174,10 @@ public class ChatEntry { public void setUnread(int unreadCount){this.unreadCount = unreadCount;} public int getUnread(){return unreadCount;} + + + public void setLastMessageTime(LocalDateTime t) { + this.lastMessageTime = t; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 6a1269b..0269753 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -765,14 +765,12 @@ public class ChatPageController { boolean edited, org.json.JSONArray reactions ) { - // meta (فرستنده + زمان [+ edited]) String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt); if (edited) metaText += " (edited)"; Label meta = new Label(metaText); meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); meta.setWrapText(true); - // نرمال‌سازی نوع پیام و متن String t = type == null ? "" : type.trim().toUpperCase(); boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t); @@ -824,6 +822,7 @@ public class ChatPageController { row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6)); + messageContainer.getChildren().add(row); } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 08fb599..5a8b81b 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -79,28 +79,24 @@ public class MainController { //For realtime handling - public void onChatUpdated(UUID chatId, - String chatType, - LocalDateTime lastTs, - boolean isIncoming, - String lastPreview) { - var opt = java.util.stream.Stream - .of(Session.chatList, Session.activeChats, Session.archivedChats) - .flatMap(java.util.Collection::stream) - .filter(c -> c.getId().equals(chatId)) - .findFirst(); - if (opt.isEmpty()) return; + public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs, + boolean isIncoming, String lastPreview) { - ChatEntry chat = opt.get(); - - if (lastTs != null) chat.setLastMessageTime(String.valueOf(lastTs)); // ← نوع: LocalDateTime - if (lastPreview != null) chat.setLastMessagePreview(lastPreview); + // اگر هیچ‌کجا نیست، بی‌خیال + boolean exists = + (Session.chatList != null && Session.chatList.stream().anyMatch(c -> chatId.equals(c.getId()))) || + (Session.activeChats != null && Session.activeChats.stream().anyMatch(c -> chatId.equals(c.getId()))) || + (Session.archivedChats != null && Session.archivedChats.stream().anyMatch(c -> chatId.equals(c.getId()))); + if (!exists) return; boolean isOpen = Session.currentChatId != null && Session.currentChatId.equals(chatId.toString()); - if (isIncoming && !isOpen) { - chat.setUnread(chat.getUnread() + 1); - } + + forEachChat(chatId, chat -> { + chat.setLastMessageTime(lastTs); + if (lastPreview != null) chat.setLastMessagePreview(lastPreview); + if (isIncoming && !isOpen) chat.setUnreadCount(chat.getUnreadCount() + 1); // ✅ یکدست با unreadCount + }); java.util.Comparator byTimeDesc = (c1, c2) -> { var t1 = c1.getLastMessageTime(); @@ -110,15 +106,34 @@ public class MainController { if (t2 == null) return -1; return t2.compareTo(t1); }; - Session.chatList.sort(byTimeDesc); - Session.activeChats.sort(byTimeDesc); - Session.archivedChats.sort(byTimeDesc); + if (Session.chatList != null) Session.chatList.sort(byTimeDesc); + if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc); + if (Session.archivedChats != null) Session.archivedChats.sort(byTimeDesc); refreshChatListUI(); refreshBadgesIfAny(); } + + + // MainController + private void forEachChat(UUID chatId, java.util.function.Consumer fn) { + java.util.List> lists = java.util.List.of( + Session.chatList != null ? Session.chatList : java.util.List.of(), + Session.activeChats != null ? Session.activeChats : java.util.List.of(), + Session.archivedChats != null ? Session.archivedChats : java.util.List.of() + ); + for (var lst : lists) { + for (var c : lst) { + if (chatId.equals(c.getId())) { + fn.accept(c); + } + } + } + } + + private void refreshChatListUI() { Platform.runLater(() -> { chatListContainer.getChildren().clear(); @@ -195,15 +210,12 @@ public class MainController { // } // }); - // وقتی کاربر Enter زد روی سرچ‌بار، درخواست سرچ بفرست searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); - // باز/بسته کردن پنل نتایج با تایپ (دلخواه) searchBar.textProperty().addListener((obs,o,n)->{ if (n!=null && !n.isBlank()) showSearchPanel(); }); - // کلیک روی نتیجه chatSearchResults.setOnMouseClicked(e -> { int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); if (idx >= 0 && idx < searchBacking.size()) { @@ -283,7 +295,6 @@ public class MainController { if (list == null || list.isEmpty()) return; - // جدا کردن Saved از بقیه ChatEntry saved = null; java.util.List others = new java.util.ArrayList<>(); for (ChatEntry c : list) { @@ -360,7 +371,6 @@ public class MainController { String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); - // اگر lastMessageTime از نوع LocalDateTime است: String timeText = chat.getLastMessageTime() == null ? "" : formatChatTime(chat.getLastMessageTime()); @@ -419,7 +429,7 @@ public class MainController { Session.currentChatId = chat.getId().toString(); chatDisplayArea.getChildren().setAll(chatPage); - + chat.setUnreadCount(0); ChatItemController item = itemControllers.get(chat.getId()); if (item != null) item.setUnread(0); @@ -815,7 +825,6 @@ public class MainController { } private String guessNameForReceiver(SearchResult r) { - // فقط برای زمانی که موجودیت در لیست نبود if (r.title != null && !r.title.isBlank()) return r.title; if (r.receiverType != null) { switch (r.receiverType) { From 71ca5b5ff428f11e1c5cd96e2d2f28e2ef2441bd Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sat, 30 Aug 2025 23:21:33 +0330 Subject: [PATCH 3/3] Connect register to main --- .../UI/RegisterController.java | 177 +++++++++++++----- 1 file changed, 129 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java b/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java index afc2174..e920f2c 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java @@ -40,18 +40,16 @@ public class RegisterController { @FXML public void initialize() { - // Sync password and confirm fields with their visible counterparts - visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); - visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty()); try { - connection = new ClientConnection("localhost", 8000); - } catch (Exception e) { - System.out.println("Could not connect to server: " + e.getMessage()); + org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + } catch (Exception ignored) { } + visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); + visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty()); } - @FXML + @FXML private void togglePasswordVisibility() { passwordVisible = !passwordVisible; visiblePasswordField.setVisible(passwordVisible); @@ -71,66 +69,149 @@ public class RegisterController { toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁"); } +// @FXML +// private void handleRegister() { +// String userID = userIdField.getText().trim(); +// String username = usernameField.getText().trim(); +// String profileName = profileNameField.getText().trim(); +// String password = passwordField.getText(); +// String confirmPass = confirmPasswordField.getText(); +// +// userDatabase userDb = new userDatabase(); +// String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"; +// +// // 1. Empty fields +// if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) { +// showError("Please fill in all required fields."); +// return; +// } +// +// // 2. Username exists +// if (userDb.existsByUsername(username)) { +// showError("This username is already taken."); +// return; +// } +// +// // 3. User ID exists +// if (userDb.existsByUserId(userID)) { +// showError("This user ID is already taken."); +// return; +// } +// +// // 4. Password mismatch +// if (!password.equals(confirmPass)) { +// showError("Passwords do not match."); +// return; +// } +// +// // 5. Weak password +// if (!password.matches(passwordRegex)) { +// showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); +// return; +// } +// +// // 6. Attempt registration +// try { +// JSONObject request = new JSONObject(); +// request.put("action", "register"); +// request.put("user_id", userID); +// request.put("username", username); +// request.put("password", password); +// request.put("profile_name", profileName); +// connection.send(request.toString()); +// +// // Simulate successful registration (since main.fxml isn’t ready) +// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!"); +// alert.show(); +// +// } catch (Exception ex) { +// showError("Failed to register. Please try again later."); +// } +// } + + + @FXML private void handleRegister() { - String userID = userIdField.getText().trim(); - String username = usernameField.getText().trim(); + String userID = userIdField.getText().trim(); + String username = usernameField.getText().trim(); String profileName = profileNameField.getText().trim(); - String password = passwordField.getText(); + String password = passwordField.getText(); String confirmPass = confirmPasswordField.getText(); userDatabase userDb = new userDatabase(); String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"; - // 1. Empty fields + // اعتبارسنجی‌ها if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) { - showError("Please fill in all required fields."); - return; + showError("Please fill in all required fields."); return; } + if (userDb.existsByUsername(username)) { showError("This username is already taken."); return; } + if (userDb.existsByUserId(userID)) { showError("This user ID is already taken."); return; } + if (!password.equals(confirmPass)) { showError("Passwords do not match."); return; } + if (!password.matches(passwordRegex)) { showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); return; } - // 2. Username exists - if (userDb.existsByUsername(username)) { - showError("This username is already taken."); - return; - } + // UI را موقتاً disable کن (اختیاری) + setBusy(true); - // 3. User ID exists - if (userDb.existsByUserId(userID)) { - showError("This user ID is already taken."); - return; - } + new Thread(() -> { + try { + // 1) مطمئن شو کانکشن/لیسنر روشن است + var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + var handler = cli.getHandler(); - // 4. Password mismatch - if (!password.equals(confirmPass)) { - showError("Passwords do not match."); - return; - } + // 2) Register + var regReq = new org.json.JSONObject() + .put("action", "register") + .put("user_id", userID) + .put("username", username) + .put("password", password) + .put("profile_name", profileName); - // 5. Weak password - if (!password.matches(passwordRegex)) { - showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); - return; - } + var regResp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(regReq); + if (regResp == null || !"success".equalsIgnoreCase(regResp.optString("status"))) { + javafx.application.Platform.runLater(() -> { + setBusy(false); + showError(regResp != null ? regResp.optString("message","Register failed.") : "Register failed."); + }); + return; + } - // 6. Attempt registration - try { - JSONObject request = new JSONObject(); - request.put("action", "register"); - request.put("user_id", userID); - request.put("username", username); - request.put("password", password); - request.put("profile_name", profileName); - connection.send(request.toString()); + // 3) Auto-Login با همان کانکشن + handler.login(username, password); - // Simulate successful registration (since main.fxml isn’t ready) - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!"); - alert.show(); + javafx.application.Platform.runLater(() -> { + setBusy(false); + if (org.to.telegramfinalproject.Client.Session.currentUser == null || !handler.wasSuccess()) { + showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage()); + return; + } + org.to.telegramfinalproject.UI.AppRouter.showMain(); + }); - } catch (Exception ex) { - showError("Failed to register. Please try again later."); - } + } catch (Exception ex) { + javafx.application.Platform.runLater(() -> { + setBusy(false); + showError("Failed to register/login: " + ex.getMessage()); + }); + } + }, "register-thread").start(); } + private void setBusy(boolean b){ + userIdField.setDisable(b); + usernameField.setDisable(b); + profileNameField.setDisable(b); + passwordField.setDisable(b); + visiblePasswordField.setDisable(b); + confirmPasswordField.setDisable(b); + visibleConfirmPasswordField.setDisable(b); + togglePasswordBtn.setDisable(b); + toggleConfirmBtn.setDisable(b); + } + + + @FXML private void switchToLogin() throws IOException { showLogin();