From ff6f771e5c0664ecdebcc3b703e0a4299411e965 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 27 Aug 2025 23:22:07 +0330 Subject: [PATCH 01/19] Connect backend to UI --- .../Client/ActionHandler.java | 188 ++++++++------ .../Client/TelegramClient.java | 217 +++++++++++++--- .../Database/MessageDatabase.java | 69 +++++ .../Models/ChatEntry.java | 24 ++ .../telegramfinalproject/Models/JsonUtil.java | 6 + .../telegramfinalproject/Models/Message.java | 5 + .../Server/ClientHandler.java | 132 ++++------ .../to/telegramfinalproject/UI/AppRouter.java | 37 +++ .../UI/ChatItemController.java | 7 + .../UI/ChatPageController.java | 238 ++++++++++++++++-- .../UI/IntroController.java | 43 ++-- .../UI/LoginController.java | 146 +++++++---- .../UI/MainController.java | 180 ++++++++++--- .../UI/SidebarMenuController.java | 9 + .../UI/TelegramApplication.java | 61 ++++- 15 files changed, 1044 insertions(+), 318 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/UI/AppRouter.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index d27b6cb..a4a510a 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -27,6 +27,14 @@ public class ActionHandler { public static volatile boolean forceExitChat = false; public static ActionHandler instance; + //use for UI + private volatile String lastStatus = "error"; // success | error + private volatile String lastMessage = ""; + + public String getLastStatus() { return lastStatus; } + public String getLastMessage() { return lastMessage; } + public boolean wasSuccess() { return "success".equalsIgnoreCase(lastStatus); } + @@ -42,6 +50,17 @@ public class ActionHandler { } + public void login(String username , String password){ + JSONObject request = new JSONObject(); + request.put("action", "login"); + request.put("user_id", JSONObject.NULL); + request.put("username", username); + request.put("password", password); + request.put("profile_name", JSONObject.NULL); + + this.send(request); + + } public void loginHandler() { System.out.println("Login form: \n"); System.out.print("Username: "); @@ -197,7 +216,7 @@ public class ActionHandler { } - private void addContact(UUID contactId) { + public void addContact(UUID contactId) { JSONObject req = new JSONObject(); req.put("action", "add_contact"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -206,7 +225,7 @@ public class ActionHandler { } - private void joinGroupOrChannel(String type, String uuid) { + public void joinGroupOrChannel(String type, String uuid) { JSONObject req = new JSONObject(); req.put("action", "join_" + type); req.put("user_id", Session.getUserUUID()); @@ -216,7 +235,7 @@ public class ActionHandler { - private ChatEntry fetchChatInfo(String receiverId, String receiverType) { + public ChatEntry fetchChatInfo(String receiverId, String receiverType) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", receiverId); @@ -256,7 +275,7 @@ public class ActionHandler { } - private void refreshChatList() { + public void refreshChatList() { JSONObject req = new JSONObject(); req.put("action", "get_chat_list"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -325,21 +344,38 @@ public class ActionHandler { } private ChatEntry parseChatEntry(JSONObject chat) { - ChatEntry entry = new ChatEntry( - UUID.fromString(chat.getString("internal_id")), - chat.getString("id"), - chat.getString("name"), - chat.optString("image_url", ""), - chat.getString("type"), - chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")), - chat.optBoolean("is_owner", false), - chat.optBoolean("is_admin", false) - ); + UUID internalId = null; + try { internalId = UUID.fromString(chat.getString("internal_id")); } catch (Exception ignored) {} - if (chat.has("other_user_id") && !chat.isNull("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); + String id = chat.optString("id", ""); + String name = chat.optString("name", ""); + String imageUrl = chat.optString("image_url", ""); + String type = chat.optString("type", ""); + boolean isOwner = chat.optBoolean("is_owner", false); + boolean isAdmin = chat.optBoolean("is_admin", false); + + LocalDateTime lastTime = null; + String lts = chat.optString("last_message_time", null); + if (lts != null && !"null".equalsIgnoreCase(lts)) { + try { lastTime = LocalDateTime.parse(lts); } catch (Exception ignored) {} + } + + ChatEntry entry = new ChatEntry(internalId, id, name, imageUrl, type, lastTime, isOwner, isAdmin); + + entry.setUnreadCount(chat.optInt("unread_count", 0)); + String preview = chat.optString("last_message_preview", null); + if (preview != null && !"null".equalsIgnoreCase(preview)) { + entry.setLastMessagePreview(preview); + } + + String other = chat.optString("other_user_id", null); + if (other != null && !"null".equalsIgnoreCase(other)) { + try { entry.setOtherUserId(UUID.fromString(other)); } catch (Exception ignored) {} + } + + if (chat.has("is_saved_messages")) { + entry.setSavedMessages(chat.optBoolean("is_saved_messages", false)); } - entry.setSavedMessages(chat.optBoolean("is_saved_messages", false)); return entry; } @@ -433,7 +469,7 @@ public class ActionHandler { - private void send(JSONObject request) { + public void send(JSONObject request) { try { if (!request.has("action") || request.isNull("action")) { System.err.println("❌ Invalid request: missing action."); @@ -471,6 +507,12 @@ public class ActionHandler { return; + status = response.optString("status","error"); + String message = response.optString("message",""); + this.lastStatus = status; + this.lastMessage = message; + + switch (action) { case "login": case "register": @@ -1057,7 +1099,7 @@ public class ActionHandler { } - private void viewProfile(UUID targetId) { + public void viewProfile(UUID targetId) { JSONObject req = new JSONObject(); req.put("action", "view_profile"); req.put("target_id", targetId.toString()); @@ -1085,7 +1127,7 @@ public class ActionHandler { } - private void startPrivateChat(ContactEntry contact) { + public void startPrivateChat(ContactEntry contact) { UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); UUID contactId = contact.getContactId(); @@ -1115,10 +1157,10 @@ public class ActionHandler { ); entry.setOtherUserId(contactId); - Session.chatList.add(0, entry); // اضافه به اول لیست + Session.chatList.add(0, entry); System.out.println("✅ Chat with " + contact.getProfileName() + " started."); - openChat(entry); // 👈 مستقیم وارد چت شو (اختیاری) + openChat(entry); } @@ -1239,7 +1281,7 @@ public class ActionHandler { System.out.println("\nYour Chats:"); System.out.println("0. 📦 Archived Chats"); - // پیدا کردن Saved در لیست + //find save messages chat Integer savedIdxInActive = null; for (int i = 0; i < Session.activeChats.size(); i++) { if (Session.activeChats.get(i).isSavedMessages()) { @@ -1248,23 +1290,19 @@ public class ActionHandler { } } - // مپ شمارهٔ نمایش → ایندکس واقعی در activeChats Map displayToActive = new HashMap<>(); int displayIndex = 1; - // چاپ Saved (در صورت وجود) و ثبت در مپ if (savedIdxInActive != null) { ChatEntry saved = Session.activeChats.get(savedIdxInActive); String last = (saved.getLastMessageTime() == null) ? "No messages yet" : saved.getLastMessageTime().toString(); System.out.println(displayIndex + ". 💾 Saved Messages - Last: " + last); - // نکتهٔ کلیدی: مپ کن تا مثل بقیه با openChat باز شود displayToActive.put(displayIndex, savedIdxInActive); displayIndex++; } - // چاپ بقیهٔ چت‌ها + ثبت مپ for (int i = 0; i < Session.activeChats.size(); i++) { if (savedIdxInActive != null && i == savedIdxInActive) continue; @@ -1276,7 +1314,6 @@ public class ActionHandler { displayIndex++; } - // انتخاب System.out.print("Select a chat by number: "); int choice; try { @@ -1298,11 +1335,11 @@ public class ActionHandler { } ChatEntry selected = Session.activeChats.get(activeIdx); - openChat(selected); // برای Saved هم همین مسیر اجرا می‌شود + openChat(selected); } - private void showArchivedChats() { + public void showArchivedChats() { if (Session.archivedChats == null || Session.archivedChats.isEmpty()) { System.out.println("📭 No archived chats."); return; @@ -1340,7 +1377,7 @@ public class ActionHandler { - private void openChat(ChatEntry chat) { + public void openChat(ChatEntry chat) { //for private chats only if (chat.getType().equalsIgnoreCase("private")) { @@ -1573,7 +1610,7 @@ public class ActionHandler { // } - private boolean showPrivateChatMenu(ChatEntry chat) { + public boolean showPrivateChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; @@ -1677,7 +1714,7 @@ public class ActionHandler { } - private JSONObject getGroupPermissions(UUID groupId) { + public JSONObject getGroupPermissions(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_group_permissions"); req.put("group_id", groupId.toString()); @@ -1691,7 +1728,7 @@ public class ActionHandler { - private boolean showGroupChatMenu(ChatEntry chat) { + public boolean showGroupChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; System.out.println("🚪 Exiting chat due to real-time update."); @@ -1804,7 +1841,7 @@ public class ActionHandler { - private boolean showChannelChatMenu(ChatEntry chat) { + public boolean showChannelChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; System.out.println("🚪 Exiting chat due to real-time update."); @@ -1962,7 +1999,7 @@ public class ActionHandler { - private void transferOwnershipAndLeave(UUID groupId) { + public void transferOwnershipAndLeave(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_admins"); req.put("group_id", groupId.toString()); @@ -2042,7 +2079,7 @@ public class ActionHandler { } - private void removeMemberFromGroup(UUID groupId) { + public void removeMemberFromGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2103,7 +2140,7 @@ public class ActionHandler { - private void addAdminToChannel(UUID channelId) { + public void addAdminToChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2180,7 +2217,7 @@ public class ActionHandler { - private void addAdminToGroup(UUID groupId) { + public void addAdminToGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2242,7 +2279,7 @@ public class ActionHandler { } - private void viewGroupMembers(UUID groupId) { + public void viewGroupMembers(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2270,7 +2307,7 @@ public class ActionHandler { - private void removeSubscriberFromChannel(UUID channelId) { + public void removeSubscriberFromChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2330,7 +2367,7 @@ public class ActionHandler { } - private void transferChannelOwnershipAndLeave(UUID channelId) { + public void transferChannelOwnershipAndLeave(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -2413,7 +2450,7 @@ public class ActionHandler { - private void deleteChannel(UUID channelId) { + public void deleteChannel(UUID channelId) { System.out.print("Are you sure you want to delete the channel? (yes/no): "); String confirm = scanner.nextLine().trim().toLowerCase(); @@ -2436,7 +2473,7 @@ public class ActionHandler { } - private void deleteGroup(UUID groupId) { + public void deleteGroup(UUID groupId) { System.out.print("Are you sure you want to delete the group? (yes/no): "); String confirm = scanner.nextLine().trim().toLowerCase(); @@ -2458,7 +2495,7 @@ public class ActionHandler { } } - private void deleteChat(UUID targetId, boolean both) { + public void deleteChat(UUID targetId, boolean both) { JSONObject req = new JSONObject(); req.put("action", "delete_private_chat"); req.put("chat_id", targetId.toString()); @@ -2475,7 +2512,7 @@ public class ActionHandler { - private void toggleBlock(UUID userId) { + public void toggleBlock(UUID userId) { JSONObject req = new JSONObject(); @@ -2497,7 +2534,7 @@ public class ActionHandler { } - private void leaveChat(UUID id, String type) { + public void leaveChat(UUID id, String type) { JSONObject req = new JSONObject(); req.put("action", "leave_chat"); req.put("user_id", Session.getUserUUID()); @@ -2518,7 +2555,7 @@ public class ActionHandler { } - private void removeAdminFromGroup(UUID groupId) { + public void removeAdminFromGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_admins"); req.put("group_id", groupId.toString()); @@ -2578,7 +2615,7 @@ public class ActionHandler { } - private void removeAdminFromChannel(UUID channelId) { + public void removeAdminFromChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -2642,7 +2679,7 @@ public class ActionHandler { - private void addMemberToGroup(UUID groupId, UUID userId) { + public void addMemberToGroup(UUID groupId, UUID userId) { JSONObject req = new JSONObject(); req.put("action", "add_member_to_group"); req.put("group_id", groupId.toString()); @@ -2654,7 +2691,7 @@ public class ActionHandler { } } - private void addSubscriberToChannel(UUID channelId, UUID targetUserId) { + public void addSubscriberToChannel(UUID channelId, UUID targetUserId) { JSONObject req = new JSONObject(); req.put("action", "add_subscriber_to_channel"); req.put("channel_id", channelId.toString()); @@ -2670,7 +2707,7 @@ public class ActionHandler { } } - private void GroupInfo(UUID groupId){ + public void GroupInfo(UUID groupId){ JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", groupId.toString()); @@ -2699,7 +2736,7 @@ public class ActionHandler { } - private void editGroupInfo(UUID groupId) { + public void editGroupInfo(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", groupId.toString()); @@ -2775,7 +2812,7 @@ public class ActionHandler { - private void viewChannelSubscribers(UUID channelId) { + public void viewChannelSubscribers(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2797,7 +2834,7 @@ public class ActionHandler { } } - private void ChannelInfo(UUID channelInternalId){ + public void ChannelInfo(UUID channelInternalId){ JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", channelInternalId.toString()); @@ -2826,7 +2863,7 @@ public class ActionHandler { } - private void editChannelInfo(UUID channelInternalId) { + public void editChannelInfo(UUID channelInternalId) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", channelInternalId.toString()); @@ -2901,7 +2938,7 @@ public class ActionHandler { } - private JSONObject getResponse() { + public JSONObject getResponse() { try { return TelegramClient.responseQueue.take(); @@ -2931,7 +2968,7 @@ public class ActionHandler { - private void editAdminPermissions(UUID chatId, String chatType) { + public void editAdminPermissions(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", chatType.equals("group") ? "view_group_admins" : "view_channel_admins"); req.put(chatType + "_id", chatId.toString()); @@ -3028,7 +3065,7 @@ public class ActionHandler { } - private void viewChannelAdmins(UUID channelId) { + public void viewChannelAdmins(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -3053,7 +3090,7 @@ public class ActionHandler { } - private JSONObject getChannelPermissions(UUID channelId) { + public JSONObject getChannelPermissions(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "get_channel_permissions"); req.put("channel_id", channelId.toString()); @@ -3353,7 +3390,7 @@ public class ActionHandler { - private void startMenuRefresherThread() { + public void startMenuRefresherThread() { Thread refresher = new Thread(() -> { System.out.println("🟡 Refresher tick. refreshCurrentChatMenu = " + Session.refreshCurrentChatMenu); @@ -3382,7 +3419,7 @@ public class ActionHandler { - private void openForeignChat(ChatEntry chat) { + public void openForeignChat(ChatEntry chat) { System.out.println("🔍 Opening " + chat.getType() + " chat (not in your chat list)"); JSONObject req = new JSONObject(); @@ -3446,7 +3483,7 @@ public class ActionHandler { - private void viewUserProfile(UUID userId) { + public void viewUserProfile(UUID userId) { JSONObject req = new JSONObject(); req.put("action", "view_profile"); req.put("target_id", userId.toString()); @@ -3471,7 +3508,7 @@ public class ActionHandler { } } - private void viewGroupOrChannelInfo(UUID id, String type) { + public void viewGroupOrChannelInfo(UUID id, String type) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", id.toString()); @@ -3496,7 +3533,7 @@ public class ActionHandler { public void toggleArchive(UUID chatId, String chatType) { - // پیدا کردن چت از Session.chatList + Optional optional = Session.chatList.stream() .filter(c -> c.getId().equals(chatId)) .findFirst(); @@ -3522,7 +3559,7 @@ public class ActionHandler { } - private void archiveChat(UUID chatId, String chatType) { + public void archiveChat(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", "archive_chat"); req.put("chat_id", chatId.toString()); @@ -3531,7 +3568,7 @@ public class ActionHandler { if (res != null) System.out.println(res.getString("message")); } - private void unarchiveChat(UUID chatId, String chatType) { + public void unarchiveChat(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", "unarchive_chat"); req.put("chat_id", chatId.toString()); @@ -3663,7 +3700,6 @@ public class ActionHandler { } } - // 🔹 فقط ارسال پیام با chat_id و receiver_type JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); messageJson.put("receiver_type", receiverType); @@ -3683,7 +3719,7 @@ public class ActionHandler { } } - private void refreshContactList() { + public void refreshContactList() { JSONObject req = new JSONObject(); req.put("action", "get_contact_list"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -3741,7 +3777,7 @@ public class ActionHandler { } } - private void viewMessagesInChat(ChatEntry chat) { + public void viewMessagesInChat(ChatEntry chat) { int offset = 0; int limit = 10; @@ -3910,7 +3946,7 @@ public class ActionHandler { } } - private void editMessage(UUID messageId) { + public void editMessage(UUID messageId) { System.out.print("📝 Enter new content: "); String newContent = scanner.nextLine().trim(); @@ -3934,7 +3970,7 @@ public class ActionHandler { } - private void reactToMessage(UUID messageId) { + public void reactToMessage(UUID messageId) { System.out.print("😊 Enter your reaction (e.g., ❤️, 👍, 😂): "); String reaction = scanner.nextLine().trim(); @@ -3957,7 +3993,7 @@ public class ActionHandler { } - private void forwardMessage(UUID originalMessageId) { + public void forwardMessage(UUID originalMessageId) { System.out.println("\n📤 Select a chat to forward this message to:"); List chatList = Session.chatList; @@ -4001,7 +4037,7 @@ public class ActionHandler { } - private void replyToMessage(UUID messageId) { + public void replyToMessage(UUID messageId) { System.out.print("💬 Enter your reply: "); String content = scanner.nextLine().trim(); @@ -4030,7 +4066,7 @@ public class ActionHandler { } - private void deleteMessage(UUID messageId) { + public void deleteMessage(UUID messageId) { System.out.println("\n🗑️ Delete Message Options:"); System.out.println("1. Delete for yourself (one-sided)"); System.out.println("2. Delete for everyone (global) [only if allowed]"); @@ -4078,7 +4114,7 @@ public class ActionHandler { } } - private void showSidebarMenu() { + public void showSidebarMenu() { System.out.println("\n--- Sidebar Menu ---"); System.out.println("1. View Profile"); System.out.println("2. New Group"); diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 8db405e..5b5efb6 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -1,3 +1,129 @@ +//package org.to.telegramfinalproject.Client; +// +//import org.json.JSONObject; +// +//import java.io.BufferedReader; +//import java.io.IOException; +//import java.io.InputStreamReader; +//import java.io.PrintWriter; +//import java.net.Socket; +//import java.util.Map; +//import java.util.Scanner; +//import java.util.UUID; +//import java.util.concurrent.BlockingQueue; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.LinkedBlockingQueue; +// +//public class TelegramClient { +// private static final String SERVER_HOST = "localhost"; +// private static final int SERVER_PORT = 8000; +// private static Socket socket; +// private BufferedReader in; +// private PrintWriter out; +// private final Scanner scanner; +// private ActionHandler handler; +// public static BlockingQueue responseQueue = new LinkedBlockingQueue<>(); +// public static UUID loggedInUserId = null; +// public static final Map> pendingResponses = new ConcurrentHashMap<>(); +// +// +// private static TelegramClient instance; +// +// public TelegramClient() { +// this.scanner = new Scanner(System.in); +// instance = this; +// } +// +// public static TelegramClient getInstance() { +// return instance; +// } +// +// public void start() { +// try { +// socket = new Socket(SERVER_HOST, SERVER_PORT); +// in = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// out = new PrintWriter(socket.getOutputStream(), true); +// System.out.println("✅ Connected to Telegram Server"); +// handler = new ActionHandler(out, in, scanner); +// +// Thread listenerThread = new Thread(new IncomingMessageListener(in)); +// listenerThread.setDaemon(true); +// listenerThread.start(); +// +// showMainMenu(); +// +// } catch (IOException e) { +// System.err.println("❌ Error connecting to server: " + e.getMessage()); +// } +// } +// +// private void showMainMenu() throws IOException { +// while (true) { +// System.out.println("Main Menu:"); +// System.out.println("1. Register"); +// System.out.println("2. Login"); +// System.out.println("3. Exit"); +// System.out.print("Choose an option: "); +// String choice = scanner.nextLine(); +// +// switch (choice) { +// case "1" -> handler.register(); +// case "2" -> { +// handler.loginHandler(); +// if (Session.currentUser != null) { +// System.out.println("✅ Login successful."); +// new Thread(new ActionHandler.ChatStateMonitor(out)).start(); +//// new Thread(new ActionHandler.CurrentChatMenuRefresher(this.handler)).start(); +// +// +// UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); +// loggedInUserId = internalId; +// +// handler.userMenu(internalId); +// } else { +// System.out.println("❌ Login failed."); +// } +// } +// case "3" -> { +// System.out.println("Exiting..."); +// return; +// } +// default -> System.out.println("Invalid choice."); +// } +// } +// } +// +// public static void send(JSONObject req) { +// try { +// responseQueue.clear(); // optional: clear old responses +// getInstance().out.println(req.toString()); +// System.out.println("📤 [SEND] " + req.toString(2)); +// +// } catch (Exception e) { +// System.err.println("❌ Error sending request: " + e.getMessage()); +// } +// } +// +// +// +// +// public static Socket getSocket() { +// return socket; +// } +// +// public static void main(String[] args) { +// new TelegramClient().start(); +// } +// +// public PrintWriter getOut() { +// return out; +// } +// +// +//} + + + package org.to.telegramfinalproject.Client; import org.json.JSONObject; @@ -7,6 +133,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Scanner; import java.util.UUID; @@ -17,46 +144,79 @@ import java.util.concurrent.LinkedBlockingQueue; public class TelegramClient { private static final String SERVER_HOST = "localhost"; private static final int SERVER_PORT = 8000; + + private static TelegramClient instance; + private static Socket socket; private BufferedReader in; private PrintWriter out; + + private final Scanner scanner; private ActionHandler handler; - public static BlockingQueue responseQueue = new LinkedBlockingQueue<>(); - public static UUID loggedInUserId = null; + + public static final BlockingQueue responseQueue = new LinkedBlockingQueue<>(); public static final Map> pendingResponses = new ConcurrentHashMap<>(); + public static UUID loggedInUserId = null; - - private static TelegramClient instance; + private volatile boolean listenerStarted = false; public TelegramClient() { this.scanner = new Scanner(System.in); instance = this; } - public static TelegramClient getInstance() { + public static synchronized TelegramClient getInstance() { + if (instance == null) instance = new TelegramClient(); return instance; } - public void start() { + public void startConsole() { try { - socket = new Socket(SERVER_HOST, SERVER_PORT); - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out = new PrintWriter(socket.getOutputStream(), true); - System.out.println("✅ Connected to Telegram Server"); - handler = new ActionHandler(out, in, scanner); - - Thread listenerThread = new Thread(new IncomingMessageListener(in)); - listenerThread.setDaemon(true); - listenerThread.start(); - + connectIfNeeded(); + initHandlerIfNeeded(); + startListenerOnce(); showMainMenu(); - } catch (IOException e) { System.err.println("❌ Error connecting to server: " + e.getMessage()); } } + public void start() { startConsole(); } + + //UI only + public static synchronized TelegramClient getOrInitForUI() throws IOException { + TelegramClient cli = getInstance(); + cli.connectIfNeeded(); + cli.initHandlerIfNeeded(); + cli.startListenerOnce(); + return cli; + } + + private synchronized void connectIfNeeded() throws IOException { + if (socket != null && socket.isConnected() && !socket.isClosed()) return; + + socket = new Socket(SERVER_HOST, SERVER_PORT); + in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + out = new PrintWriter(socket.getOutputStream(), true); + System.out.println("✅ Connected to Telegram Server"); + } + + private synchronized void initHandlerIfNeeded() { + if (handler == null) { + handler = new ActionHandler(out, in, scanner); + } + } + + private void startListenerOnce() { + if (listenerStarted) return; + listenerStarted = true; + Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener"); + listenerThread.setDaemon(true); + listenerThread.start(); + } + + //console private void showMainMenu() throws IOException { while (true) { System.out.println("Main Menu:"); @@ -73,8 +233,6 @@ public class TelegramClient { if (Session.currentUser != null) { System.out.println("✅ Login successful."); new Thread(new ActionHandler.ChatStateMonitor(out)).start(); -// new Thread(new ActionHandler.CurrentChatMenuRefresher(this.handler)).start(); - UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); loggedInUserId = internalId; @@ -95,28 +253,21 @@ public class TelegramClient { public static void send(JSONObject req) { try { - responseQueue.clear(); // optional: clear old responses + responseQueue.clear(); getInstance().out.println(req.toString()); System.out.println("📤 [SEND] " + req.toString(2)); - } catch (Exception e) { System.err.println("❌ Error sending request: " + e.getMessage()); } } - - - - public static Socket getSocket() { - return socket; - } + public BufferedReader getIn() { return in; } + public PrintWriter getOut() { return out; } + public ActionHandler getHandler() { return handler; } + public static Socket getSocket() { return socket; } public static void main(String[] args) { - new TelegramClient().start(); + new TelegramClient().startConsole(); } - - public PrintWriter getOut() { - return out; - } - } + diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 1e6efb1..5085ed5 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1057,4 +1057,73 @@ public class MessageDatabase { } } + + public static Message getLastMessage(UUID targetId, String type) { + final String sql = + "SELECT * FROM messages " + + "WHERE receiver_type = ? AND receiver_id = ? " + + "ORDER BY send_at DESC LIMIT 1"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, type.toLowerCase()); // "private" | "group" | "channel" + ps.setObject(2, targetId); + + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? mapRow(rs) : null; + } + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + public static int getUnreadCount(UUID me, UUID targetId, String type) { + final String sql = + "SELECT COUNT(*) FROM messages " + + "WHERE receiver_type = ? AND receiver_id = ? " + + "AND sender_id <> ? " + + "AND (status IS NULL OR status <> 'SEEN')"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, type.toLowerCase()); + ps.setObject(2, targetId); + ps.setObject(3, me); + + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + + + } + + private static Message mapRow(ResultSet rs) throws SQLException { + Message m = new Message(); + m.setMessage_id((UUID) rs.getObject("message_id")); + m.setSender_id((UUID) rs.getObject("sender_id")); + m.setReceiver_id((UUID) rs.getObject("receiver_id")); + m.setReceiver_type(rs.getString("receiver_type")); + if ("private".equalsIgnoreCase(m.getReceiver_type())) { + m.setReceiver_id(m.getReceiver_id()); + } else { + m.setReceiver_id(null); + } + m.setMessage_type(rs.getString("message_type")); + m.setContent(rs.getString("content")); + Timestamp ts = rs.getTimestamp("send_at"); + if (ts != null) m.setSend_at(ts.toLocalDateTime()); + m.setIs_edited(rs.getBoolean("is_edited")); + m.setStatus(rs.getString("status")); + Object rtid = rs.getObject("reply_to_id"); + m.setReply_to_id(rtid == null ? null : (UUID) rtid); + return m; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 8f57ff8..9275b96 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -22,6 +22,17 @@ public class ChatEntry { private JSONObject permissions; + + + +// private UUID otherUserId; + private int unreadCount; + private String lastMessagePreview; + private String lastMessageType; // TEXT / IMAGE / AUDIO / VIDEO / ... + private UUID lastMessageSenderId; + + + public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime) { this.internalId = internalId; this.displayId = displayId; @@ -149,4 +160,17 @@ public class ChatEntry { public void setSavedMessages(boolean savedMessages) { this.savedMessages = savedMessages; } + + + public int getUnreadCount() { return unreadCount; } + public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; } + + public String getLastMessagePreview() { return lastMessagePreview; } + public void setLastMessagePreview(String lastMessagePreview) { this.lastMessagePreview = lastMessagePreview; } + + public String getLastMessageType() { return lastMessageType; } + public void setLastMessageType(String lastMessageType) { this.lastMessageType = lastMessageType; } + + public UUID getLastMessageSenderId() { return lastMessageSenderId; } + public void setLastMessageSenderId(UUID lastMessageSenderId) { this.lastMessageSenderId = lastMessageSenderId; } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index 7bb8791..63e270f 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -186,6 +186,12 @@ public class JsonUtil { obj.put("is_owner", entry.isOwner()); obj.put("is_admin", entry.isAdmin()); obj.put("is_saved_messages", entry.isSavedMessages()); + obj.put("unread_count", entry.getUnreadCount()); + obj.put("last_message_preview", entry.getLastMessagePreview()); + obj.put("last_message_type", entry.getLastMessageType()); + obj.put("last_message_sender_id", + entry.getLastMessageSenderId() != null ? entry.getLastMessageSenderId().toString() : JSONObject.NULL); + jsonArray.put(obj); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index cfbed22..27c9a15 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -125,6 +125,9 @@ public class Message { this.edited_at = editedAt; } + public Message() { + + } // ✅ Getters & Setters @@ -203,4 +206,6 @@ public class Message { return is_deleted_globally; } + public void receiver_id(UUID receiverId) {this.receiver_id = receiverId; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 06072e2..b5bb596 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -125,78 +125,6 @@ public class ClientHandler implements Runnable { } - -// for (Contact contact : contacts) { -// User target = userDatabase.findByInternalUUID(contact.getContact_id()); -// if (target == null) continue; -// LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(user.getInternal_uuid(), target.getInternal_uuid(), "private"); -// -//// chatList.add(new ChatEntry( -//// target.getInternal_uuid(), // internal UUID -//// target.getUser_id(), // public display ID -//// target.getProfile_name(), -//// target.getImage_url(), -//// "private", -//// last, -//// false, -//// false -//// )); -// -// UUID targetId = target.getInternal_uuid(); -// -// ChatEntry entry = new ChatEntry( -// targetId, -// target.getUser_id(), -// target.getProfile_name(), -// target.getImage_url(), -// "private", -// last, -// false, -// false -// ); -// -// -// -// } - - -// -// List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); -// for (PrivateChat chat : privateChats) { -// UUID otherId = chat.getUser1_id().equals(currentUser.getInternal_uuid()) ? -// chat.getUser2_id() : chat.getUser1_id(); -// -// User otherUser = userDatabase.findByInternalUUID(otherId); -// if (otherUser == null) continue; -// -// LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); -// -// -// ChatEntry entry = new ChatEntry( -// chat.getChat_id(), -// otherUser.getUser_id(), -// otherUser.getProfile_name(), -// otherUser.getImage_url(), -// "private", -// lastMessageTime, -// false, -// false -// ); -// entry.setOtherUserId(otherId); -// -// if (currentUser.getInternal_uuid() == otherId) { -// entry.setSavedMessages(true); -// } -// -// if (archivedChatIds.contains(chat.getChat_id())) { -// archivedChatList.add(entry); -// chatList.add(entry); -// } else { -// activeChatList.add(entry); -// chatList.add(entry); -// } -// } - List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); ChatEntry savedEntry = null; @@ -229,10 +157,12 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); if (isSelf) { - entry.setSavedMessages(true); // فلگ مهم - savedEntry = entry; // برای بردن به اول لیست + entry.setSavedMessages(true); + savedEntry = entry; } + enrichChatEntry(entry, user.getInternal_uuid()); + if (!isSelf && archivedChatIds.contains(chat.getChat_id())) { archivedChatList.add(entry); } else { @@ -270,6 +200,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, user.getInternal_uuid()); if (archivedChatIds.contains(group.getInternal_uuid())) { archivedChatList.add(entry); @@ -309,6 +240,8 @@ public class ClientHandler implements Runnable { isAdmin ); + enrichChatEntry(entry, user.getInternal_uuid()); + if (archivedChatIds.contains(channel.getInternal_uuid())) { archivedChatList.add(entry); chatList.add(entry); @@ -774,9 +707,9 @@ public class ClientHandler implements Runnable { ChatEntry entry = new ChatEntry( chat.getChat_id(), - otherUser.getUser_id(), - otherUser.getProfile_name(), - otherUser.getImage_url(), + isSavedMessages ? "Saved Messages" : otherUser.getUser_id(), + isSavedMessages ? "Saved Messages" : otherUser.getProfile_name(), + isSavedMessages ? null : otherUser.getImage_url(), "private", lastMessageTime, false, @@ -784,18 +717,20 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); + // Mark it as saved messages if it's the special self-chat if (isSavedMessages) { entry.setSavedMessages(true); } + enrichChatEntry(entry, currentUser.getInternal_uuid()); - if (archivedChatIds.contains(chat.getChat_id())) { + if (archivedChatIds.contains(chat.getChat_id()) && !isSavedMessages) { archivedChatList.add(entry); - chatList.add(entry); } else { activeChatList.add(entry); - chatList.add(entry); } + chatList.add(entry); + } @@ -826,6 +761,8 @@ public class ClientHandler implements Runnable { isAdmin ); + enrichChatEntry(entry, currentUser.getInternal_uuid()); + if (archivedChatIds.contains(group.getInternal_uuid())) { archivedChatList.add(entry); chatList.add(entry); @@ -863,6 +800,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, currentUser.getInternal_uuid()); if (archivedChatIds.contains(channel.getInternal_uuid())) { archivedChatList.add(entry); @@ -2855,4 +2793,38 @@ public class ClientHandler implements Runnable { + + private void enrichChatEntry(ChatEntry e, UUID me) { + // آخرین پیام + Message last = MessageDatabase.getLastMessage(e.getId(), e.getType()); + if (last != null) { + e.setLastMessageTime(String.valueOf(last.getSend_at())); + e.setLastMessagePreview(buildPreview(last)); + e.setLastMessageType(last.getMessage_type()); + e.setLastMessageSenderId(last.getSender_id()); + } + + // تعداد نخوانده‌ها + int unread = MessageDatabase.getUnreadCount(me, e.getId(), e.getType()); + e.setUnreadCount(unread); + } + + private String buildPreview(Message m) { + String t = m.getMessage_type(); + if ("TEXT".equalsIgnoreCase(t)) { + String c = m.getContent(); + if (c == null) return ""; + // پیش‌نمایش کوتاه + return c.length() > 120 ? c.substring(0, 120) + "…" : c; + } + switch (t) { + case "IMAGE": return "[Image]"; + case "AUDIO": return "[Audio]"; + case "VIDEO": return "[Video]"; + case "FILE": return "[File]"; + default: return "[Message]"; + } + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java b/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java new file mode 100644 index 0000000..4c8ae53 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java @@ -0,0 +1,37 @@ +// org.to.telegramfinalproject.UI.AppRouter +package org.to.telegramfinalproject.UI; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public final class AppRouter { + private static Stage stage; + private static Scene scene; + + private AppRouter() {} + + public static void init(Stage st, Scene sc) { + stage = st; + scene = sc; + } + + public static void showIntro() { setRoot("/org/to/telegramfinalproject/Fxml/intro.fxml"); } + public static void showLogin() { setRoot("/org/to/telegramfinalproject/Fxml/login_view.fxml"); } + public static void showRegister(){ setRoot("/org/to/telegramfinalproject/Fxml/register_view.fxml"); } + public static void showMain() { setRoot("/org/to/telegramfinalproject/Fxml/main.fxml"); } + + private static void setRoot(String fxmlPath) { + try { + System.out.println("Router: setRoot -> " + fxmlPath); + FXMLLoader fx = new FXMLLoader(AppRouter.class.getResource(fxmlPath)); + Parent root = fx.load(); + if (scene != null) scene.setRoot(root); + else if (stage != null) stage.setScene(new Scene(root, 1480, 820)); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index e1e4d84..286e59b 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -23,4 +23,11 @@ public class ChatItemController { unreadCount.setVisible(unread > 0); unreadCount.setText(String.valueOf(unread)); } + + public void setUnread(int unread) { + boolean show = unread > 0; + unreadCount.setVisible(show); + unreadCount.setManaged(show); + if (show) unreadCount.setText(String.valueOf(unread)); + } } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 6c0aa7c..f63b5b7 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -10,38 +10,62 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; import javafx.stage.FileChooser; +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Models.ChatEntry; +import org.json.JSONArray; +import org.json.JSONObject; import java.io.File; +import java.util.UUID; public class ChatPageController { // ===== messages area ===== - @FXML private VBox messageContainer; - @FXML private ScrollPane messageScrollPane; + @FXML + private VBox messageContainer; + @FXML + private ScrollPane messageScrollPane; // ===== input area ===== - @FXML private TextArea messageInput; - @FXML private Button sendButton; + @FXML + private TextArea messageInput; + @FXML + private Button sendButton; - @FXML private Button attachmentButton; - @FXML private ImageView attachmentIcon; // inside the attachment button + @FXML + private Button attachmentButton; + @FXML + private ImageView attachmentIcon; // inside the attachment button // ===== header ===== - @FXML private ImageView userAvatar; // 36x36 in the FXML - @FXML private Label chatTitle; // contact/group title - @FXML private Label chatStatus; // last seen / online + @FXML + private ImageView userAvatar; // 36x36 in the FXML + @FXML + private Label chatTitle; // contact/group title + @FXML + private Label chatStatus; // last seen / online - @FXML private Button searchInChatButton; // magnifier button - @FXML private ImageView searchIcon; + @FXML + private Button searchInChatButton; // magnifier button + @FXML + private ImageView searchIcon; - @FXML private Button moreButton; // 3-dots button - @FXML private ImageView moreIcon; - @FXML private ContextMenu moreMenu; - @FXML private MenuItem viewProfileItem; - @FXML private MenuItem deleteChatItem; + @FXML + private Button moreButton; // 3-dots button + @FXML + private ImageView moreIcon; + @FXML + private ContextMenu moreMenu; + @FXML + private MenuItem viewProfileItem; + @FXML + private MenuItem deleteChatItem; // ===== send icon ===== - @FXML private ImageView sendIcon; + @FXML + private ImageView sendIcon; // ===== state ===== private String chatName; @@ -49,9 +73,26 @@ public class ChatPageController { // Where your icons live private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; + private ChatEntry currentChat; + private UUID me; + + private void initCurrentUserId() { + try { + // از سشنی که سمت کلاینت داری: + String meStr = org.to.telegramfinalproject.Client.Session + .currentUser.getString("internal_uuid"); + me = UUID.fromString(meStr); + } catch (Exception ignore) { + me = null; // اگر به هر دلیلی نبود، خروجی‌ها رو ورودی فرض نکن + } + } + @FXML public void initialize() { + + initCurrentUserId(); + // Send button if (sendButton != null) { sendButton.setOnAction(e -> sendMessage()); @@ -162,7 +203,9 @@ public class ChatPageController { MainController.getInstance().showSearchPanel(); } - /** Called by main controller when opening a chat. */ + /** + * Called by main controller when opening a chat. + */ public void setChat(String chatName, String avatarPath) { this.chatName = chatName; @@ -210,14 +253,16 @@ public class ChatPageController { // ----- UI helpers ----- - /** Add a normal message bubble (very simple for now). */ + /** + * Add a normal message bubble (very simple for now). + */ public void addMessage(String sender, String content) { Label msg = new Label(sender + ": " + content); msg.setWrapText(true); boolean dark = themeManager.isDarkMode(); String bubbleColor = dark ? "#20405a" : "#4fa8f0"; - String textColor = dark ? "#e8f1f8" : "#0f141a"; + String textColor = dark ? "#e8f1f8" : "#0f141a"; msg.setStyle( "-fx-background-color: " + bubbleColor + ";" + "-fx-text-fill: " + textColor + ";" + @@ -235,7 +280,9 @@ public class ChatPageController { messageScrollPane.setVvalue(1.0); } - /** Update all header/footer icons according to current theme. */ + /** + * Update all header/footer icons according to current theme. + */ private void syncIconsWithTheme() { boolean dark = themeManager.isDarkMode(); // We use “_light” icons on dark backgrounds, and “_dark” on light backgrounds. @@ -274,4 +321,153 @@ public class ChatPageController { } return new Image(url.toExternalForm()); } + + public void showChat(ChatEntry entry) { + this.currentChat = entry; + + // Header + chatTitle.setText(entry.getName()); + chatStatus.setText(""); // اگر last seen داری اینجا بگذار + if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { + try { + userAvatar.setImage(new Image(entry.getImageUrl())); // یا لود از ریسورس خودت + userAvatar.setClip(new Circle(18, 18, 18)); + } catch (Exception ignored) { + } + } + + messageContainer.getChildren().clear(); + loadMessages(entry); + + // مارک به‌عنوان خوانده + markAsRead(entry); + + // فوکوس روی ورودی + Platform.runLater(() -> messageInput.requestFocus()); + } + + private void loadMessages(ChatEntry entry) { + JSONObject req = new JSONObject(); + req.put("action", "get_messages"); + req.put("receiver_id", String.valueOf(entry.getId())); + req.put("receiver_type", entry.getType()); + req.put("limit", 50); + + new Thread(() -> { + JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + if (resp == null) return; + + // بدون opt* : + String status = ""; + try { + status = resp.getString("status"); + } catch (Exception ignore) { + } + if (!"success".equals(status)) return; + + JSONObject data = null; + try { + data = resp.getJSONObject("data"); + } catch (Exception ignore) { + } + if (data == null) return; + + JSONArray arr = null; + try { + arr = data.getJSONArray("messages"); + } catch (Exception ignore) { + } + if (arr == null) return; + + JSONArray finalArr = arr; + Platform.runLater(() -> renderMessages(finalArr)); + }).start(); + } + + + private void renderMessages(JSONArray arr) { + messageContainer.getChildren().clear(); + for (int i = 0; i < arr.length(); i++) { + JSONObject m = arr.getJSONObject(i); + String senderId = m.optString("sender_id", ""); + String type = m.optString("message_type", "TEXT"); + String content = m.optString("content", ""); + + boolean outgoing = false; + if (me != null && senderId != null && !senderId.isEmpty()) { + try { + outgoing = me.equals(UUID.fromString(senderId)); + } catch (Exception ignore) { + } + } + + String text; + switch (type.toUpperCase()) { + case "TEXT": + text = content; + break; + case "IMAGE": + text = "[Image]"; + break; + case "AUDIO": + text = "[Audio]"; + break; + case "VIDEO": + text = "[Video]"; + break; + case "FILE": + text = "[File]"; + break; + default: + text = "[Message]"; + } + addBubble(outgoing, text); + } + messageScrollPane.layout(); + messageScrollPane.setVvalue(1.0); + } + + + private void markAsRead(ChatEntry entry) { + JSONObject readReq = new JSONObject(); + readReq.put("action", "mark_as_read"); + readReq.put("receiver_id", entry.getId().toString()); // ⛳️ internal_id + readReq.put("receiver_type", entry.getType()); + ActionHandler.sendWithResponse(readReq); + } + + private void addBubble(boolean outgoing, String content) { + Label msg = new Label(content); + msg.setWrapText(true); + + boolean dark = themeManager.isDarkMode(); + String mine = dark ? "#2b7cff" : "#d8ecff"; + String theirs = dark ? "#2c333a" : "#ffffff"; + String bg = outgoing ? mine : theirs; + + msg.setStyle( + "-fx-background-color:" + bg + ";" + + "-fx-padding:8 12;" + + "-fx-background-radius:12;" + + "-fx-max-width: 520;" + ); + msg.setMinHeight(Region.USE_PREF_SIZE); + + javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(msg); + row.setFillHeight(true); + row.setSpacing(6); + row.setAlignment(outgoing + ? javafx.geometry.Pos.CENTER_RIGHT + : javafx.geometry.Pos.CENTER_LEFT); + + messageContainer.getChildren().add(row); + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/IntroController.java b/src/main/java/org/to/telegramfinalproject/UI/IntroController.java index 25a5c93..3d25621 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/IntroController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/IntroController.java @@ -118,25 +118,34 @@ public class IntroController { slider.setDaemon(true); slider.start(); } +// +// @FXML +// private void handleStartMessaging(ActionEvent event) { +// try { +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/login_view.fxml")); +// Scene loginScene = new Scene(loader.load()); +// +// // Get the current stage and its dimensions +// Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); +// double currentWidth = stage.getWidth(); +// double currentHeight = stage.getHeight(); +// +// // Set the new scene and apply the previous size +// stage.setScene(loginScene); +// stage.setWidth(currentWidth); +// stage.setHeight(currentHeight); +// stage.show(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } @FXML private void handleStartMessaging(ActionEvent event) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/login_view.fxml")); - Scene loginScene = new Scene(loader.load()); - - // Get the current stage and its dimensions - Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); - double currentWidth = stage.getWidth(); - double currentHeight = stage.getHeight(); - - // Set the new scene and apply the previous size - stage.setScene(loginScene); - stage.setWidth(currentWidth); - stage.setHeight(currentHeight); - stage.show(); - } catch (IOException e) { - e.printStackTrace(); - } + AppRouter.showLogin(); // همون Scene می‌مونه، فقط Root عوض میشه } + + @FXML private void goRegister() { AppRouter.showRegister(); } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java index 4c35011..c1d12a3 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -9,7 +9,10 @@ import javafx.scene.control.*; import javafx.stage.Stage; import javafx.util.Duration; import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.ClientConnection; +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Client.TelegramClient; import org.to.telegramfinalproject.Database.userDatabase; import org.to.telegramfinalproject.Models.User; import org.to.telegramfinalproject.Security.PasswordHashing; @@ -23,7 +26,7 @@ public class LoginController { @FXML private PasswordField passwordField; @FXML private TextField visiblePasswordField; - @FXML private Button togglePasswordBtn; + @FXML private Button toggleVisibilityBtn; @FXML private Label errorLabel; private ClientConnection connection; @@ -48,60 +51,119 @@ public class LoginController { visiblePasswordField.setManaged(passwordVisible); passwordField.setVisible(!passwordVisible); passwordField.setManaged(!passwordVisible); - togglePasswordBtn.setText(passwordVisible ? "👁" : "👁"); + toggleVisibilityBtn.setText(passwordVisible ? "👁" : "👁"); } +// @FXML +// private void handleLogin() { +// String username = usernameField.getText(); +// String password = passwordField.getText(); +// +// // 1. Check for empty fields +// if (username.isEmpty() || password.isEmpty()) { +// showError("Please fill in all required fields."); +// return; +// } +// +// // 2. Check if username exists +// userDatabase userDb = new userDatabase(); +// if (!userDb.existsByUsername(username)) { +// showError("This username doesn’t exist."); +// return; +// } +// +// // 3. Check if password is correct +// User user = userDb.findByUsername(username); +// if (!PasswordHashing.verify(password, user.getPassword())) { +// showError("Incorrect password."); +// return; +// } +// +// // 4. Attempt to send to server +// try { +// JSONObject request = new JSONObject(); +// request.put("action", "login"); +// request.put("user_id", JSONObject.NULL); +// request.put("username", username); +// request.put("password", password); +// request.put("profile_name", JSONObject.NULL); +// +// if (connection != null) { +// connection.send(request.toString()); +// String responseStr = connection.receive(); +// JSONObject response = new JSONObject(responseStr); +// System.out.println("Status: " + response.getString("status")); +// System.out.println("Message: " + response.getString("message")); +// +// // Simulate successful login (since main.fxml isn’t ready) +// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!"); +// alert.show(); +// } +// +// } catch (Exception ex) { +// showError("Unable to connect to server. Please try again later."); +// } +// } + + + + // LoginController.java (متد اصلی) @FXML private void handleLogin() { - String username = usernameField.getText(); - String password = passwordField.getText(); + String u = usernameField.getText().trim(); + String p = passwordField.getText(); + if (u.isEmpty() || p.isEmpty()) { showError("Please fill in all required fields."); return; } - // 1. Check for empty fields - if (username.isEmpty() || password.isEmpty()) { - showError("Please fill in all required fields."); - return; - } + setUiBusy(true); - // 2. Check if username exists - userDatabase userDb = new userDatabase(); - if (!userDb.existsByUsername(username)) { - showError("This username doesn’t exist."); - return; - } + new Thread(() -> { + try { + var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + var handler = cli.getHandler(); - // 3. Check if password is correct - User user = userDb.findByUsername(username); - if (!PasswordHashing.verify(password, user.getPassword())) { - showError("Incorrect password."); - return; - } + // بساز و بفرست — همون send خودت که Session رو پر می‌کند +// org.json.JSONObject req = new org.json.JSONObject() +// .put("action","login") +// .put("username", u) +// .put("password", p); +// +// handler.send(req); // ⬅️ بلاکینگ؛ پس درستش کردیم که تو Thread هست - // 4. Attempt to send to server - try { - JSONObject request = new JSONObject(); - request.put("action", "login"); - request.put("user_id", JSONObject.NULL); - request.put("username", username); - request.put("password", password); - request.put("profile_name", JSONObject.NULL); - if (connection != null) { - connection.send(request.toString()); - String responseStr = connection.receive(); - JSONObject response = new JSONObject(responseStr); - System.out.println("Status: " + response.getString("status")); - System.out.println("Message: " + response.getString("message")); + handler.login(u,p); - // Simulate successful login (since main.fxml isn’t ready) - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!"); - alert.show(); + javafx.application.Platform.runLater(() -> { + setUiBusy(false); + + if (!handler.wasSuccess() || Session.currentUser == null) { + showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage()); + return; + } + + // موفق: Session از داخل send پر شده + goMain(); // بدون Alert → مستقیم به main.fxml + }); + + } catch (Exception ex) { + javafx.application.Platform.runLater(() -> { + setUiBusy(false); + showError("Connection error."); + }); } - - } catch (Exception ex) { - showError("Unable to connect to server. Please try again later."); - } + }, "login-thread").start(); } + private void goMain() { + AppRouter.showMain(); + } + + + private void setUiBusy(boolean b) { + usernameField.setDisable(b); + passwordField.setDisable(b); + } + + @FXML private void switchToRegister() throws IOException { switchScene("register_view.fxml"); diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 12bfc5d..18e3422 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -13,8 +13,15 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.to.telegramfinalproject.Models.ChatEntry; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; public class MainController { @@ -57,10 +64,14 @@ public class MainController { public static MainController getInstance() { return instance; } + private final Map itemControllers = new HashMap<>(); + @FXML public void initialize() { - addSampleChats(); +// addSampleChats(); + populateChatListFromSession(); + // Register the scene for automatic CSS updates Platform.runLater(() -> { @@ -138,58 +149,155 @@ public class MainController { scrollPane.setManaged(true); } - private void addSampleChats() { - addChat("Archived Chats", "Your archived chats", "10:45", 0); - addChat("Saved Messages", "Keep messages for later", "Yesterday", 0); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// private void addSampleChats() { +// addChat("Archived Chats", "Your archived chats", "10:45", 0); +// addChat("Saved Messages", "Keep messages for later", "Yesterday", 0); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// +// +// } + + + private final DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm"); + private final DateTimeFormatter dayFmt = DateTimeFormatter.ofPattern("dd/MM"); // همون سال + private final DateTimeFormatter fullDateFmt= DateTimeFormatter.ofPattern("yyyy/MM/dd"); // سال متفاوت + + private String formatListTimestamp(LocalDateTime ts) { + if (ts == null) return ""; + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + // امروز → فقط ساعت + return timeFmt.format(ts); + } + // اگر همان سال است → تاریخ کوتاه + ساعت + if (d.getYear() == today.getYear()) { + return dayFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 15/08 13:28 + } + // سال متفاوت → تاریخ کامل + ساعت + return fullDateFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 2024/12/31 21:10 } - private void addChat(String name, String lastMsg, String time, int unread) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); - Node chatItem = loader.load(); - ChatItemController controller = loader.getController(); - controller.setChatData(name, lastMsg, time, unread); - chatItem.setOnMouseClicked(e -> openChat(name)); - chatListContainer.getChildren().add(chatItem); - } catch (IOException e) { - e.printStackTrace(); + + private void populateChatListFromSession() { + chatListContainer.getChildren().clear(); + + var list = (org.to.telegramfinalproject.Client.Session.activeChats != null + && !org.to.telegramfinalproject.Client.Session.activeChats.isEmpty()) + ? org.to.telegramfinalproject.Client.Session.activeChats + : org.to.telegramfinalproject.Client.Session.chatList; + + if (list == null || list.isEmpty()) return; + + for (ChatEntry c : list) { + addChatNode(c); } } - private void openChat(String chatName) { + +// 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 lastPreview = ""; +// String time = (chat.getLastMessageTime() != null) +// ? timeFmt.format(chat.getLastMessageTime()) : ""; +// +// cc.setChatData(chat.getName(), lastPreview, time, 0); +// +// item.setOnMouseClicked(e -> openChat(String.valueOf(chat))); +// chatListContainer.getChildren().add(item); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + 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 time = formatListTimestamp(chat.getLastMessageTime()); + + cc.setChatData(chat.getName(), preview, time, chat.getUnreadCount()); + 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]"; + case "AUDIO": return "[Audio]"; + case "VIDEO": return "[Video]"; + case "FILE": return "[File]"; + default: return "[Message]"; + } + } + + +// private void addChat(String name, String lastMsg, String time, int unread) { +// try { +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node chatItem = loader.load(); +// ChatItemController controller = loader.getController(); +// controller.setChatData(name, lastMsg, time, unread); +// +// chatItem.setOnMouseClicked(e -> openChat(name)); +// chatListContainer.getChildren().add(chatItem); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + + private void openChat(ChatEntry chat) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); Node chatPage = loader.load(); ChatPageController controller = loader.getController(); - controller.setChat("Alice", "/org/to/telegramfinalproject/Avatars/profile_test.png"); + controller.showChat(chat); // ✅ متد جدید در ChatPageController + chatDisplayArea.getChildren().setAll(chatPage); - chatDisplayArea.getChildren().clear(); - chatDisplayArea.getChildren().add(chatPage); + // اختیاری: صفر کردن badge و مارک‌کردن به‌عنوان خوانده در UI + ChatItemController item = itemControllers.get(chat.getId()); + if (item != null) item.setUnread(0); - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); } } + @FXML private void toggleSidebar() { if (isSidebarOpen) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 2d08572..6cfb82a 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.UI; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.Image; @@ -12,12 +13,16 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Duration; + import java.net.URL; public class SidebarMenuController { private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/"; + + + @FXML private VBox sidebarRoot; @FXML private ImageView profileImage; @FXML private Label usernameLabel; @@ -176,4 +181,8 @@ public class SidebarMenuController { private void openSettings() { System.out.println("Opening Settings..."); } private void openTelegramFeatures() { System.out.println("Opening Telegram Features..."); } private void openTelegramQnA() { System.out.println("Opening Telegram Q&A..."); } + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java index 3a1dea3..6374ef5 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java +++ b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java @@ -1,4 +1,36 @@ -package org.to.telegramfinalproject.UI; +package org.to.telegramfinalproject.UI;//package org.to.telegramfinalproject.UI; +// +// +//import javafx.application.Application; +//import javafx.fxml.FXMLLoader; +//import javafx.scene.Scene; +//import javafx.scene.image.Image; +//import javafx.stage.Stage; +// +//import java.io.IOException; +// +//public class TelegramApplication extends Application { +// @Override +// public void start(Stage stage) throws IOException { +// FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/main.fxml")); +// Scene scene = new Scene(fxmlLoader.load(), 1480, 820); +// +// scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm()); +// +// stage.setTitle("Telegram"); +// stage.setScene(scene); +// +// // Add icon to the stage +// Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png")); +// stage.getIcons().add(icon); +// +// stage.show(); +// } +// public static void main(String[] args) { +// launch(); +// } +// +//} import javafx.application.Application; @@ -6,28 +38,31 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import org.to.telegramfinalproject.UI.AppRouter; import java.io.IOException; public class TelegramApplication extends Application { @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/main.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 1480, 820); + // اول intro.fxml + FXMLLoader fx = new FXMLLoader( + TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/intro.fxml")); + Scene scene = new Scene(fx.load(), 1480, 820); - scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm()); + scene.getStylesheets().add( + getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm() + ); stage.setTitle("Telegram"); + stage.getIcons().add(new Image( + TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png") + )); stage.setScene(scene); - - // Add icon to the stage - Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png")); - stage.getIcons().add(icon); - stage.show(); - } - public static void main(String[] args) { - launch(); + + AppRouter.init(stage, scene); } -} \ No newline at end of file + public static void main(String[] args) { launch(); } +} From 9d1737a90f2c8a865f6067ac3f9590d699e8fb2d Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 28 Aug 2025 23:27:03 +0330 Subject: [PATCH 02/19] Time Formatter --- .../UI/ChatPageController.java | 125 ++++++++++++------ .../UI/MainController.java | 78 ++++++++--- .../UI/SidebarMenuController.java | 31 +++++ 3 files changed, 171 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index f63b5b7..54e05e9 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -13,11 +13,16 @@ import javafx.stage.FileChooser; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Models.ChatEntry; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.UUID; public class ChatPageController { @@ -78,15 +83,47 @@ public class ChatPageController { private void initCurrentUserId() { try { - // از سشنی که سمت کلاینت داری: String meStr = org.to.telegramfinalproject.Client.Session .currentUser.getString("internal_uuid"); me = UUID.fromString(meStr); } catch (Exception ignore) { - me = null; // اگر به هر دلیلی نبود، خروجی‌ها رو ورودی فرض نکن + me = null; } } + //Time formatter for messages + + private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); + private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; + + private String formatWhen(LocalDateTime ts) { + if (ts == null) return ""; + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + return FMT_HHMM.format(ts); + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); + } else { + return FMT_DATE_TIME.format(ts); + } + } + + /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ + private LocalDateTime parseWhen(String iso) { + if (iso == null || iso.isEmpty()) return null; + try { + return OffsetDateTime.parse(iso).toLocalDateTime(); + } catch (Exception ignore) { + try { + return LocalDateTime.parse(iso); + } catch (Exception e) { + return null; + } + } + } @FXML public void initialize() { @@ -327,10 +364,10 @@ public class ChatPageController { // Header chatTitle.setText(entry.getName()); - chatStatus.setText(""); // اگر last seen داری اینجا بگذار + chatStatus.setText(""); if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { try { - userAvatar.setImage(new Image(entry.getImageUrl())); // یا لود از ریسورس خودت + userAvatar.setImage(new Image(entry.getImageUrl())); userAvatar.setClip(new Circle(18, 18, 18)); } catch (Exception ignored) { } @@ -339,10 +376,8 @@ public class ChatPageController { messageContainer.getChildren().clear(); loadMessages(entry); - // مارک به‌عنوان خوانده markAsRead(entry); - // فوکوس روی ورودی Platform.runLater(() -> messageInput.requestFocus()); } @@ -363,7 +398,6 @@ public class ChatPageController { } if (resp == null) return; - // بدون opt* : String status = ""; try { status = resp.getString("status"); @@ -391,49 +425,48 @@ public class ChatPageController { } - private void renderMessages(JSONArray arr) { + private void renderMessages(org.json.JSONArray arr) { messageContainer.getChildren().clear(); - for (int i = 0; i < arr.length(); i++) { - JSONObject m = arr.getJSONObject(i); - String senderId = m.optString("sender_id", ""); - String type = m.optString("message_type", "TEXT"); - String content = m.optString("content", ""); - boolean outgoing = false; - if (me != null && senderId != null && !senderId.isEmpty()) { - try { - outgoing = me.equals(UUID.fromString(senderId)); - } catch (Exception ignore) { - } - } + String myId = Session.currentUser != null + ? Session.currentUser.optString("internal_uuid", "") + : ""; + + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject m = arr.getJSONObject(i); + + String senderId = m.has("sender_id") && !m.isNull("sender_id") ? m.getString("sender_id") : ""; + String senderName = m.has("sender_name") && !m.isNull("sender_name") ? m.getString("sender_name") : ""; + String type = m.has("message_type") && !m.isNull("message_type") ? m.getString("message_type") : "TEXT"; + String content = m.has("content") && !m.isNull("content") ? m.getString("content") : ""; + String whenStr = m.has("send_at") && !m.isNull("send_at") ? m.getString("send_at") : null; + + boolean outgoing = senderId.equalsIgnoreCase(myId); + + String display = outgoing ? "You" + : (!senderName.isEmpty() ? senderName + : (senderId.isEmpty() ? "Unknown" + : senderId.substring(0, Math.min(8, senderId.length())))); String text; switch (type.toUpperCase()) { - case "TEXT": - text = content; - break; - case "IMAGE": - text = "[Image]"; - break; - case "AUDIO": - text = "[Audio]"; - break; - case "VIDEO": - text = "[Video]"; - break; - case "FILE": - text = "[File]"; - break; - default: - text = "[Message]"; + case "IMAGE": text = "[Image]"; break; + case "AUDIO": text = "[Audio]"; break; + case "VIDEO": text = "[Video]"; break; + case "FILE": text = "[File]"; break; + default: text = content; break; } - addBubble(outgoing, text); + + LocalDateTime ts = parseWhen(whenStr); + addBubble(outgoing, display, text, ts); } + messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } + private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); @@ -442,14 +475,19 @@ public class ChatPageController { ActionHandler.sendWithResponse(readReq); } - private void addBubble(boolean outgoing, String content) { + private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { + + Label meta = new Label(displayName + " • " + formatWhen(sentAt)); + meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); + meta.setWrapText(true); + Label msg = new Label(content); msg.setWrapText(true); boolean dark = themeManager.isDarkMode(); - String mine = dark ? "#2b7cff" : "#d8ecff"; + String mine = dark ? "#2b7cff" : "#d8ecff"; String theirs = dark ? "#2c333a" : "#ffffff"; - String bg = outgoing ? mine : theirs; + String bg = outgoing ? mine : theirs; msg.setStyle( "-fx-background-color:" + bg + ";" + @@ -459,7 +497,9 @@ public class ChatPageController { ); msg.setMinHeight(Region.USE_PREF_SIZE); - javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(msg); + VBox bubble = new VBox(4, meta, msg); + + javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); row.setFillHeight(true); row.setSpacing(6); row.setAlignment(outgoing @@ -470,4 +510,5 @@ public class ChatPageController { } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 18e3422..65b7ea5 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -48,6 +48,8 @@ public class MainController { // === Sidebar === @FXML private Pane overlay; @FXML private ImageView menuIcon; + private SidebarMenuController sidebarController; //save sidebar controller + // === STATE === private static MainController instance; @@ -173,29 +175,32 @@ public class MainController { // } - private final DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm"); - private final DateTimeFormatter dayFmt = DateTimeFormatter.ofPattern("dd/MM"); // همون سال - private final DateTimeFormatter fullDateFmt= DateTimeFormatter.ofPattern("yyyy/MM/dd"); // سال متفاوت + private static final java.time.format.DateTimeFormatter FMT_HHMM = + java.time.format.DateTimeFormatter.ofPattern("HH:mm"); + private static final java.time.format.DateTimeFormatter FMT_DATE_TIME = + java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; - private String formatListTimestamp(LocalDateTime ts) { + // ... + private String formatChatTime(java.time.LocalDateTime ts) { if (ts == null) return ""; - LocalDate today = LocalDate.now(); - LocalDate d = ts.toLocalDate(); + var today = java.time.LocalDate.now(); + var d = ts.toLocalDate(); if (d.isEqual(today)) { - // امروز → فقط ساعت - return timeFmt.format(ts); + //today + return FMT_HHMM.format(ts); + //yesterday + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL; + } else { + return FMT_DATE_TIME.format(ts); } - // اگر همان سال است → تاریخ کوتاه + ساعت - if (d.getYear() == today.getYear()) { - return dayFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 15/08 13:28 - } - // سال متفاوت → تاریخ کامل + ساعت - return fullDateFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 2024/12/31 21:10 } + private void populateChatListFromSession() { chatListContainer.getChildren().clear(); @@ -239,9 +244,9 @@ public class MainController { ChatItemController cc = fx.getController(); String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); - String time = formatListTimestamp(chat.getLastMessageTime()); + String timeText = formatChatTime(chat.getLastMessageTime()); - cc.setChatData(chat.getName(), preview, time, chat.getUnreadCount()); + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount()); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); @@ -284,11 +289,10 @@ public class MainController { Node chatPage = loader.load(); ChatPageController controller = loader.getController(); - controller.showChat(chat); // ✅ متد جدید در ChatPageController + controller.showChat(chat); chatDisplayArea.getChildren().setAll(chatPage); - // اختیاری: صفر کردن badge و مارک‌کردن به‌عنوان خوانده در UI ChatItemController item = itemControllers.get(chat.getId()); if (item != null) item.setUnread(0); @@ -307,30 +311,62 @@ public class MainController { } } +// @FXML +// private void openSidebar() { +// try { +// if (sidebarRoot == null) { +// sidebarRoot = FXMLLoader.load(getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); +// StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT); +// sidebarRoot.setTranslateX(-getSidebarWidth()); +// mainRoot.getChildren().add(sidebarRoot); +// } +// +// TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot); +// slideIn.setToX(0); +// slideIn.play(); +// +// overlay.setVisible(true); +// overlay.setMouseTransparent(false); +// +// isSidebarOpen = true; +// +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + @FXML private void openSidebar() { try { if (sidebarRoot == null) { - sidebarRoot = FXMLLoader.load(getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); + sidebarRoot = loader.load(); + sidebarController = loader.getController(); + StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT); sidebarRoot.setTranslateX(-getSidebarWidth()); mainRoot.getChildren().add(sidebarRoot); } + if (sidebarController != null && org.to.telegramfinalproject.Client.Session.currentUser != null) { + sidebarController.setUserFromSession( + org.to.telegramfinalproject.Client.Session.currentUser); + } + TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot); slideIn.setToX(0); slideIn.play(); overlay.setVisible(true); overlay.setMouseTransparent(false); - isSidebarOpen = true; - } catch (IOException e) { e.printStackTrace(); } } + @FXML private void closeSidebar() { if (sidebarRoot != null && isSidebarOpen) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 6cfb82a..5c63f81 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -12,6 +12,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.json.JSONObject; import java.net.URL; @@ -184,5 +185,35 @@ public class SidebarMenuController { + public void setUserFromSession(JSONObject user) { + if (user == null) return; + + // نام نمایشی (اول profile_name بعد username) + String displayName = user.optString("profile_name", + user.optString("username", "")); + usernameLabel.setText(displayName); + + // عکس پروفایل: هم URL اینترنتی هم مسیر ریسورس را پشتیبانی کن + String img = user.optString("image_url", ""); + Image pic = tryLoadImage(img); + if (pic == null) { + pic = loadImage("/org/to/telegramfinalproject/Avatars/profile.png"); + } + if (pic != null) profileImage.setImage(pic); + } + + // کمک‌کننده: هم URL و هم ریسورس کلاس‌پث را امتحان می‌کند + private Image tryLoadImage(String src) { + if (src == null || src.isBlank()) return null; + try { + // اگر ریسورس داخل پروژه است (با / شروع شود یا در resources موجود باشد) + var res = getClass().getResource(src); + if (res != null) return new Image(res.toExternalForm(), true); + // در غیر این صورت فرض کن URL است (http/https/file) + return new Image(src, true); + } catch (Exception ignored) { + return null; + } + } } From c5164592e9e4305e9fa341e7f682bd4015fad662 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 18:09:30 +0330 Subject: [PATCH 03/19] Merge with Main_UI --- .../java/org/to/telegramfinalproject/UI/MainController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 9f6e80b..42065c0 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -248,9 +248,8 @@ public class MainController { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); Node chatItem = loader.load(); ChatItemController controller = loader.getController(); - controller.setChatData(name, lastMsg, time, unread, "/org/to/telegramfinalproject/Avatars/default_profile.png"); - cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount()); + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); From f538e480eda2e769697699898e24ba1b6518d3f4 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 20:00:25 +0330 Subject: [PATCH 04/19] Connection --- .../UI/ChatPageController.java | 290 ++++++++++++++++-- .../UI/LoginController.java | 1 - .../to/telegramfinalproject/UI/LoginForm.java | 107 ------- .../telegramfinalproject/UI/RegisterForm.java | 110 ------- 4 files changed, 260 insertions(+), 248 deletions(-) delete mode 100644 src/main/java/org/to/telegramfinalproject/UI/LoginForm.java delete mode 100644 src/main/java/org/to/telegramfinalproject/UI/RegisterForm.java diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 54e05e9..5157ebb 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -97,6 +97,17 @@ public class ChatPageController { private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); private static final String YESTERDAY_LABEL = "Yesterday"; + private static String str(org.json.JSONObject j, String k) { + try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; } + } + private static boolean bool(org.json.JSONObject j, String k) { + try { return (j.has(k) && !j.isNull(k)) && j.getBoolean(k); } catch (Exception e) { return false; } + } + private static org.json.JSONArray arr(org.json.JSONObject j, String k) { + try { return (j.has(k) && !j.isNull(k)) ? j.getJSONArray(k) : null; } catch (Exception e) { return null; } + } + + private String formatWhen(LocalDateTime ts) { if (ts == null) return ""; LocalDate today = LocalDate.now(); @@ -425,46 +436,101 @@ public class ChatPageController { } - private void renderMessages(org.json.JSONArray arr) { +// private void renderMessages(org.json.JSONArray arr) { +// messageContainer.getChildren().clear(); +// +// String myId = Session.currentUser != null +// ? Session.currentUser.optString("internal_uuid", "") +// : ""; +// +// for (int i = 0; i < arr.length(); i++) { +// org.json.JSONObject m = arr.getJSONObject(i); +// +// String senderId = m.has("sender_id") && !m.isNull("sender_id") ? m.getString("sender_id") : ""; +// String senderName = m.has("sender_name") && !m.isNull("sender_name") ? m.getString("sender_name") : ""; +// String type = m.has("message_type") && !m.isNull("message_type") ? m.getString("message_type") : "TEXT"; +// String content = m.has("content") && !m.isNull("content") ? m.getString("content") : ""; +// String whenStr = m.has("send_at") && !m.isNull("send_at") ? m.getString("send_at") : null; +// +// boolean outgoing = senderId.equalsIgnoreCase(myId); +// +// String display = outgoing ? "You" +// : (!senderName.isEmpty() ? senderName +// : (senderId.isEmpty() ? "Unknown" +// : senderId.substring(0, Math.min(8, senderId.length())))); +// +// String text; +// switch (type.toUpperCase()) { +// case "IMAGE": text = "[Image]"; break; +// case "AUDIO": text = "[Audio]"; break; +// case "VIDEO": text = "[Video]"; break; +// case "FILE": text = "[File]"; break; +// default: text = content; break; +// } +// +// LocalDateTime ts = parseWhen(whenStr); +// addBubble(outgoing, display, text, ts); +// } +// +// messageScrollPane.layout(); +// messageScrollPane.setVvalue(1.0); +// } + + + private final java.util.Map msgIndex = new java.util.HashMap<>(); + + private void renderMessages(org.json.JSONArray list) { messageContainer.getChildren().clear(); - String myId = Session.currentUser != null - ? Session.currentUser.optString("internal_uuid", "") - : ""; + msgIndex.clear(); + for (int i=0; i showing bracket for msgId=" + msgId); } - LocalDateTime ts = parseWhen(whenStr); - addBubble(outgoing, display, text, ts); + + addBubble(outgoing, senderName, type, content, ts, msgId, + fwdFrom, fwdBy, replyToId, edited, reactions); } messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } + private String shortId(String id){ + return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); + } + private void markAsRead(ChatEntry entry) { @@ -475,13 +541,73 @@ public class ChatPageController { ActionHandler.sendWithResponse(readReq); } - private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { +// private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { +// +// Label meta = new Label(displayName + " • " + formatWhen(sentAt)); +// meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); +// meta.setWrapText(true); +// +// Label msg = new Label(content); +// msg.setWrapText(true); +// +// boolean dark = themeManager.isDarkMode(); +// String mine = dark ? "#2b7cff" : "#d8ecff"; +// String theirs = dark ? "#2c333a" : "#ffffff"; +// String bg = outgoing ? mine : theirs; +// +// msg.setStyle( +// "-fx-background-color:" + bg + ";" + +// "-fx-padding:8 12;" + +// "-fx-background-radius:12;" + +// "-fx-max-width: 520;" +// ); +// msg.setMinHeight(Region.USE_PREF_SIZE); +// +// VBox bubble = new VBox(4, meta, msg); +// +// javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); +// row.setFillHeight(true); +// row.setSpacing(6); +// row.setAlignment(outgoing +// ? javafx.geometry.Pos.CENTER_RIGHT +// : javafx.geometry.Pos.CENTER_LEFT); +// +// messageContainer.getChildren().add(row); +// } - Label meta = new Label(displayName + " • " + formatWhen(sentAt)); + + + + + private void addBubble( + boolean outgoing, + String displayName, + String type, + String content, + java.time.LocalDateTime sentAt, + String messageId, + String forwardedFrom, + String forwardedBy, + String replyToId, + 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); - Label msg = new Label(content); + // --- نرمال‌سازی نوع پیام + fallback --- + String t = (type == null) ? "" : type.trim().toUpperCase(); + // اگر type نامعتبر/خالی بود ولی content متن داشت، فرض کن TEXT است. + boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); + + String bodyText = isText ? (content == null ? "" : content) + : bracketLabel(t); // [Image] / [Audio] / ... + + Label msg = new Label(bodyText); msg.setWrapText(true); boolean dark = themeManager.isDarkMode(); @@ -497,18 +623,122 @@ public class ChatPageController { ); msg.setMinHeight(Region.USE_PREF_SIZE); - VBox bubble = new VBox(4, meta, msg); + VBox bubble = new VBox(6); + bubble.getChildren().add(meta); + + if (notBlank(forwardedFrom) || notBlank(forwardedBy)) { + bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); + } + if (notBlank(replyToId)) { + bubble.getChildren().add(buildReplyBoxFromIndex(replyToId)); + } + + bubble.getChildren().add(msg); + + if (reactions != null && reactions.length() > 0) { + bubble.getChildren().add(buildReactionsBarFromJson(reactions, dark)); + } javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); row.setFillHeight(true); row.setSpacing(6); - row.setAlignment(outgoing - ? javafx.geometry.Pos.CENTER_RIGHT + row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT : javafx.geometry.Pos.CENTER_LEFT); messageContainer.getChildren().add(row); } + private String bracketLabel(String t) { + String tt = (t == null) ? "" : t.trim().toUpperCase(); + switch (tt) { + case "IMAGE": return "[Image]"; + case "AUDIO": return "[Audio]"; + case "VIDEO": return "[Video]"; + case "FILE": return "[File]"; + default: return "[Message]"; + } + } + + + private javafx.scene.Node buildForwardHeader(String forwardedFrom, String forwardedBy) { + String txt; + if (notBlank(forwardedFrom) && notBlank(forwardedBy)) { + txt = "Forwarded from " + forwardedFrom + " by " + forwardedBy; + } else if (notBlank(forwardedFrom)) { + txt = "Forwarded from " + forwardedFrom; + } else if (notBlank(forwardedBy)) { + txt = "Forwarded by " + forwardedBy; + } else { + txt = "Forwarded"; + } + Label l = new Label(txt); + l.setStyle("-fx-font-size: 11; -fx-text-fill: #5b8bb1;"); + l.setWrapText(true); + return l; + } + + private javafx.scene.Node buildReplyBoxFromIndex(String replyToId) { + org.json.JSONObject r = msgIndex.get(replyToId); + String from = "Unknown"; + String preview; + + if (r != null) { + String rType = r.optString("message_type", "TEXT"); + String rContent = r.optString("content", ""); + String rSender = r.optString("sender_name", ""); + if (!rSender.isEmpty()) from = rSender; + + preview = "TEXT".equalsIgnoreCase(rType) ? rContent : bracketLabel(rType); + } else { + preview = "[Quoted message]"; + } + + Label t = new Label("Reply to " + from); + t.setStyle("-fx-font-size: 11; -fx-text-fill: #5b8bb1; -fx-font-weight: bold;"); + + Label p = new Label(ellipsize(preview == null ? "" : preview, 140)); + p.setWrapText(true); + p.setStyle("-fx-font-size: 12; -fx-text-fill: #6b7c8a;"); + + VBox box = new VBox(2, t, p); + box.setStyle( + "-fx-background-color: rgba(125,150,175,0.10);" + + "-fx-padding:6 8; -fx-background-radius:8;" + + "-fx-border-color:#7da0b8; -fx-border-width:0 0 0 2;" + ); + return box; + } + + private javafx.scene.Node buildReactionsBarFromJson(org.json.JSONArray arr, boolean dark) { + javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(6); + bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject ro = arr.optJSONObject(i); + if (ro == null) continue; + String emoji = ro.optString("emoji", "👍"); + int count = ro.optInt("count", 1); + boolean byMe = ro.optBoolean("by_me", false); + + String chipBg = byMe ? (dark ? "#215a9f" : "#d1e8ff") + : (dark ? "#2f3942" : "#eef3f7"); + String text = emoji + (count > 0 ? (" " + count) : ""); + Label chip = new Label(text); + chip.setStyle( + "-fx-font-size: 12;" + + "-fx-background-radius: 12;" + + "-fx-padding: 2 8;" + + "-fx-background-color: " + chipBg + ";" + ); + bar.getChildren().add(chip); + } + return bar; + } + + + 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; } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java index c1d12a3..c72d699 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -107,7 +107,6 @@ public class LoginController { - // LoginController.java (متد اصلی) @FXML private void handleLogin() { String u = usernameField.getText().trim(); diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java b/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java deleted file mode 100644 index 2b6b929..0000000 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.to.telegramfinalproject.UI; - - -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.PasswordField; -import javafx.scene.control.TextField; -import javafx.stage.Stage; -import org.json.JSONObject; -import org.to.telegramfinalproject.Client.ClientConnection; -import org.to.telegramfinalproject.Database.userDatabase; -import org.to.telegramfinalproject.Models.User; -import org.to.telegramfinalproject.Security.PasswordHashing; - -import java.io.IOException; - -public class LoginForm { - - @FXML - private Button loginButton; - @FXML - private Button backButton; - @FXML private TextField usernameField; - @FXML private PasswordField passwordField; - - private ClientConnection connection; - - @FXML - public void initialize() { - - try { - connection = new ClientConnection("localhost", 8000); - } catch (Exception e) { - System.out.println("Could not connect to server: " + e.getMessage()); - } - - loginButton.setOnAction(e -> { - String username = usernameField.getText(); - String password = passwordField.getText(); - - JSONObject request = new JSONObject(); - request.put("action", "login"); - request.put("user_id", JSONObject.NULL); - request.put("username", username); - request.put("password", password); - request.put("profile_name", JSONObject.NULL); - if (connection!=null) { - connection.send(request.toString()); - } - userDatabase userDb = new userDatabase(); - User user = userDb.findByUsername(username); - - if(!userDb.existsByUsername(username)){ - Alert alert = new Alert(Alert.AlertType.ERROR, "Invalid username"); - alert.show(); - } - else if(!PasswordHashing.verify(password,user.getPassword())){ - Alert alert = new Alert(Alert.AlertType.ERROR, "Invalid password"); - alert.show(); - } - else if(!PasswordHashing.verify(password,user.getPassword()) && !userDb.existsByUsername(username)){ - Alert alert = new Alert(Alert.AlertType.ERROR, "Invalid password and username"); - alert.show(); - } - else{ - try { - String responseStr = connection.receive(); - JSONObject response = new JSONObject(responseStr); - System.out.println("Status: " + response.getString("status")); - System.out.println("Message: " + response.getString("message")); - Alert alert = new Alert(Alert.AlertType.INFORMATION, " Message: " + response.getString("message")); - alert.show(); - } catch (Exception ex) { - Alert alert = new Alert(Alert.AlertType.ERROR, "Error receiving response: " + ex.getMessage()); - alert.show(); - } - } - }); - - backButton.setOnAction(e -> { - switchScene("login_view.fxml"); - }); - - - } - - private void switchScene(String fxmlFile) { - try { - - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/" + fxmlFile)); - Parent root = loader.load(); - - - Stage stage = (Stage) backButton.getScene().getWindow(); - stage.setScene(new Scene(root)); - stage.show(); - - } catch (IOException e) { - e.printStackTrace(); - } - } -} - diff --git a/src/main/java/org/to/telegramfinalproject/UI/RegisterForm.java b/src/main/java/org/to/telegramfinalproject/UI/RegisterForm.java deleted file mode 100644 index 9bda6be..0000000 --- a/src/main/java/org/to/telegramfinalproject/UI/RegisterForm.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.to.telegramfinalproject.UI; - - -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.PasswordField; -import javafx.scene.control.TextField; -import javafx.stage.Stage; -import org.json.JSONObject; -import org.to.telegramfinalproject.Client.ClientConnection; -import org.to.telegramfinalproject.Database.userDatabase; - -import java.io.IOException; - -public class RegisterForm { - @FXML - private Button submitButton; - @FXML - private Button backButton; - @FXML private TextField userIdField; - @FXML private TextField usernameField; - @FXML private TextField profileNameField; - @FXML private PasswordField passwordField; - @FXML private PasswordField confirmPasswordField; - - - - private ClientConnection connection; - - @FXML - public void initialize() { - - try { - connection = new ClientConnection("localhost", 8000); - } catch (Exception e) { - System.out.println("Could not connect to server: " + e.getMessage()); - } - - submitButton.setOnAction(e -> { - String userID = userIdField.getText(); - String username = usernameField.getText(); - String profile_name = profileNameField.getText(); - String password = passwordField.getText(); - String confirmPass =confirmPasswordField.getText(); - JSONObject request = new JSONObject(); - String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"; - - userDatabase userDb = new userDatabase(); - if(password.equals(confirmPass) && password.matches(passwordRegex) && !userDb.existsByUserId(userID)&& !userDb.existsByUsername(username)){ - try { - request.put("action", "register"); - request.put("user_id", userID); - request.put("username", username); - request.put("password", password); - request.put("profile_name", profile_name); - connection.send(request.toString()); - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration is successful"); - alert.show(); - - } catch (Exception ex) { - Alert alert = new Alert(Alert.AlertType.ERROR, "Error receiving response: " + ex.getMessage()); - alert.show(); - } - - } - else if(!password.equals(confirmPass) && password.matches(passwordRegex)) { - Alert alert = new Alert(Alert.AlertType.ERROR, "Password doesn't match"); - alert.show(); - } - else if(userDb.existsByUserId(userID)) - { - Alert alert = new Alert(Alert.AlertType.ERROR, "User ID is already exist"); - alert.show(); - } - else if(userDb.existsByUsername(username)){ - Alert alert = new Alert(Alert.AlertType.ERROR, "Username is already exist"); - alert.show(); - } - else { - Alert alert = new Alert(Alert.AlertType.ERROR, "Password isn't Strong enough"); - alert.show(); - } - }); - - backButton.setOnAction(e -> { - switchScene("login_view.fxml"); - }); - } - - private void switchScene(String fxmlFile) { - try { - - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/" + fxmlFile)); - Parent root = loader.load(); - - - Stage stage = (Stage) backButton.getScene().getWindow(); - stage.setScene(new Scene(root)); - stage.show(); - - } catch (IOException e) { - e.printStackTrace(); - } - } -} - From 3091764a3b4aefb2f704e9f68ce8012c9d0150b3 Mon Sep 17 00:00:00 2001 From: Asal Lotfi Date: Fri, 29 Aug 2025 20:34:28 +0330 Subject: [PATCH 05/19] Clean code. --- .../UI/ChatPageController.java | 94 +++++++++---------- .../UI/MainController.java | 19 ++-- .../UI/SidebarMenuController.java | 2 +- .../UI/TelegramApplication.java | 2 +- 4 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 54e05e9..302a794 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -72,6 +72,11 @@ public class ChatPageController { @FXML private ImageView sendIcon; + // ===== Time formatter for messages ===== + private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); + private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; + // ===== state ===== private String chatName; private final ThemeManager themeManager = ThemeManager.getInstance(); @@ -81,53 +86,8 @@ public class ChatPageController { private ChatEntry currentChat; private UUID me; - private void initCurrentUserId() { - try { - String meStr = org.to.telegramfinalproject.Client.Session - .currentUser.getString("internal_uuid"); - me = UUID.fromString(meStr); - } catch (Exception ignore) { - me = null; - } - } - - //Time formatter for messages - - private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); - private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); - private static final String YESTERDAY_LABEL = "Yesterday"; - - private String formatWhen(LocalDateTime ts) { - if (ts == null) return ""; - LocalDate today = LocalDate.now(); - LocalDate d = ts.toLocalDate(); - - if (d.isEqual(today)) { - return FMT_HHMM.format(ts); - } else if (d.isEqual(today.minusDays(1))) { - return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); - } else { - return FMT_DATE_TIME.format(ts); - } - } - - /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ - private LocalDateTime parseWhen(String iso) { - if (iso == null || iso.isEmpty()) return null; - try { - return OffsetDateTime.parse(iso).toLocalDateTime(); - } catch (Exception ignore) { - try { - return LocalDateTime.parse(iso); - } catch (Exception e) { - return null; - } - } - } - @FXML public void initialize() { - initCurrentUserId(); // Send button @@ -235,6 +195,44 @@ public class ChatPageController { themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme()); } + private void initCurrentUserId() { + try { + String meStr = org.to.telegramfinalproject.Client.Session + .currentUser.getString("internal_uuid"); + me = UUID.fromString(meStr); + } catch (Exception ignore) { + me = null; + } + } + + private String formatWhen(LocalDateTime ts) { + if (ts == null) return ""; + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + return FMT_HHMM.format(ts); + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); + } else { + return FMT_DATE_TIME.format(ts); + } + } + + /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ + private LocalDateTime parseWhen(String iso) { + if (iso == null || iso.isEmpty()) return null; + try { + return OffsetDateTime.parse(iso).toLocalDateTime(); + } catch (Exception ignore) { + try { + return LocalDateTime.parse(iso); + } catch (Exception e) { + return null; + } + } + } + @FXML private void openSearchPanel() { MainController.getInstance().showSearchPanel(); @@ -424,7 +422,6 @@ public class ChatPageController { }).start(); } - private void renderMessages(org.json.JSONArray arr) { messageContainer.getChildren().clear(); @@ -465,8 +462,6 @@ public class ChatPageController { messageScrollPane.setVvalue(1.0); } - - private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); @@ -508,7 +503,4 @@ public class ChatPageController { 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 42065c0..db68b76 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -50,6 +50,12 @@ public class MainController { @FXML private ImageView menuIcon; private SidebarMenuController sidebarController; //save sidebar controller + // === Time formatter for messages === + private static final java.time.format.DateTimeFormatter FMT_HHMM = + java.time.format.DateTimeFormatter.ofPattern("HH:mm"); + private static final java.time.format.DateTimeFormatter FMT_DATE_TIME = + java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; // === STATE === private static MainController instance; @@ -174,13 +180,6 @@ public class MainController { // // } - - private static final java.time.format.DateTimeFormatter FMT_HHMM = - java.time.format.DateTimeFormatter.ofPattern("HH:mm"); - private static final java.time.format.DateTimeFormatter FMT_DATE_TIME = - java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); - private static final String YESTERDAY_LABEL = "Yesterday"; - // ... private String formatChatTime(java.time.LocalDateTime ts) { if (ts == null) return ""; @@ -198,9 +197,6 @@ public class MainController { } } - - - private void populateChatListFromSession() { chatListContainer.getChildren().clear(); @@ -216,7 +212,6 @@ public class MainController { } } - // private void addChatNode(ChatEntry chat ) { // try { // FXMLLoader fx = new FXMLLoader(getClass().getResource( @@ -260,7 +255,6 @@ public class MainController { } } - private String mapTypeToLabel(String t) { switch (t.toUpperCase()) { case "IMAGE": return "[Image]"; @@ -271,7 +265,6 @@ public class MainController { } } - // private void addChat(String name, String lastMsg, String time, int unread) { // try { // FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 1098278..6a5eff8 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -197,7 +197,7 @@ public class SidebarMenuController { String img = user.optString("image_url", ""); Image pic = tryLoadImage(img); if (pic == null) { - pic = loadImage("/org/to/telegramfinalproject/Avatars/profile.png"); + pic = loadImage("/org/to/telegramfinalproject/Avatars/default_profile.png"); } if (pic != null) profileImage.setImage(pic); } diff --git a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java index 6374ef5..e13a0be 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java +++ b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java @@ -45,7 +45,7 @@ import java.io.IOException; public class TelegramApplication extends Application { @Override public void start(Stage stage) throws IOException { - // اول intro.fxml + // First: intro.fxml FXMLLoader fx = new FXMLLoader( TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/intro.fxml")); Scene scene = new Scene(fx.load(), 1480, 820); From 41e9382c42a8e2438cf43839b34b3a55f6043a6e Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 20:44:46 +0330 Subject: [PATCH 06/19] Show forwarded replied messages and reactions --- .../Server/ClientHandler.java | 157 +++++++++++++++++- .../UI/ChatPageController.java | 111 ++++++++----- 2 files changed, 220 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index b5bb596..8005356 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1344,6 +1344,160 @@ public class ClientHandler implements Runnable { break; } + case "get_messages_UI": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + try { + final String receiverId = requestJson.getString("receiver_id"); + final String receiverType = requestJson.getString("receiver_type").toLowerCase(); + final int offset = requestJson.optInt("offset", 0); + final int limit = requestJson.optInt("limit", 50); + + List messages = new ArrayList<>(); + + switch (receiverType) { + case "private" -> { + // receiver_id = private_chat.chat_id + UUID chatId = UUID.fromString(receiverId); + List members = PrivateChatDatabase.getMembers(chatId); + if (members == null || !members.contains(currentUser.getInternal_uuid())) { + response = new ResponseModel("error", "You're not a member of this private chat."); + break; + } + messages = MessageDatabase.privateChatHistory(chatId, currentUser.getInternal_uuid()); + } + case "group" -> { + Group group = GroupDatabase.findByInternalUUID(UUID.fromString(receiverId)); + if (group == null) { + response = new ResponseModel("error", "Group not found."); + break; + } + messages = MessageDatabase.groupChatHistory(group.getInternal_uuid(), currentUser.getInternal_uuid()); + } + case "channel" -> { + Channel channel = ChannelDatabase.findByInternalUUID(UUID.fromString(receiverId)); + if (channel == null) { + response = new ResponseModel("error", "Channel not found."); + break; + } + messages = MessageDatabase.channelChatHistory(channel.getInternal_uuid(), currentUser.getInternal_uuid()); + } + default -> { + response = new ResponseModel("error", "Invalid receiver type."); + break; + } + } + if (response != null) break; + + if (offset > 0 || limit > 0) { + int from = Math.max(0, Math.min(offset, messages.size())); + int to = Math.max(from, Math.min(from + limit, messages.size())); + messages = messages.subList(from, to); + } + + JSONArray messageArray = new JSONArray(); + + for (Message m : messages) { + JSONObject obj = new JSONObject(); + + obj.put("message_id", m.getMessage_id().toString()); + obj.put("sender_id", m.getSender_id().toString()); + + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + obj.put("sender_name", senderUser != null ? senderUser.getProfile_name() : "Unknown"); + + obj.put("receiver_id", m.getReceiver_id().toString()); + obj.put("receiver_type", m.getReceiver_type()); + + String receiverName = switch (m.getReceiver_type()) { + case "group" -> { + Group g = GroupDatabase.findByInternalUUID(m.getReceiver_id()); + yield g != null ? g.getGroup_name() : "Unknown group"; + } + case "channel" -> { + Channel c = ChannelDatabase.findByInternalUUID(m.getReceiver_id()); + yield c != null ? c.getChannel_name() : "Unknown channel"; + } + case "private" -> { + UUID otherId = PrivateChatDatabase.getOtherParticipant(m.getReceiver_id(), currentUser.getInternal_uuid()); + User other = userDatabase.findByInternalUUID(otherId); + yield other != null ? other.getProfile_name() : "Unknown user"; + } + default -> "Unknown"; + }; + obj.put("receiver_name", receiverName); + + obj.put("content", m.getContent()); + obj.put("message_type", m.getMessage_type()); // TEXT/IMAGE/AUDIO/VIDEO/FILE + obj.put("send_at", m.getSend_at().toString()); + + obj.put("is_edited", m.isIs_edited()); + obj.put("is_deleted_globally", m.isIs_deleted_globally()); + obj.put("edited_at", m.getEdited_at() != null ? m.getEdited_at().toString() : JSONObject.NULL); + + if (m.getReply_to_id() != null) { + obj.put("reply_to_id", m.getReply_to_id().toString()); + + Message replied = MessageDatabase.findById(m.getReply_to_id()); + if (replied != null) { + User rSender = userDatabase.findByInternalUUID(replied.getSender_id()); + obj.put("reply_to_sender", rSender != null ? rSender.getProfile_name() : "Unknown"); + obj.put("reply_to_content", replied.getContent()); + obj.put("reply_to_type", replied.getMessage_type()); + } + } else { + obj.put("reply_to_id", JSONObject.NULL); + } + + if (m.getOriginal_message_id() != null && m.getForwarded_from() != null) { + User originalSender = userDatabase.findByInternalUUID(m.getForwarded_from()); + obj.put("is_forwarded", true); + obj.put("forwarded_from", originalSender != null + ? originalSender.getProfile_name() + : m.getForwarded_from().toString()); + obj.put("forwarded_by", senderUser != null ? senderUser.getProfile_name() : "Unknown"); + obj.put("forwarded_from_id", m.getForwarded_from().toString()); + } else { + obj.put("is_forwarded", false); + obj.put("forwarded_from", JSONObject.NULL); + obj.put("forwarded_by", JSONObject.NULL); + } + + JSONArray reactionsArr = new JSONArray(); + try { + List reactions = MessageReactionDatabase.getReactions(m.getMessage_id()); + if (reactions != null && !reactions.isEmpty()) { + Map counter = new HashMap<>(); + for (String r : reactions) { + counter.put(r, counter.getOrDefault(r, 0) + 1); + } + for (Map.Entry e : counter.entrySet()) { + JSONObject ro = new JSONObject(); + ro.put("emoji", e.getKey()); + ro.put("count", e.getValue()); + reactionsArr.put(ro); + } + } + } catch (Exception ignore) {} + obj.put("reactions", reactionsArr); + + messageArray.put(obj); + } + + JSONObject data = new JSONObject(); + data.put("messages", messageArray); + + response = new ResponseModel("success", "Messages fetched.", data); + + } catch (Exception e) { + response = new ResponseModel("error", "Error fetching messages: " + e.getMessage()); + } + break; + } + case "toggle_block": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); @@ -2795,7 +2949,6 @@ public class ClientHandler implements Runnable { private void enrichChatEntry(ChatEntry e, UUID me) { - // آخرین پیام Message last = MessageDatabase.getLastMessage(e.getId(), e.getType()); if (last != null) { e.setLastMessageTime(String.valueOf(last.getSend_at())); @@ -2804,7 +2957,6 @@ public class ClientHandler implements Runnable { e.setLastMessageSenderId(last.getSender_id()); } - // تعداد نخوانده‌ها int unread = MessageDatabase.getUnreadCount(me, e.getId(), e.getType()); e.setUnreadCount(unread); } @@ -2814,7 +2966,6 @@ public class ClientHandler implements Runnable { if ("TEXT".equalsIgnoreCase(t)) { String c = m.getContent(); if (c == null) return ""; - // پیش‌نمایش کوتاه return c.length() > 120 ? c.substring(0, 120) + "…" : c; } switch (t) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 5157ebb..c4dc1d7 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -81,6 +81,17 @@ public class ChatPageController { private ChatEntry currentChat; private UUID me; + // ----- helpers for safe strings ----- + private static String nz(String s) { + return s == null ? "" : s.trim(); + } + private static boolean hasVal(String s) { + if (s == null) return false; + String t = s.trim(); + return !t.isEmpty() && !"null".equalsIgnoreCase(t); + } + + private void initCurrentUserId() { try { String meStr = org.to.telegramfinalproject.Client.Session @@ -394,7 +405,7 @@ public class ChatPageController { private void loadMessages(ChatEntry entry) { JSONObject req = new JSONObject(); - req.put("action", "get_messages"); + req.put("action", "get_messages_UI"); req.put("receiver_id", String.valueOf(entry.getId())); req.put("receiver_type", entry.getType()); req.put("limit", 50); @@ -482,47 +493,51 @@ public class ChatPageController { private void renderMessages(org.json.JSONArray list) { messageContainer.getChildren().clear(); + // برای reply-preview: ایندکس کردن پیام‌ها با message_id msgIndex.clear(); - for (int i=0; i showing bracket for msgId=" + msgId); - } - - + // نمایش addBubble(outgoing, senderName, type, content, ts, msgId, fwdFrom, fwdBy, replyToId, edited, reactions); } + // کمی فاصله بین پیام‌ها + messageContainer.setSpacing(8); messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } @@ -592,20 +607,17 @@ public class ChatPageController { boolean edited, org.json.JSONArray reactions ) { - // --- meta (نام فرستنده + زمان [+ edited]) --- + // 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); - // --- نرمال‌سازی نوع پیام + fallback --- - String t = (type == null) ? "" : type.trim().toUpperCase(); - // اگر type نامعتبر/خالی بود ولی content متن داشت، فرض کن TEXT است. + // نرمال‌سازی نوع پیام و متن + 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); // [Image] / [Audio] / ... + String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t); Label msg = new Label(bodyText); msg.setWrapText(true); @@ -623,28 +635,38 @@ public class ChatPageController { ); msg.setMinHeight(Region.USE_PREF_SIZE); - VBox bubble = new VBox(6); + // بدنه حباب + VBox bubble = new VBox(4); // spacing عمودی داخل حباب bubble.getChildren().add(meta); - if (notBlank(forwardedFrom) || notBlank(forwardedBy)) { + // Forward header (اختیاری) + if (hasVal(forwardedFrom) || hasVal(forwardedBy)) { bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); } - if (notBlank(replyToId)) { + + // Reply preview (اختیاری) + if (hasVal(replyToId)) { bubble.getChildren().add(buildReplyBoxFromIndex(replyToId)); } + // متن اصلی bubble.getChildren().add(msg); + // Reactions (اختیاری) if (reactions != null && reactions.length() > 0) { bubble.getChildren().add(buildReactionsBarFromJson(reactions, dark)); } + // چیدمان راست/چپ javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); row.setFillHeight(true); - row.setSpacing(6); + row.setSpacing(4); 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); } @@ -662,16 +684,14 @@ public class ChatPageController { private javafx.scene.Node buildForwardHeader(String forwardedFrom, String forwardedBy) { - String txt; - if (notBlank(forwardedFrom) && notBlank(forwardedBy)) { - txt = "Forwarded from " + forwardedFrom + " by " + forwardedBy; - } else if (notBlank(forwardedFrom)) { - txt = "Forwarded from " + forwardedFrom; - } else if (notBlank(forwardedBy)) { - txt = "Forwarded by " + forwardedBy; - } else { - txt = "Forwarded"; - } + String from = hasVal(forwardedFrom) ? forwardedFrom.trim() : null; + String by = hasVal(forwardedBy) ? forwardedBy.trim() : null; + + String txt = (from != null && by != null) ? ("Forwarded from " + from + " by " + by) + : (from != null) ? ("Forwarded from " + from) + : (by != null) ? ("Forwarded by " + by) + : "Forwarded"; + Label l = new Label(txt); l.setStyle("-fx-font-size: 11; -fx-text-fill: #5b8bb1;"); l.setWrapText(true); @@ -687,7 +707,7 @@ public class ChatPageController { String rType = r.optString("message_type", "TEXT"); String rContent = r.optString("content", ""); String rSender = r.optString("sender_name", ""); - if (!rSender.isEmpty()) from = rSender; + if (hasVal(rSender)) from = rSender; preview = "TEXT".equalsIgnoreCase(rType) ? rContent : bracketLabel(rType); } else { @@ -710,6 +730,7 @@ public class ChatPageController { return box; } + private javafx.scene.Node buildReactionsBarFromJson(org.json.JSONArray arr, boolean dark) { javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(6); bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); From 4ac5d66452829e6127c932f11042db5c9a506f1c Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 20:50:54 +0330 Subject: [PATCH 07/19] Switched from Login to Register --- .../to/telegramfinalproject/UI/LoginController.java | 12 +++--------- .../telegramfinalproject/UI/RegisterController.java | 4 +++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java index c72d699..bc257a1 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -19,6 +19,8 @@ import org.to.telegramfinalproject.Security.PasswordHashing; import java.io.IOException; +import static org.to.telegramfinalproject.UI.AppRouter.showRegister; + public class LoginController { @FXML private TextField usernameField; @@ -120,14 +122,6 @@ public class LoginController { var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); var handler = cli.getHandler(); - // بساز و بفرست — همون send خودت که Session رو پر می‌کند -// org.json.JSONObject req = new org.json.JSONObject() -// .put("action","login") -// .put("username", u) -// .put("password", p); -// -// handler.send(req); // ⬅️ بلاکینگ؛ پس درستش کردیم که تو Thread هست - handler.login(u,p); @@ -165,7 +159,7 @@ public class LoginController { @FXML private void switchToRegister() throws IOException { - switchScene("register_view.fxml"); + showRegister(); } private void switchScene(String fxmlFile) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java b/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java index a8f29bb..afc2174 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java @@ -14,6 +14,8 @@ import org.to.telegramfinalproject.Database.userDatabase; import java.io.IOException; +import static org.to.telegramfinalproject.UI.AppRouter.showLogin; + public class RegisterController { @FXML private TextField userIdField; @@ -131,7 +133,7 @@ public class RegisterController { @FXML private void switchToLogin() throws IOException { - switchScene("login_view.fxml"); + showLogin(); } private void switchScene(String fxmlFile) { From 2bae5a5b782f261ad1edec193c072679bcffcfb5 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 21:52:27 +0330 Subject: [PATCH 08/19] Message receipts --- .../Database/MessageDatabase.java | 351 ++++++++++++++---- .../Server/ClientHandler.java | 50 +++ .../UI/ChatPageController.java | 80 ++-- 3 files changed, 374 insertions(+), 107 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 5085ed5..f248058 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -381,71 +381,180 @@ public class MessageDatabase { +// public static List getUnreadMessages(UUID userId) { +// List messages = new ArrayList<>(); +// +// String sql = """ +// SELECT m.* +// FROM messages m +// LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? +// LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? +// WHERE r.user_id IS NULL +// AND d.message_id IS NULL +// AND m.is_deleted_globally = FALSE +// AND ( +// (m.receiver_type = 'private' AND m.receiver_id = ?) +// OR +// (m.receiver_type = 'group' AND EXISTS ( +// SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? +// )) +// OR +// (m.receiver_type = 'channel' AND EXISTS ( +// SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? +// )) +// ) +// ORDER BY m.send_at DESC +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// +// stmt.setObject(1, userId); // for message_receipts +// stmt.setObject(2, userId); // for deleted_messages +// stmt.setObject(3, userId); // for private messages +// stmt.setObject(4, userId); // for group members +// stmt.setObject(5, userId); // for channel subscribers +// +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// Message message = new Message( +// UUID.fromString(rs.getString("message_id")), +// rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, +// rs.getString("receiver_type"), +// UUID.fromString(rs.getString("receiver_id")), +// rs.getString("content"), +// rs.getString("message_type"), +// rs.getTimestamp("send_at").toLocalDateTime(), +// rs.getString("status"), +// rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, +// rs.getBoolean("is_edited"), +// rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, +// rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, +// rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null, +// rs.getBoolean("is_deleted_globally"), +// rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null +// ); +// +// messages.add(message); +// } +// +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// return messages; +// } + + + public static List getUnreadMessages(UUID userId) { + return getUnreadMessages(userId, 200); // یک سقف معقول + } + + public static List getUnreadMessages(UUID userId, int limit) { List messages = new ArrayList<>(); + // توجه: نام جدول private_chats/gm/cs را با اسامی واقعی دیتابیس‌ت هماهنگ کن String sql = """ SELECT m.* FROM messages m - LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? - LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? - WHERE r.user_id IS NULL + LEFT JOIN message_receipts r + ON r.message_id = m.message_id + AND r.user_id = ? + LEFT JOIN deleted_messages d + ON d.message_id = m.message_id + AND d.user_id = ? + WHERE r.message_id IS NULL AND d.message_id IS NULL AND m.is_deleted_globally = FALSE + AND m.sender_id <> ? AND ( - (m.receiver_type = 'private' AND m.receiver_id = ?) - OR - (m.receiver_type = 'group' AND EXISTS ( - SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? - )) - OR - (m.receiver_type = 'channel' AND EXISTS ( - SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? - )) + (m.receiver_type = 'private' AND EXISTS ( + SELECT 1 + FROM private_chat pc + WHERE pc.chat_id = m.receiver_id + AND (pc.user1_id = ? OR pc.user2_id = ?) + ))) + OR (m.receiver_type = 'group' AND EXISTS ( + SELECT 1 + FROM group_members gm + WHERE gm.group_id = m.receiver_id + AND gm.user_id = ? + )) + OR (m.receiver_type = 'channel' AND EXISTS ( + SELECT 1 + FROM channel_subscribers cs + WHERE cs.channel_id = m.receiver_id + AND cs.user_id = ? + )) ) - ORDER BY m.send_at DESC - """; + ORDER BY m.send_at ASC + LIMIT ? + """; try (Connection conn = ConnectionDb.connect(); - PreparedStatement stmt = conn.prepareStatement(sql)) { + PreparedStatement ps = conn.prepareStatement(sql)) { - stmt.setObject(1, userId); // for message_receipts - stmt.setObject(2, userId); // for deleted_messages - stmt.setObject(3, userId); // for private messages - stmt.setObject(4, userId); // for group members - stmt.setObject(5, userId); // for channel subscribers + int i = 1; + ps.setObject(i++, userId); // r.user_id + ps.setObject(i++, userId); // d.user_id + ps.setObject(i++, userId); // m.sender_id <> ? + ps.setObject(i++, userId); // pc.user1_id=userId + ps.setObject(i++, userId); // pc.user2_id=userId + ps.setObject(i++, userId); // gm.user_id + ps.setObject(i++, userId); // cs.user_id + ps.setInt(i++, Math.max(1, limit)); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - Message message = new Message( - UUID.fromString(rs.getString("message_id")), - rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, - rs.getString("receiver_type"), - UUID.fromString(rs.getString("receiver_id")), - rs.getString("content"), - rs.getString("message_type"), - rs.getTimestamp("send_at").toLocalDateTime(), - rs.getString("status"), - rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, - rs.getBoolean("is_edited"), - rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, - rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, - rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null, - rs.getBoolean("is_deleted_globally"), - rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null - ); - - messages.add(message); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + messages.add(mapMessage(rs)); // همان mapMessage که گفتیم + } } - } catch (SQLException e) { e.printStackTrace(); } - return messages; } + private static java.util.UUID readUUID(ResultSet rs, String col) throws SQLException { + Object o = rs.getObject(col); + if (o == null) return null; + if (o instanceof java.util.UUID) return (java.util.UUID) o; + return java.util.UUID.fromString(o.toString()); + } + + private static java.time.LocalDateTime readLdt(ResultSet rs, String col) throws SQLException { + java.sql.Timestamp ts = rs.getTimestamp(col); + return (ts == null) ? null : ts.toLocalDateTime(); + } + + private static boolean readBool(ResultSet rs, String col) throws SQLException { + boolean v = rs.getBoolean(col); + return v; + } + + private static Message mapMessage(ResultSet rs) throws SQLException { + Message m = new Message(); + + m.setMessage_id( readUUID(rs, "message_id") ); + m.setSender_id( readUUID(rs, "sender_id") ); + m.setReceiver_type( rs.getString("receiver_type") ); + m.setReceiver_id( readUUID(rs, "receiver_id") ); + m.setContent( rs.getString("content") ); + m.setMessage_type( rs.getString("message_type") ); + m.setSend_at( readLdt(rs, "send_at") ); + m.setStatus( rs.getString("status") ); + m.setReply_to_id( readUUID(rs, "reply_to_id") ); + m.setIs_edited( readBool(rs, "is_edited") ); + m.setEdited_at( readLdt(rs, "edited_at") ); + m.setOriginal_message_id( readUUID(rs, "original_message_id") ); + m.setForwarded_by( readUUID(rs, "forwarded_by") ); + m.setForwarded_from( readUUID(rs, "forwarded_from") ); + m.setIs_deleted_globally( readBool(rs, "is_deleted_globally") ); + + return m; + } public static List getAttachments(UUID messageId) { List attachments = new ArrayList<>(); @@ -1079,30 +1188,30 @@ public class MessageDatabase { } } - public static int getUnreadCount(UUID me, UUID targetId, String type) { - final String sql = - "SELECT COUNT(*) FROM messages " + - "WHERE receiver_type = ? AND receiver_id = ? " + - "AND sender_id <> ? " + - "AND (status IS NULL OR status <> 'SEEN')"; - - try (Connection conn = ConnectionDb.connect(); - PreparedStatement ps = conn.prepareStatement(sql)) { - - ps.setString(1, type.toLowerCase()); - ps.setObject(2, targetId); - ps.setObject(3, me); - - try (ResultSet rs = ps.executeQuery()) { - return rs.next() ? rs.getInt(1) : 0; - } - } catch (SQLException e) { - e.printStackTrace(); - return 0; - } - - - } +// public static int getUnreadCount(UUID me, UUID targetId, String type) { +// final String sql = +// "SELECT COUNT(*) FROM messages " + +// "WHERE receiver_type = ? AND receiver_id = ? " + +// "AND sender_id <> ? " + +// "AND (status IS NULL OR status <> 'SEEN')"; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement ps = conn.prepareStatement(sql)) { +// +// ps.setString(1, type.toLowerCase()); +// ps.setObject(2, targetId); +// ps.setObject(3, me); +// +// try (ResultSet rs = ps.executeQuery()) { +// return rs.next() ? rs.getInt(1) : 0; +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// return 0; +// } +// +// +// } private static Message mapRow(ResultSet rs) throws SQLException { Message m = new Message(); @@ -1126,4 +1235,112 @@ public class MessageDatabase { return m; } + + + public static List getUnreadMessageIds(UUID me, UUID chatId, String chatType, int limit) { + String sql = """ + SELECT m.message_id + FROM messages m + WHERE m.receiver_id = ? AND m.receiver_type = ? + AND m.sender_id <> ? + AND NOT EXISTS ( + SELECT 1 FROM message_receipts r + WHERE r.message_id = m.message_id + AND r.user_id = ? + ) + ORDER BY m.send_at ASC + LIMIT ? + """; + List ids = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.setString(2, chatType); + ps.setObject(3, me); + ps.setObject(4, me); + ps.setInt(5, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + ids.add((UUID) rs.getObject(1)); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return ids; + } + + // تعداد ناخوانده‌ها (برای چت‌لیست) + public static int getUnreadCount(UUID me, UUID chatId, String chatType) { + String sql = """ + SELECT COUNT(1) + FROM messages m + WHERE m.receiver_id = ? + AND m.receiver_type = ? + AND m.sender_id <> ? + AND NOT EXISTS ( + SELECT 1 FROM message_receipts r + WHERE r.message_id = m.message_id + AND r.user_id = ? + ) + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.setString(2, chatType); + ps.setObject(3, me); + ps.setObject(4, me); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + + public static int insertReceiptIfAbsent(UUID messageId, UUID userId) { + String sql = """ + INSERT INTO message_receipts(message_id, user_id, read_at) + VALUES (?, ?, now()) + ON CONFLICT (message_id, user_id) DO NOTHING + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + return ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + //Read status if it's first reader + public static int setMessageReadIfNeeded(UUID messageId, UUID viewerId) { + String sql = """ + UPDATE messages m + SET status = 'READ' + WHERE m.message_id = ? + AND m.sender_id <> ? + AND m.status <> 'READ' + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, viewerId); + return ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + + + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 8005356..31fc039 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -11,6 +11,7 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; +import java.sql.Connection; import java.time.LocalDateTime; import java.util.*; @@ -2734,6 +2735,55 @@ public class ClientHandler implements Runnable { break; } + case "mark_as_read": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + try { + UUID chatId = UUID.fromString(requestJson.getString("receiver_id")); + String chatType = requestJson.getString("receiver_type").toLowerCase(); + int limit = requestJson.optInt("limit", 500); + + List targetMessageIds = new ArrayList<>(); + if (requestJson.has("message_ids")) { + JSONArray arr = requestJson.getJSONArray("message_ids"); + for (int i = 0; i < arr.length(); i++) { + targetMessageIds.add(UUID.fromString(arr.getString(i))); + } + } else { + targetMessageIds = MessageDatabase.getUnreadMessageIds( + currentUser.getInternal_uuid(), chatId, chatType, limit + ); + } + + int updatedStatus = 0; + int insertedReceipts = 0; + + try (Connection c = ConnectionDb.connect()) { + c.setAutoCommit(false); + for (UUID mid : targetMessageIds) { + updatedStatus += MessageDatabase.setMessageReadIfNeeded(mid, currentUser.getInternal_uuid()); + insertedReceipts+= MessageDatabase.insertReceiptIfAbsent(mid, currentUser.getInternal_uuid()); + } + c.commit(); + } catch (Exception tx) { + tx.printStackTrace(); + } + + JSONObject data = new JSONObject(); + data.put("marked_count", targetMessageIds.size()); + data.put("status_updates", updatedStatus); + data.put("receipts_inserted", insertedReceipts); + + response = new ResponseModel("success", "Marked as read.", data); + } catch (Exception e) { + response = new ResponseModel("error", "Error in mark_as_read: " + e.getMessage()); + } + break; + } + + default: response = new ResponseModel("error", "Unknown action: " + action); } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 3a51a9d..aee726f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -109,9 +109,9 @@ public class ChatPageController { //Time formatter for messages - private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); - private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); - private static final String YESTERDAY_LABEL = "Yesterday"; +// private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); +// private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); +// private static final String YESTERDAY_LABEL = "Yesterday"; private static String str(org.json.JSONObject j, String k) { try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; } @@ -261,43 +261,43 @@ public class ChatPageController { themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme()); } - private void initCurrentUserId() { - try { - String meStr = org.to.telegramfinalproject.Client.Session - .currentUser.getString("internal_uuid"); - me = UUID.fromString(meStr); - } catch (Exception ignore) { - me = null; - } - } - - private String formatWhen(LocalDateTime ts) { - if (ts == null) return ""; - LocalDate today = LocalDate.now(); - LocalDate d = ts.toLocalDate(); - - if (d.isEqual(today)) { - return FMT_HHMM.format(ts); - } else if (d.isEqual(today.minusDays(1))) { - return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); - } else { - return FMT_DATE_TIME.format(ts); - } - } - - /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ - private LocalDateTime parseWhen(String iso) { - if (iso == null || iso.isEmpty()) return null; - try { - return OffsetDateTime.parse(iso).toLocalDateTime(); - } catch (Exception ignore) { - try { - return LocalDateTime.parse(iso); - } catch (Exception e) { - return null; - } - } - } +// private void initCurrentUserId() { +// try { +// String meStr = org.to.telegramfinalproject.Client.Session +// .currentUser.getString("internal_uuid"); +// me = UUID.fromString(meStr); +// } catch (Exception ignore) { +// me = null; +// } +// } +// +// private String formatWhen(LocalDateTime ts) { +// if (ts == null) return ""; +// LocalDate today = LocalDate.now(); +// LocalDate d = ts.toLocalDate(); +// +// if (d.isEqual(today)) { +// return FMT_HHMM.format(ts); +// } else if (d.isEqual(today.minusDays(1))) { +// return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); +// } else { +// return FMT_DATE_TIME.format(ts); +// } +// } +// +// /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ +// private LocalDateTime parseWhen(String iso) { +// if (iso == null || iso.isEmpty()) return null; +// try { +// return OffsetDateTime.parse(iso).toLocalDateTime(); +// } catch (Exception ignore) { +// try { +// return LocalDateTime.parse(iso); +// } catch (Exception e) { +// return null; +// } +// } +// } @FXML private void openSearchPanel() { From 60338fec52ec54b50b80113692f52f06b19b8959 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 29 Aug 2025 23:26:06 +0330 Subject: [PATCH 09/19] Work on connection to search bar --- .../Client/ActionHandler.java | 20 +- .../UI/MainController.java | 371 +++++++++++++++++- 2 files changed, 375 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index a4a510a..5ce10d6 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -30,6 +30,11 @@ public class ActionHandler { //use for UI private volatile String lastStatus = "error"; // success | error private volatile String lastMessage = ""; + // ===== UI hook for search results ===== + + + + public String getLastStatus() { return lastStatus; } public String getLastMessage() { return lastMessage; } @@ -37,7 +42,6 @@ public class ActionHandler { - private void handleRealTime(JSONObject json) throws IOException { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); @@ -141,6 +145,18 @@ public class ActionHandler { send(model.toJson()); } + public void searchUI(String keyword) { + + if (Session.currentUser == null || !Session.currentUser.has("user_id")) { + System.out.println("You must be logged in to search."); + return; + } + + String userId = Session.currentUser.getString("user_id"); + SearchRequestModel model = new SearchRequestModel("search", keyword, userId); + send(model.toJson()); + } + public void searchInUsers(){ System.out.println("Enter keyword to search: "); @@ -664,6 +680,8 @@ public class ActionHandler { } break; case "search" : + + JSONArray results = response.getJSONObject("data").getJSONArray("results"); if (results.isEmpty()) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index db68b76..132b5bc 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -14,7 +14,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; import org.to.telegramfinalproject.Models.ChatEntry; - +import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; @@ -118,23 +118,41 @@ public class MainController { mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider()); } - // Search in chat field listener - searchBar.textProperty().addListener((obs, oldV, newV) -> { - if (!chatSearchPane.isVisible()) return; // only react if in search mode - if (newV.trim().isEmpty()) { - chatSearchResults.setVisible(false); - chatSearchResults.setManaged(false); - } else { - chatSearchResults.setVisible(true); - chatSearchResults.setManaged(true); - chatSearchResults.getItems().setAll( - "Result 1: " + newV, - "Result 2: " + newV, - "Result 3: " + newV - ); + // Search in chat field listener +// searchBar.textProperty().addListener((obs, oldV, newV) -> { +// if (!chatSearchPane.isVisible()) return; // only react if in search mode +// +// if (newV.trim().isEmpty()) { +// chatSearchResults.setVisible(false); +// chatSearchResults.setManaged(false); +// } else { +// chatSearchResults.setVisible(true); +// chatSearchResults.setManaged(true); +// chatSearchResults.getItems().setAll( +// "Result 1: " + newV, +// "Result 2: " + newV, +// "Result 3: " + newV +// ); +// } +// }); + + // وقتی کاربر 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()) { + openSearchResult(searchBacking.get(idx)); } }); + } // Called from ChatPageController when user clicks search button @@ -207,11 +225,31 @@ 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) { + if (saved == null && isSavedMessages(c)) { + saved = c; + } else { + others.add(c); + } + } + + // اگر “Archived Chats” یا هدر دیگری داری، قبلش اضافه کن (اختیاری) + // addArchivedHeaderIfYouHaveOne(); + + // 1) همیشه Saved اول بیاد (اگر وجود داشت) + if (saved != null) { + addChatNode(saved); + } + + for (ChatEntry c : others) { addChatNode(c); } } + // private void addChatNode(ChatEntry chat ) { // try { // FXMLLoader fx = new FXMLLoader(getClass().getResource( @@ -417,4 +455,307 @@ public class MainController { private void updateLabelsForLightMode() { //myLabel.setStyle("-fx-text-fill: black;"); } + + + + + + // ===== Search UI state ===== + private final java.util.List searchBacking = new java.util.ArrayList<>(); + + private enum SRType { USER, GROUP, CHANNEL, MESSAGE } + + private static class SearchResult { + final SRType type; + final String title; // name / sender_name / ... + final String subtitle; // @"id" / context string / time + final String receiverType; // برای MESSAGE/GROUP/CHANNEL: private|group|channel, برای USER: null + final java.util.UUID uuid; // uuid آن موجودیت (user/group/channel/chat holder) یا receiver_id پیام + final String displayId; // id قابل نمایش (مثل username یا group_id/channel_id) + final String messageId; // فقط برای MESSAGE + final String time; // نمایش + + SearchResult(SRType t, String title, String subtitle, String receiverType, + java.util.UUID uuid, String displayId, String messageId, String time) { + this.type = t; this.title = title; this.subtitle = subtitle; + this.receiverType = receiverType; this.uuid = uuid; + this.displayId = displayId; this.messageId = messageId; this.time = time; + } + + String toDisplay() { + // رشته‌ای که داخل ListView نشان می‌دهیم + switch (type) { + case MESSAGE: + String left = (title == null || title.isBlank()) ? "Message" : title; + String right = (time == null ? "" : (" • " + time)); + return "🗨 " + left + right + (subtitle==null?"":(" — " + subtitle)); + case USER: return "👤 " + title + (subtitle==null?"":(" — " + subtitle)); + case GROUP: return "👥 " + title + (subtitle==null?"":(" — " + subtitle)); + case CHANNEL: return "📣 " + title + (subtitle==null?"":(" — " + subtitle)); + } + return title; + } + } + + + public void performGlobalSearch(String keyword) { + if (keyword == null || keyword.isBlank()) { + chatSearchResults.getItems().clear(); + searchBacking.clear(); + return; + } + if (org.to.telegramfinalproject.Client.Session.currentUser == null || + !org.to.telegramfinalproject.Client.Session.currentUser.has("user_id")) { + System.out.println("You must be logged in to search."); + return; + } + + showSearchPanel(); + + // درخواست به سرور (مثل کنسول) + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "search"); + req.put("keyword", keyword); + req.put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")); + + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + if (resp == null || !"success".equals(resp.optString("status"))) return; + + org.json.JSONArray arr = resp.optJSONObject("data").optJSONArray("results"); + if (arr == null) arr = new org.json.JSONArray(); + + final java.util.List tmp = new java.util.ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject it = arr.getJSONObject(i); + String type = it.optString("type",""); + + switch (type) { + case "user": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.USER, name, + id.isBlank()? null : "@"+id, + null, uuid, id, null, null + )); + break; + } + case "group": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown group"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.GROUP, name, + id.isBlank()? null : id, + "group", uuid, id, null, null + )); + break; + } + case "channel": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown channel"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.CHANNEL, name, + id.isBlank()? null : id, + "channel", uuid, id, null, null + )); + break; + } + case "message": { + String senderName = it.optString("sender_name", it.optString("sender","Unknown")); + String time = it.optString("time", ""); + String content = it.optString("content","[No content]"); + String rType = it.optString("receiver_type",""); // private|group|channel + java.util.UUID rUuid = java.util.UUID.fromString(it.getString("receiver_id")); + String ctx = ""; + if ("group".equals(rType)) ctx = it.optString("group_name",""); + if ("channel".equals(rType)) ctx = it.optString("channel_name",""); + 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("message_id", null), + time + )); + break; + } + } + } + + Platform.runLater(() -> renderSearchResults(tmp)); + }).start(); + } + + private String subtitleSep(String s){ return s==null || s.isBlank()? "" : " — "; } + + private void renderSearchResults(java.util.List results) { + searchBacking.clear(); + searchBacking.addAll(results); + + javafx.collections.ObservableList view = javafx.collections.FXCollections.observableArrayList(); + for (SearchResult r : results) view.add(r.toDisplay()); + chatSearchResults.setItems(view); + chatSearchResults.setVisible(true); + chatSearchResults.setManaged(true); + } + + + 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 سمت سرور + if (chatId == null) { + System.out.println("❌ Failed to create/find private chat."); + return; + } + } + + org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); + ce.setId(chatId.toString()); // ⬅️ internal chat_id + ce.setDisplayId(r.displayId); // username + ce.setName(r.title); // profile_name + ce.setType("private"); + + openChat(ce); // همون متد فعلی MainController + break; + } + + case GROUP: + case CHANNEL: { + // اگر تو لیست هست بازش کن، وگرنه با همون uuid باز کن + org.to.telegramfinalproject.Models.ChatEntry existing = + findExistingChat(r.uuid, r.receiverType); + if (existing != null) { + openChat(existing); + } else { + org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); + ce.setId(r.uuid.toString()); // internal_uuid group/channel + ce.setDisplayId(r.displayId); // group_id/channel_id + ce.setName(r.title); + ce.setType(r.receiverType); + openChat(ce); + } + break; + } + + case MESSAGE: { + // مثل کنسول: چتِ پیام را باز کن (اسکرول به پیام را بعداً اضافه کن) + org.to.telegramfinalproject.Models.ChatEntry existing = + findExistingChat(r.uuid, r.receiverType); + if (existing != null) { + openChat(existing); + } else { + 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.setDisplayId(r.displayId); + openChat(ce); + } + break; + } + } + } + + private org.to.telegramfinalproject.Models.ChatEntry findExistingChat(java.util.UUID internalId, String type) { + var list = (org.to.telegramfinalproject.Client.Session.chatList==null) + ? java.util.Collections.emptyList() + : org.to.telegramfinalproject.Client.Session.chatList; + for (var c : list) { + if (internalId.toString().equals(c.getId().toString()) + && type.equalsIgnoreCase(c.getType())) { + return c; + } + } + return null; + } + + 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) {} + } + } + } + return null; + } + + private java.util.UUID fetchOrCreatePrivateChat(java.util.UUID otherUserUuid) { + try { + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "get_or_create_private_chat"); + req.put("user1", org.to.telegramfinalproject.Client.Session.currentUser.getString("internal_uuid")); + req.put("user2", otherUserUuid.toString()); + org.json.JSONObject resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + if (resp != null && "success".equals(resp.optString("status"))) { + String chatId = resp.getJSONObject("data").getString("chat_id"); + return java.util.UUID.fromString(chatId); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private String guessNameForReceiver(SearchResult r) { + // فقط برای زمانی که موجودیت در لیست نبود + if (r.title != null && !r.title.isBlank()) return r.title; + if (r.receiverType != null) { + switch (r.receiverType) { + case "group": return "Group"; + case "channel": return "Channel"; + case "private": return "Private Chat"; + } + } + return "Chat"; + } + + private boolean isSavedMessages(ChatEntry c) { + if (c == null) return false; + + // اگر تایپ اختصاصی داری + if ("saved".equalsIgnoreCase(c.getType())) return true; + + // اگر با نام مشخص ذخیره می‌کنی + String n = c.getName(); + if (n != null && n.equalsIgnoreCase("Saved Messages")) return true; + + // حالت پرایوت با خودِ کاربر + String me = (org.to.telegramfinalproject.Client.Session.currentUser != null) + ? org.to.telegramfinalproject.Client.Session.currentUser.optString("internal_uuid", "") + : ""; + try { + UUID other = c.getOtherUserId(); // اگر این فیلد را داری + if ("private".equalsIgnoreCase(c.getType()) && + other != null && other.toString().equalsIgnoreCase(me)) { + return true; + } + } catch (Exception ignore) {} + + return false; + } + + + + } \ No newline at end of file From fe70aa6bd7bdabf409de4e2a5cd3d2bf190184f1 Mon Sep 17 00:00:00 2001 From: Asal Lotfi Date: Sat, 30 Aug 2025 17:01:58 +0330 Subject: [PATCH 10/19] Global search UI implemented. --- .../UI/ChatItemController.java | 14 +- .../UI/ChatPageController.java | 11 +- .../UI/MainController.java | 244 +++++++++++++++--- .../UI/SidebarMenuController.java | 5 +- .../Avatars/default_channel_profile.png | Bin 0 -> 1837 bytes .../Avatars/default_group_profile.png | Bin 0 -> 1478 bytes ...t_profile.png => default_user_profile.png} | Bin .../telegramfinalproject/CSS/dark_theme.css | 87 ++++++- .../telegramfinalproject/CSS/light_theme.css | 108 +++++++- .../to/telegramfinalproject/Fxml/main.fxml | 42 +-- .../Icons/no_result_dark.png | Bin 0 -> 812 bytes .../Icons/no_result_light.png | Bin 0 -> 827 bytes 12 files changed, 446 insertions(+), 65 deletions(-) create mode 100644 src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png create mode 100644 src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png rename src/main/resources/org/to/telegramfinalproject/Avatars/{default_profile.png => default_user_profile.png} (100%) create mode 100644 src/main/resources/org/to/telegramfinalproject/Icons/no_result_dark.png create mode 100644 src/main/resources/org/to/telegramfinalproject/Icons/no_result_light.png diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index 270cb94..e4e5d1e 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -34,7 +34,7 @@ public class ChatItemController { * @param unread Unread message count * @param imageUrl Path/URL of profile image (can be null/empty) */ - public void setChatData(String name, String lastMsg, String time, int unread, String imageUrl) { + public void setChatData(String name, String lastMsg, String time, int unread, String imageUrl, String chatType) { chatName.setText(name); lastMessage.setText(lastMsg); chatTime.setText(time); @@ -77,9 +77,17 @@ public class ChatItemController { profileImageUser.setManaged(true); } else { + String path; + if ("group".equalsIgnoreCase(chatType)) { + path = "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; + } else if ("channel".equalsIgnoreCase(chatType)) { + path = "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; + } else { + path = "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + } + profileImageUser.setImage(new Image( - Objects.requireNonNull(getClass().getResourceAsStream( - "/org/to/telegramfinalproject/Icons/default_profile.png")) + Objects.requireNonNull(getClass().getResourceAsStream(path)) )); profileImageUser.setVisible(true); profileImageUser.setManaged(true); diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index aee726f..58d12fc 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -588,12 +588,10 @@ public class ChatPageController { return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); } - - private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); - readReq.put("receiver_id", entry.getId().toString()); // ⛳️ internal_id + readReq.put("receiver_id", entry.getId().toString()); // internal_id readReq.put("receiver_type", entry.getType()); ActionHandler.sendWithResponse(readReq); } @@ -632,10 +630,6 @@ public class ChatPageController { // messageContainer.getChildren().add(row); // } - - - - private void addBubble( boolean outgoing, String displayName, @@ -801,7 +795,4 @@ 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; } - - - } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 132b5bc..2ecd909 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -9,6 +9,7 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -16,25 +17,30 @@ import javafx.util.Duration; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; public class MainController { // === LEFT PANE === @FXML private VBox chatListContainer; // inside the scrollPane @FXML private ScrollPane scrollPane; // chat list scroll + + // === Global Search === + @FXML private TextField searchBar; + @FXML private VBox globalSearchPane; + @FXML private ListView globalSearchResults; + @FXML private VBox noResultsBox; + @FXML private ImageView noResultIcon; + private enum SearchMode { + GLOBAL, + CHAT + } + private SearchMode currentSearchMode = SearchMode.GLOBAL; + private UUID currentChatId; // if in CHAT mode, which chat to search in + + // === Search In Chat === @FXML private VBox chatSearchPane; // search results panel @FXML private ListView chatSearchResults; - @FXML private MenuButton scopeDropdown; - - // === TOP BAR === - @FXML private TextField searchBar; // global search bar - @FXML private Button menuButton; // === RIGHT PANE === @FXML private VBox leftPane; @@ -137,15 +143,104 @@ public class MainController { // } // }); - // وقتی کاربر Enter زد روی سرچ‌بار، درخواست سرچ بفرست - searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); + searchBar.setOnAction(e -> { + String keyword = searchBar.getText().trim(); + if (keyword.isEmpty()) return; - // باز/بسته کردن پنل نتایج با تایپ (دلخواه) - searchBar.textProperty().addListener((obs,o,n)->{ - if (n!=null && !n.isBlank()) showSearchPanel(); + if (currentSearchMode == SearchMode.GLOBAL) { + performGlobalSearch(keyword); + } else if (currentSearchMode == SearchMode.CHAT && currentChatId != null) { + performChatSearch(keyword, currentChatId); + } }); - // کلیک روی نتیجه + searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); + + searchBar.textProperty().addListener((obs,o,n)->{ + if (n!=null && !n.isBlank()) showGlobalSearchPanel(); + }); + + globalSearchResults.setOnMouseClicked(e -> { + int idx = globalSearchResults.getSelectionModel().getSelectedIndex(); + if (idx >= 0 && idx < searchBacking.size()) { + openSearchResult(searchBacking.get(idx)); + } + }); + + globalSearchResults.setCellFactory(list -> new ListCell<>() { + private final HBox container = new HBox(10); + private final ImageView avatar = new ImageView(); + private final VBox texts = new VBox(2); + private final Label title = new Label(); + private final Label subtitle = new Label(); + + { + avatar.setFitWidth(36); + avatar.setFitHeight(36); + avatar.getStyleClass().add("global-search-avatar"); + title.getStyleClass().add("global-search-title"); + subtitle.getStyleClass().add("global-search-subtitle"); + texts.getChildren().addAll(title, subtitle); + container.getChildren().addAll(avatar, texts); + } + + @Override + protected void updateItem(SearchResult r, boolean empty) { + super.updateItem(r, empty); + if (empty || r == null) { + setGraphic(null); + } else { + title.setText(r.title); + if (r.type == SRType.MESSAGE) { + subtitle.setText(r.subtitle != null ? r.subtitle : ""); + } else { + subtitle.setText("Press to see messages"); + } + + // Default profile images depending on type + switch (r.type) { + case USER: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png" + )) + )); + break; + case GROUP: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_group_profile.png" + )) + )); + break; + case CHANNEL: + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_channel_profile.png" + )) + )); + break; + case MESSAGE: + // For messages, use sender default (user style) + avatar.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png" + )) + )); + break; + } + + avatar.setFitWidth(50); + avatar.setFitHeight(50); + avatar.setPreserveRatio(true); + avatar.setSmooth(true); + + setGraphic(container); + } + } + }); + + // CLick on the search result chatSearchResults.setOnMouseClicked(e -> { int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); if (idx >= 0 && idx < searchBacking.size()) { @@ -166,11 +261,25 @@ public class MainController { searchBar.requestFocus(); } + public void showGlobalSearchPanel() { + scrollPane.setVisible(false); + scrollPane.setManaged(false); + + chatSearchPane.setVisible(false); + chatSearchPane.setManaged(false); + + globalSearchPane.setVisible(true); + globalSearchPane.setManaged(true); + } + @FXML public void closeSearchPanel() { chatSearchPane.setVisible(false); chatSearchPane.setManaged(false); + currentSearchMode = SearchMode.GLOBAL; + currentChatId = null; + scrollPane.setVisible(true); scrollPane.setManaged(true); } @@ -282,7 +391,7 @@ public class MainController { Node chatItem = loader.load(); ChatItemController controller = loader.getController(); - cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_user_profile.png", chat.getType()); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); @@ -325,6 +434,9 @@ public class MainController { ChatPageController controller = loader.getController(); controller.showChat(chat); + currentSearchMode = SearchMode.CHAT; + currentChatId = UUID.fromString(chat.getId().toString()); + chatDisplayArea.getChildren().setAll(chatPage); ChatItemController item = itemControllers.get(chat.getId()); @@ -441,11 +553,13 @@ public class MainController { private void updateIconsForDarkMode() { // Example: switch images to white versions menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_light.png"))); + noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_light.png"))); } private void updateIconsForLightMode() { // Example: switch images to black versions menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_dark.png"))); + noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_dark.png"))); } private void updateLabelsForDarkMode() { @@ -497,6 +611,49 @@ public class MainController { } } + private void performChatSearch(String keyword, UUID chatId) { + if (keyword == null || keyword.isBlank()) { + chatSearchResults.getItems().clear(); + return; + } + + showSearchPanel(); + + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "search_in_chat"); + req.put("keyword", keyword); + req.put("chat_id", chatId.toString()); + req.put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")); + + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + + if (resp == null || !"success".equals(resp.optString("status"))) return; + org.json.JSONArray arr = resp.optJSONObject("data").optJSONArray("results"); + if (arr == null) arr = new org.json.JSONArray(); + + java.util.List tmp = new java.util.ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject it = arr.getJSONObject(i); + String time = it.optString("time", ""); + String content = it.optString("content", "[No content]"); + tmp.add("🗨 " + content + (time.isEmpty() ? "" : " • " + time)); + } + + Platform.runLater(() -> { + chatSearchResults.getItems().setAll(tmp); + chatSearchResults.setVisible(true); + chatSearchResults.setManaged(true); + }); + }).start(); + } + public void performGlobalSearch(String keyword) { if (keyword == null || keyword.isBlank()) { @@ -510,7 +667,7 @@ public class MainController { return; } - showSearchPanel(); + showGlobalSearchPanel(); // درخواست به سرور (مثل کنسول) org.json.JSONObject req = new org.json.JSONObject(); @@ -597,17 +754,46 @@ public class MainController { private String subtitleSep(String s){ return s==null || s.isBlank()? "" : " — "; } - private void renderSearchResults(java.util.List results) { - searchBacking.clear(); - searchBacking.addAll(results); +// private void renderSearchResults(java.util.List results) { +// searchBacking.clear(); +// searchBacking.addAll(results); +// +// javafx.collections.ObservableList view = +// javafx.collections.FXCollections.observableArrayList(results); +// globalSearchResults.setItems(view); +// globalSearchResults.setVisible(true); +// globalSearchResults.setManaged(true); +// } - javafx.collections.ObservableList view = javafx.collections.FXCollections.observableArrayList(); - for (SearchResult r : results) view.add(r.toDisplay()); - chatSearchResults.setItems(view); - chatSearchResults.setVisible(true); - chatSearchResults.setManaged(true); + private void renderSearchResults(List results) { + globalSearchResults.getItems().clear(); + + if (results.isEmpty()) { + noResultsBox.setVisible(true); + noResultsBox.setManaged(true); + globalSearchResults.setVisible(false); + globalSearchResults.setManaged(false); + return; + } + + noResultsBox.setVisible(false); + noResultsBox.setManaged(false); + globalSearchResults.setVisible(true); + globalSearchResults.setManaged(true); + + globalSearchResults.getItems().addAll(results); } + @FXML + public void closeGlobalSearch() { + globalSearchPane.setVisible(false); + globalSearchPane.setManaged(false); + + scrollPane.setVisible(true); + scrollPane.setManaged(true); + + searchBar.clear(); + } private void openSearchResult(SearchResult r) { switch (r.type) { @@ -754,8 +940,4 @@ public class MainController { return false; } - - - - } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 6a5eff8..d1810ee 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -3,7 +3,6 @@ package org.to.telegramfinalproject.UI; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.Image; @@ -48,7 +47,7 @@ public class SidebarMenuController { @FXML public void initialize() { // Load default profile image - Image profile = loadImage("/org/to/telegramfinalproject/Avatars/default_profile.png"); + Image profile = loadImage("/org/to/telegramfinalproject/Avatars/default_user_profile.png"); if (profile != null) profileImage.setImage(profile); setupButtonActions(); @@ -197,7 +196,7 @@ public class SidebarMenuController { String img = user.optString("image_url", ""); Image pic = tryLoadImage(img); if (pic == null) { - pic = loadImage("/org/to/telegramfinalproject/Avatars/default_profile.png"); + pic = loadImage("/org/to/telegramfinalproject/Avatars/default_user_profile.png"); } if (pic != null) profileImage.setImage(pic); } diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..f9343f6beb60f6bcc99f4de717f073641d9bdb9f GIT binary patch literal 1837 zcmV+|2h#Y7P)aT%1l9+@W>r5C|YzrTXSztq}M04EL~T8*`y%-|l{URT%cv1=nWQfGg-i0qRmF9=VG0| zvSeDbZf_%`5iiPdyeJ*gBCWRAd7mi(#AU93{#?3ME#hrFkxzN;wq({O5y0hF#@`~7 zVrdpH$~UsE7L$>NEhPKKUAF-QkDRN_GkMFt=PMxnT@~I9bb1sCAh#Dl^i*S0{skcy z;%NPF@j0>cRP;5%%Mfpc78JhLzN;}?~=X~5R!JeIa;l4se_J^-TY8pI{4eYyu+1ulrD z00B$utunD?9h1muOKhbOjVFsQ8Y?6(V3|zMKcM)%Gb~LVz+w&ZVO?oOu_w-5qcWjY zM~n1|E2L>IHpi=FqPC5Sg_;*s7N50I@r{|2YkOB@8tp{p`6eyjm=N6o zAY_`WrnCG`8RW5)$z$n`*2LvmUu?IEfrFFD$Ja_KO`Zs2FDN=lkL zONeIOR$<1d-x*5E058hB-AzWN5iw8PbBv|cA}?T>EIfVH(uq@c7O3PSeWw`! zgs3$O&>5!+qz|=hzJ%<}Vsivj6=a;dwrb*r-6Ul6eA1yytR3059imD2X;I7?z?3ncBE z6L!yYjSoDk6hN+#G*efgMZf+@(vK99g=elQ8c#1crvyMNM(Y0KH4eZpFri$senZsy zm+6g-sVbTM{J zQUxp>5k%>!b<&ScT_zp74YiY4zl1rJInnYiI5|y2iWHAfq}6Z{NXW!solH6u_w5JjFjcDN7|GX*Tn+;py4884J1PKp#pk z@(PjuBR(LRg*Ph|K-7#Z#V;{8PkC-AGDS153f_LPVZJ?wJ+kW|enZH_je9B#pgT9t z=?qk@3RF`}UH%0Oo0wsww<*W-5v^u|2Khx7jFualwW2Ni<8I}zkuL)ogftwpB7e1C zV8S!Ujy`GAUZ()!GS?~J1jVMU!Up~J~l+Ynw;DjhdW24}I}GyJT)V&4)kxicGB9%bQtaU! zrAeM16z%_5NDp(3rj|@yLIxp?-#>Uddr%tx#J^7>@$wl+x&@CVF=@Q>rC*dbFm)Bn zvAHXc<)2-ZRC<}B4U?(M$RMPlq1ZHv$I|Z(Pu%`@Q$(?VGpja@~}hl6lF=rOzNj2M}foCTt2} zdPJWigguK18^eUDb`)WzXjq7C%!u(QmsVIBg4Qm8c}!@RUE{F{BLhw#LI-1rQ`!N{ zJ3yF}eh0#7qY*Az^u%4M;$>$)dN-d=u5=3@vDvQm`HHg_naCiz ze57NI2Zv^hnc*Fv4RH=EOXKI91CTZtvrp_AKNlSUnTIhw8aAGeCNh4mI-m*Ee5n0S zVmf*fyWQmrQ^j+;%NH7{MrBvJ1vH{KuFUAL&)4J#;Ee_IZryCYE#A;P|mXNbqcvM`~2<{hDZFc*u9 z2;GGV?Z<>pMzjwRRt0+xRnrE3JM1sKLgQCXgrDX{16BP<|pkyczcZ+jR@Q6MD4f0 zt@is%+@LTAW6^oSb|y`vRR;s8nx8u~Aa4>x`E*0a#ZYC&jlz8*7?rlOqi^9UwvII?b7A9DiNDpLK%R?}TD25F-MzO?tVDA6Nj zRZ${AQcfkp_4rr;#`I12)yJH`H3{S=|FFSX@czk>N!Vf_$;~T|5^Zm6HzFm;<90bb z9+m}_s`iNCO^RDA&zRJ44S~Z1>Tx2H<`aK#QPQ-> z4t5|An9r9?r*N%_Q0Weg*;(R!@;W?Nkejf*e1vpu>6f3ji;WO{5o7v|H?RCJCvg)B zl(blIPMhW$uBAXpgQ_5i@&qVsuM-gpI)w|(8uacMvnw`UUao}ijo;v<8+V#S-j=vN z-nLG~e)r4-?ld8!Y*msZ99bF56s}3MEi)ReFYWZ?TB#+h+t_;Akttl0@VUv81SMq| z9QT4K%T`sQsrzy?S_Og4H-e(l3v1GiYZ64*h`vDYRfImdC@9IH1SL*wI6fRDy1!_X ze)NYv7?cZB!ENj?aF-OCam^0Er*R|ihJY+6D%}Z<@BBqw_Sp6)5#3zR&vzw@+t~RV zybmPAHA!wRUBQ1Yf$KGu@BX{v!(R2Bu1o5{?eEDK)Xj&4u3qT-uZvwy5dQ2Fyt3RxkiwbuY!_tIa~(ikRU0Sw6T79 g&p|1amnq!;0NK=C5RI5xDF6Tf07*qoM6N<$g1CyhB>(^b literal 0 HcmV?d00001 diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_user_profile.png similarity index 100% rename from src/main/resources/org/to/telegramfinalproject/Avatars/default_profile.png rename to src/main/resources/org/to/telegramfinalproject/Avatars/default_user_profile.png diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css index 8360317..5e00d84 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css @@ -1,3 +1,9 @@ +/* ===== Dark Theme Root ===== */ +.root { + -fx-background-color: #0f1c26; /* overall dark base */ + -fx-text-fill: #e8f1f8; +} + /* Sidebar buttons */ .sidebar-btn { -fx-background-color: transparent; @@ -170,7 +176,7 @@ .search-placeholder { -fx-text-fill: #8ea1b2; -fx-font-size: 13px; } /* ===== Global search bar ===== */ -.global-search { +.global-search-pane { -fx-background-color: #1b2b39; -fx-background-radius: 15; -fx-text-fill: #e8f1f8; @@ -294,3 +300,82 @@ -fx-background-radius: 10; -fx-font-weight: bold; } + +/* === Global Search List (Dark) === */ +.global-search-list { + -fx-background-color: #1b2735; /* uniform dark background */ + -fx-control-inner-background: #1b2735; + -fx-control-inner-background-alt: #1b2735; /* disable stripes */ +} + +.global-search-list .virtual-flow, +.global-search-list .clipped-container, +.global-search-list .sheet { + -fx-background-color: #1b2735; /* internal containers same */ +} + +.global-search-list .list-cell { + -fx-padding: 8 10; + -fx-background-color: #1b2735; /* solid dark rows */ + -fx-border-color: #2e3948; /* subtle divider */ + -fx-border-width: 0 0 0.5 0; + -fx-alignment: CENTER_LEFT; + -fx-text-fill: #e8f1f8; /* light text */ +} + +.global-search-list .list-cell:hover { + -fx-background-color: rgba(255, 255, 255, 0.08); /* lighter on hover */ +} + +.global-search-list .list-cell:selected { + -fx-background-color: #2a3a4a; /* darker blue-gray on select */ +} + +/* Titles & subtitles */ +.global-search-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #e8f1f8; +} +.global-search-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #8ea1b2; /* muted gray-blue */ +} + +/* Search header */ +.search-header { + -fx-padding: 8 12; + -fx-background-color: #1b2735; + -fx-border-color: #2e3948; + -fx-border-width: 0 0 1 0; +} +.search-header-label { + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-text-fill: #e8f1f8; +} + +/* No results */ +.no-results-label { + -fx-font-size: 12px; + -fx-text-fill: #8ea1b2; +} + +/* Avatar placeholders */ +.global-search-avatar { + -fx-background-radius: 50%; + -fx-background-color: #444; + -fx-min-width: 36; + -fx-min-height: 36; +} + +/* Close button */ +.search-close-btn { + -fx-background-color: transparent; + -fx-text-fill: #e8f1f8; + -fx-font-size: 14px; + -fx-cursor: hand; +} +.search-close-btn:hover { + -fx-text-fill: #2ca4ff; +} diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css index f2b261d..5bf2e2e 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css @@ -1,3 +1,8 @@ +.root { + -fx-base: #ffffff; + -fx-text-base-color: #000000; +} + /* Sidebar buttons */ .sidebar-btn { -fx-background-color: transparent; @@ -195,7 +200,7 @@ -fx-cursor: hand; } -.global-search { +.global-search-pane { -fx-background-color: #f5f5f5; -fx-background-radius: 15; -fx-text-fill: #0f141a; @@ -293,3 +298,104 @@ -fx-background-radius: 10; -fx-font-weight: bold; } + +/* === Global Search List === */ +.global-search-list .list-cell { + -fx-padding: 8 10; + -fx-background-color: transparent; + -fx-border-color: #ddd; /* light divider */ + -fx-border-width: 0 0 0.5 0; + -fx-alignment: CENTER_LEFT; +} + +.global-search-list .list-cell:hover { + -fx-background-color: rgba(0, 0, 0, 0.05); /* light hover */ +} + +.global-search-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: black; +} + +.global-search-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #666; /* gray for light mode */ +} + +/* Avatar placeholder (if no image) */ +.global-search-avatar { + -fx-background-radius: 50%; + -fx-background-color: #bbb; + -fx-min-width: 36; + -fx-min-height: 36; +} + +/* Global search panel */ +.global-search-pane { + -fx-background-color: -fx-base; +} + +.search-header { + -fx-padding: 8 12; + -fx-background-color: transparent; + -fx-border-color: #ddd; + -fx-border-width: 0 0 1 0; +} + +.search-header-label { + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-text-fill: -fx-text-base-color; +} + +.no-results-label { + -fx-font-size: 12px; + -fx-text-fill: gray; +} + +.close-btn { + -fx-background-color: transparent; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +/* ===== Global Search Result Panel ===== */ +.search-results-container { + -fx-background-color: -fx-base; + -fx-padding: 8; + -fx-spacing: 6; + -fx-border-color: transparent transparent #ddd transparent; + -fx-border-width: 0 0 1 0; +} + +.search-results-title { + -fx-font-weight: bold; + -fx-font-size: 13px; + -fx-text-fill: -fx-text-base-color; +} + +.search-result-item { + -fx-background-color: transparent; + -fx-padding: 6 10; + -fx-spacing: 8; +} + +.search-result-item:hover { + -fx-background-color: rgba(0,0,0,0.05); +} + +/* Avatar always square, not squeezed */ +.search-result-avatar { + -fx-fit-width: 40; + -fx-fit-height: 40; + -fx-preserve-ratio: true; +} + +/* Close button */ +.search-close-btn { + -fx-background-color: transparent; + -fx-text-fill: -fx-text-base-color; + -fx-font-size: 14px; + -fx-cursor: hand; +} diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml index d6db77f..62f4177 100644 --- a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml @@ -69,44 +69,54 @@ - + -