diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index d27b6cb..b2b16ed 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -27,13 +27,36 @@ 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 = ""; + // ===== UI hook for search results ===== + + + public String getLastStatus() { return lastStatus; } + public String getLastMessage() { return lastMessage; } + public boolean wasSuccess() { return "success".equalsIgnoreCase(lastStatus); } + + + +// private void handleRealTime(JSONObject json) throws IOException { +// IncomingMessageListener listener = new IncomingMessageListener(this.in); +// listener.handleRealTimeEvent (json); +// } + + // ActionHandler.java private void handleRealTime(JSONObject json) throws IOException { - IncomingMessageListener listener = new IncomingMessageListener(this.in); - listener.handleRealTimeEvent (json); + IncomingMessageListener listener = TelegramClient.getInstance().getListener(); + if (listener != null) { + listener.handleRealTimeEvent(json); + } else { + System.err.println("[RT] Listener not ready; dropping RT event: " + json); + } } + public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { this.out = out; this.in = in; @@ -42,6 +65,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: "); @@ -122,6 +156,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: "); @@ -197,7 +243,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 +252,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 +262,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 +302,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 +371,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 +496,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 +534,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": @@ -622,6 +691,8 @@ public class ActionHandler { } break; case "search" : + + JSONArray results = response.getJSONObject("data").getJSONArray("results"); if (results.isEmpty()) { @@ -1057,7 +1128,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 +1156,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 +1186,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 +1310,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 +1319,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 +1343,6 @@ public class ActionHandler { displayIndex++; } - // انتخاب System.out.print("Select a chat by number: "); int choice; try { @@ -1298,11 +1364,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 +1406,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 +1639,7 @@ public class ActionHandler { // } - private boolean showPrivateChatMenu(ChatEntry chat) { + public boolean showPrivateChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; @@ -1677,7 +1743,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 +1757,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 +1870,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 +2028,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 +2108,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 +2169,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 +2246,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 +2308,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 +2336,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 +2396,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 +2479,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 +2502,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 +2524,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 +2541,7 @@ public class ActionHandler { - private void toggleBlock(UUID userId) { + public void toggleBlock(UUID userId) { JSONObject req = new JSONObject(); @@ -2497,7 +2563,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 +2584,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 +2644,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 +2708,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 +2720,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 +2736,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 +2765,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 +2841,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 +2863,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 +2892,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 +2967,7 @@ public class ActionHandler { } - private JSONObject getResponse() { + public JSONObject getResponse() { try { return TelegramClient.responseQueue.take(); @@ -2931,7 +2997,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 +3094,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 +3119,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 +3419,7 @@ public class ActionHandler { - private void startMenuRefresherThread() { + public void startMenuRefresherThread() { Thread refresher = new Thread(() -> { System.out.println("🟡 Refresher tick. refreshCurrentChatMenu = " + Session.refreshCurrentChatMenu); @@ -3382,7 +3448,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 +3512,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 +3537,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 +3562,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 +3588,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 +3597,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 +3729,6 @@ public class ActionHandler { } } - // 🔹 فقط ارسال پیام با chat_id و receiver_type JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); messageJson.put("receiver_type", receiverType); @@ -3683,7 +3748,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 +3806,7 @@ public class ActionHandler { } } - private void viewMessagesInChat(ChatEntry chat) { + public void viewMessagesInChat(ChatEntry chat) { int offset = 0; int limit = 10; @@ -3910,7 +3975,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 +3999,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 +4022,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 +4066,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 +4095,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 +4143,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/AvatarLocalResolver.java b/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java new file mode 100644 index 0000000..2458d99 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java @@ -0,0 +1,47 @@ +package org.to.telegramfinalproject.Client; + +import javafx.scene.image.Image; +import java.nio.file.*; + +public final class AvatarLocalResolver { + private static final Path UPLOADS_ROOT = Paths.get( + System.getProperty("app.uploads.root", "uploads") + ).toAbsolutePath().normalize(); + + private static boolean isHttp(String s) { + return s.startsWith("http://") || s.startsWith("https://"); + } + + public static String resolve(String serverValue) { + if (serverValue == null || serverValue.isBlank()) return null; + if (isHttp(serverValue) || serverValue.startsWith("file:")) return serverValue; + + String rel = serverValue.startsWith("/") ? serverValue.substring(1) : serverValue; // "avatars/..." + Path p = UPLOADS_ROOT.resolve(rel).normalize(); + if (!p.startsWith(UPLOADS_ROOT)) return null; // امنیت مسیر + if (!Files.exists(p)) { // مهم: واقعاً وجود دارد؟ + System.err.println("Avatar resolve: NOT FOUND -> " + p); + return null; + } + String url = p.toUri().toString(); // file:///.../uploads/avatars/.... + System.out.println("Avatar resolve: " + serverValue + " -> " + url); + return url; + } + + public static Image load(String serverValue) { + String url = resolve(serverValue); + if (url == null) return null; + + // فقط برای http/https کش‌بریکر + if (isHttp(url)) { + url += (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis(); + } + + Image img = new Image(url, false); // sync load تا خطا را همان‌جا بفهمیم + if (img.isError()) { + System.err.println("Avatar load failed: " + url + " -> " + img.getException()); + return null; + } + return img; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index e6882a2..afded31 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -1,21 +1,36 @@ package org.to.telegramfinalproject.Client; +import javafx.application.Platform; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.UI.MainController; import java.io.BufferedReader; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; public class IncomingMessageListener implements Runnable { private final BufferedReader in; + public enum UIMode { CONSOLE, UI } + private final UIMode uiMode; // runtime mode - public IncomingMessageListener(BufferedReader in) { + private final java.util.Set seenMessageIds = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + + + public IncomingMessageListener(BufferedReader in, UIMode uiMode) { this.in = in; + this.uiMode = uiMode; } +// public IncomingMessageListener(BufferedReader in) { +// this.in = in; +// } + @Override public void run() { try { @@ -79,7 +94,7 @@ public class IncomingMessageListener implements Runnable { "update_group_or_channel", "chat_deleted", "blocked_by_user", "unblocked_by_user", "message_seen", "removed_from_group", "removed_from_channel", - "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true; + "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true; default -> false; }; } @@ -88,13 +103,15 @@ public class IncomingMessageListener implements Runnable { String action = response.getString("action"); JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject(); - + JSONObject finalMsg = msg; + JSONObject finalMsg1 = msg; switch (action) { case "added_to_group", "added_to_channel", - "removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> { + "removed_from_group", "removed_from_channel", + "chat_deleted", "created_private_chat" -> { + // این قسمت مستقل از UI/کنسول است System.out.println("🔄 Chat list changed. Updating..."); Session.forceRefreshChatList = true; - System.out.println("🧪 Calling requestChatList() after being added"); String chatId = msg.getString("chat_id"); String chatType = msg.getString("chat_type"); @@ -106,36 +123,81 @@ public class IncomingMessageListener implements Runnable { } } - case "chat_updated" -> { - System.out.println("\n🔄 Chat info updated."); +// case "chat_updated" -> { +// if (uiMode == UIMode.UI) { +// bumpChatListFromUpdate(msg); // برای UI (سایدبار و سورت) +// } else { +// updateLastMessageTime(msg); // برای کنسول (لیست‌های Session) +// } +// } - if (msg.has("last_message_time")) { - updateLastMessageTime(msg); - } else { - new Thread(() -> { - try { - handleAdminRoleChanged(msg); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); - } - } - - - case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> { + case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { try { - handleAdminRoleChanged(msg); //new thread + handleAdminRoleChanged(finalMsg1); } catch (IOException e) { e.printStackTrace(); } }).start(); } + case "new_message" -> { + JSONObject data = response.optJSONObject("data"); + if (data == null) break; - default -> displayRealTimeMessage(action, msg); + String mid = data.optString("id", data.optString("message_id","")); + if (mid.isEmpty()) break; + if (!seenMessageIds.add(mid)) break; + + bumpChatListFromMessage(data); + + JSONObject uiMsg = new JSONObject(data.toString()); + if (!uiMsg.has("message_id") && uiMsg.has("id")) { + uiMsg.put("message_id", uiMsg.getString("id")); + } + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + var chatCtl = (mc != null) ? mc.getChatPageController() : null; + if (chatCtl != null) { + chatCtl.onRealTimeNewMessage(uiMsg); + } + }); + } + + + case "chat_updated" -> { + var data = response.getJSONObject("data"); + bumpChatListFromUpdate(data); + } + + case "user_status_changed" -> { + + displayRealTimeMessage(action, msg); + Platform.runLater(() -> { + var mc = org.to.telegramfinalproject.UI.MainController.getInstance(); + var cp = (mc != null) ? mc.getChatPageController() : null; + if (cp != null) cp.onUserStatusChanged( + msg.optString("user_id",""), + msg.optString("status",""), + msg.optString("last_seen","") + ); + }); + } + + + + + case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted" + , "blocked_by_user", "unblocked_by_user", "message_seen" -> { + displayRealTimeMessage(action, msg); + } + + default -> { + System.out.println("\n❓ Unknown real-time action: " + action); + System.out.println(msg.toString(2)); + } } System.out.print(">> "); @@ -362,4 +424,58 @@ public class IncomingMessageListener implements Runnable { } } } + + + + private static LocalDateTime parseIsoFlexible(String iso) { + if (iso == null || iso.isBlank()) return null; + try { return LocalDateTime.parse(iso); } catch (Exception ignore) {} + try { return OffsetDateTime.parse(iso).toLocalDateTime(); } catch (Exception ignore) {} + return null; + } + + + + private void bumpChatListFromUpdate(JSONObject data) { + try { + UUID chatId = UUID.fromString(data.optString("chat_id","")); + String chatType = data.optString("chat_type",""); + LocalDateTime ts = parseIsoFlexible(data.optString("last_message_time", null)); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ false, null); + }); + } catch (Exception e) { System.err.println("[RT] bumpChatListFromUpdate: " + e.getMessage()); } + } + + + + private String previewOf(String type, String content) { + String t = type == null ? "" : type.trim().toUpperCase(); + return switch (t) { + case "IMAGE" -> "[Image]"; + case "AUDIO" -> "[Audio]"; + case "VIDEO" -> "[Video]"; + case "FILE" -> "[File]"; + default -> (content == null ? "" : content); + }; + } + + private void bumpChatListFromMessage(JSONObject m) { + try { + UUID chatId = UUID.fromString(m.optString("receiver_id","")); + String chatType = m.optString("receiver_type",""); + LocalDateTime ts = parseIsoFlexible(m.optString("send_at", null)); + + String preview = previewOf(m.optString("message_type","TEXT"), + m.optString("content","")); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ true, preview); + }); + } catch (Exception e) { System.err.println("[RT] bumpChatListFromMessage: " + e.getMessage()); } + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 8db405e..eb68156 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,112 @@ 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 { +// connectIfNeeded(); +// initHandlerIfNeeded(); +// startListenerOnce(); +// showMainMenu(); +// } catch (IOException e) { +// System.err.println("❌ Error connecting to server: " + e.getMessage()); +// } +// } + + 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(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول 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; +// } + + public static synchronized TelegramClient getOrInitForUI() throws IOException { + TelegramClient cli = getInstance(); + cli.connectIfNeeded(); + cli.initHandlerIfNeeded(); + cli.startListenerOnce(IncomingMessageListener.UIMode.UI); // ← UI + 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(); +// } + + + private void startListenerOnce(IncomingMessageListener.UIMode mode) { + if (listenerStarted) return; + listenerStarted = true; + + Thread listenerThread = new Thread( + new IncomingMessageListener(in, mode), + "socket-listener" + ); + listenerThread.setDaemon(true); + listenerThread.start(); + } + + //console private void showMainMenu() throws IOException { while (true) { System.out.println("Main Menu:"); @@ -73,8 +266,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 +286,29 @@ 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; + // TelegramClient.java + private IncomingMessageListener listener; + + public IncomingMessageListener getListener() { + return listener; } } + diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java new file mode 100644 index 0000000..63a3eec --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java @@ -0,0 +1,130 @@ +package org.to.telegramfinalproject.Database; + +import org.json.JSONObject; + +import java.sql.*; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class ChatInfoDatabase { + + /** + * هدر چت را بر اساس نوع آن برمی‌گرداند. + * private: name,image_url, online,last_seen + * group : name,image_url, member_count + * channel: name,image_url, member_count + */ + public static JSONObject getHeaderInfo(String type, UUID receiverId, UUID viewerId) throws SQLException { + try (Connection conn = ConnectionDb.connect()) { + switch (type.toLowerCase()) { + case "private": + return getPrivateHeader(conn, receiverId, viewerId); + case "group": + return getGroupHeader(conn, receiverId); + case "channel": + return getChannelHeader(conn, receiverId); + default: + throw new IllegalArgumentException("Unsupported receiver_type: " + type); + } + } + } + + /** هدر چت خصوصی: other-user نسبت به viewerId را پیدا می‌کنیم */ + private static JSONObject getPrivateHeader(Connection conn, UUID chatId, UUID viewerId) throws SQLException { + if (viewerId == null) throw new IllegalArgumentException("viewerId is required for private header."); + + final String sql = """ + SELECT + u.profile_name AS name, + COALESCE(u.image_url, '') AS image_url, + (u.status = 'online') AS online, + u.status AS status, + u.last_seen AS last_seen + FROM private_chat pc + JOIN users u + ON u.internal_uuid = CASE WHEN pc.user1_id = ? THEN pc.user2_id ELSE pc.user1_id END + WHERE pc.chat_id = ? + AND (pc.user1_id = ? OR pc.user2_id = ?) + LIMIT 1 + """; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + int i = 1; + ps.setObject(i++, viewerId); + ps.setObject(i++, chatId); + ps.setObject(i++, viewerId); + ps.setObject(i++, viewerId); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Private chat not found or viewer is not a participant."); + + JSONObject j = new JSONObject(); + j.put("type", "private"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + + // خواندن بولینِ online با سازگاری DB + Object onlineObj = rs.getObject("online"); + boolean online = (onlineObj instanceof Boolean) ? (Boolean) onlineObj + : rs.getInt("online") == 1; + j.put("online", online); + j.put("status", rs.getString("status")); // رشته‌ی وضعیت هم می‌آید + + Timestamp ts = rs.getTimestamp("last_seen"); + j.put("last_seen", ts == null ? JSONObject.NULL : ts.toInstant().toString()); + return j; + } + } + } + + + /** هدر گروه: نام، عکس و تعداد اعضا */ + private static JSONObject getGroupHeader(Connection conn, UUID groupInternalId) throws SQLException { + String sql = """ + SELECT + g.group_name AS name, + COALESCE(g.image_url, '') AS image_url, + (SELECT COUNT(*) FROM group_members gm WHERE gm.group_id = g.internal_uuid) AS member_count + FROM groups g + WHERE g.internal_uuid = ? + LIMIT 1 + """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, groupInternalId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Group not found."); + JSONObject j = new JSONObject(); + j.put("type", "group"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + j.put("member_count", rs.getInt("member_count")); + return j; + } + } + } + + /** هدر کانال: نام، عکس و تعداد سابسکرایبر */ + private static JSONObject getChannelHeader(Connection conn, UUID channelInternalId) throws SQLException { + String sql = """ + SELECT + c.channel_name AS name, + COALESCE(c.image_url, '') AS image_url, + (SELECT COUNT(*) FROM channel_subscribers cs WHERE cs.channel_id = c.internal_uuid) AS member_count + FROM channels c + WHERE c.internal_uuid = ? + LIMIT 1 + """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, channelInternalId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Channel not found."); + JSONObject j = new JSONObject(); + j.put("type", "channel"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + j.put("member_count", rs.getInt("member_count")); + return j; + } + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 1e6efb1..af0033d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -381,72 +381,177 @@ 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<>(); 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 + ps.setObject(i++, userId); // pc.user2_id + 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)); } - } 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<>(); String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?"; @@ -1057,4 +1162,181 @@ 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; + } + + + + 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/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 8f57ff8..3fefe32 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; @@ -117,8 +128,6 @@ public class ChatEntry { this.archived = archived; } -// public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); -// } public void setLastMessageTime(String newTime) { @@ -149,4 +158,26 @@ 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; } + + public void setUnread(int unreadCount){this.unreadCount = unreadCount;} + public int getUnread(){return unreadCount;} + + + public void setLastMessageTime(LocalDateTime t) { + this.lastMessageTime = t; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/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..cccdc4c 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.*; @@ -125,78 +126,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 +158,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 +201,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, user.getInternal_uuid()); if (archivedChatIds.contains(group.getInternal_uuid())) { archivedChatList.add(entry); @@ -309,6 +241,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 +708,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 +718,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 +762,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 +801,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, currentUser.getInternal_uuid()); if (archivedChatIds.contains(channel.getInternal_uuid())) { archivedChatList.add(entry); @@ -1406,6 +1345,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."); @@ -1523,21 +1616,16 @@ public class ClientHandler implements Runnable { break; } - // دریافت شناسه چت و نوع حذف (یک‌طرفه یا دوطرفه) UUID targetId = UUID.fromString(requestJson.getString("chat_id")); boolean both = requestJson.getBoolean("both"); - // Real-Time Event Dispatch (اطلاع‌رسانی ریل تایم) if (both) { - // حذف دوطرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); RealTimeEventDispatcher.notifyChatDeleted("private", currentUser.getInternal_uuid(), List.of(targetId)); } else { - // حذف یک‌طرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); } - // فراخوانی متد حذف چت response = handleDeleteChat(requestJson); break; } @@ -2642,6 +2730,115 @@ 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; + } + + case "get_other_user_status": { + try { + String userIdStr = requestJson.getString("user_id"); + UUID user_id = UUID.fromString(userIdStr); + + String lastSeenStr = userDatabase.getLastSeen(user_id); // returns ISO string? + JSONObject data = new JSONObject(); + + if (lastSeenStr != null && !"Unknown".equals(lastSeenStr)) { + // parse string to time + LocalDateTime lastSeen = LocalDateTime.parse(lastSeenStr); + long diffMillis = java.time.Duration.between(lastSeen, LocalDateTime.now()).toMillis(); + + boolean online = diffMillis <= 120_000; + + data.put("online", online); + data.put("last_seen", lastSeen.toString()); + + response = new ResponseModel("success", "Status fetched.", data); + } else { + response = new ResponseModel("error", "User not found."); + } + + } catch (Exception e) { + e.printStackTrace(); + response = new ResponseModel("error", "Failed to fetch status."); + } + break; + } + + + + case "get_header_info": { + try { + String type = requestJson.getString("receiver_type"); // "private" | "group" | "channel" + UUID receiverId = UUID.fromString(requestJson.getString("receiver_id")); + + UUID viewerId = null; + if ("private".equalsIgnoreCase(type)) { + String v = requestJson.optString("viewer_id", + requestJson.optString("my_id", null)); + if (v == null) { + response = new ResponseModel("error", "viewer_id (or my_id) is required for private chats."); + break; + } + viewerId = UUID.fromString(v); + } + + org.json.JSONObject data = org.to.telegramfinalproject.Database.ChatInfoDatabase + .getHeaderInfo(type, receiverId, viewerId); + + response = new ResponseModel("success", "Header info fetched.", data); + + } catch (Exception e) { + e.printStackTrace(); + response = new ResponseModel("error", "Failed to fetch header info."); + } + break; + } + + default: response = new ResponseModel("error", "Unknown action: " + action); } @@ -2768,6 +2965,8 @@ public class ClientHandler implements Runnable { RealTimeEventDispatcher.sendToUser(receiver, chatPayload); } + RealTimeEventDispatcher.sendToUser(senderId, chatPayload); + JSONObject data = new JSONObject(); data.put("message_id", messageId.toString()); return new ResponseModel("success", "Message sent successfully.", data); @@ -2855,4 +3054,35 @@ 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/AvatarFX.java b/src/main/java/org/to/telegramfinalproject/UI/AvatarFX.java new file mode 100644 index 0000000..c26d0f4 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/AvatarFX.java @@ -0,0 +1,21 @@ +package org.to.telegramfinalproject.UI; + +import javafx.beans.binding.Bindings; +import javafx.scene.image.ImageView; +import javafx.scene.shape.Circle; + +public final class AvatarFX { + + public static void circleClip(ImageView iv, double sizePx) { + iv.setFitWidth(sizePx); + iv.setFitHeight(sizePx); + iv.setPreserveRatio(true); + iv.setSmooth(true); + + Circle c = new Circle(); + c.radiusProperty().bind(Bindings.min(iv.fitWidthProperty(), iv.fitHeightProperty()).divide(2)); + c.centerXProperty().bind(iv.fitWidthProperty().divide(2)); + c.centerYProperty().bind(iv.fitHeightProperty().divide(2)); + iv.setClip(c); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index 5fd3f33..4b61f3a 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -6,6 +6,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.scene.shape.Circle; +import org.to.telegramfinalproject.Client.AvatarLocalResolver; import java.util.Objects; @@ -21,10 +22,6 @@ public class ChatItemController { @FXML private ImageView profileImageSystem; @FXML private Circle systemCircle; - // Default avatar resource path - private static final String DEFAULT_AVATAR = - "/org/to/telegramfinalproject/Avatars/default_avatar.png"; - /** * Set chat item data, including a profile image (or default). * @@ -34,7 +31,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); @@ -72,17 +69,52 @@ public class ChatItemController { systemAvatar.setManaged(true); } else if (imageUrl != null && !imageUrl.isEmpty()) { - profileImageUser.setImage(new Image(imageUrl, true)); +// profileImageUser.setImage(new Image(imageUrl, true)); +// profileImageUser.setVisible(true); +// profileImageUser.setManaged(true); + + Image img = AvatarLocalResolver.load(imageUrl); + if (img != null) { + profileImageUser.setImage(img); + AvatarFX.circleClip(profileImageUser, 40); + } 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( + java.util.Objects.requireNonNull(getClass().getResourceAsStream(path)) + )); + } profileImageUser.setVisible(true); 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); } } + + public void setUnread(int unread) { + boolean show = unread > 0; + unreadCount.setVisible(show); + unreadCount.setManaged(show); + if (show) unreadCount.setText(String.valueOf(unread)); + } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 6c0aa7c..45a0000 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -10,48 +10,151 @@ 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.Client.AvatarLocalResolver; +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.time.temporal.ChronoUnit; +import java.util.Objects; +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; + + // ===== 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(); + private final java.util.Set pendingClientMsgIds = + java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); // Where your icons live private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; + 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 + .currentUser.getString("internal_uuid"); + me = UUID.fromString(meStr); + } catch (Exception ignore) { + me = null; + } + } + + 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(); + 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 if (sendButton != null) { sendButton.setOnAction(e -> sendMessage()); @@ -157,44 +260,195 @@ 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(); } - /** Called by main controller when opening a chat. */ - public void setChat(String chatName, String avatarPath) { - this.chatName = chatName; - - // Header text (if present) - if (chatTitle != null) chatTitle.setText(chatName); - if (chatStatus != null) chatStatus.setText("last seen recently"); // or live status - - // Load avatar if provided - if (userAvatar != null && avatarPath != null) { - try { - Image avatarImg = new Image(getClass().getResourceAsStream(avatarPath)); - userAvatar.setImage(avatarImg); - Circle clip = new Circle(18, 18, 18); // x, y, radius - userAvatar.setClip(clip); - } catch (Exception e) { - System.err.println("Could not load avatar: " + avatarPath); - } - } - - addSystemMessage("Chat with " + chatName + " opened."); - Platform.runLater(() -> messageInput.requestFocus()); - } +// /** +// * Called by main controller when opening a chat. +// */ +// public void setChat(String chatName, String avatarPath) { +// this.chatName = chatName; +// +// // Header text (if present) +// if (chatTitle != null) chatTitle.setText(chatName); +// if (chatStatus != null) chatStatus.setText("last seen recently"); // or live status +// +// // Load avatar if provided +// if (userAvatar != null && avatarPath != null) { +// try { +// Image avatarImg = new Image(getClass().getResourceAsStream(avatarPath)); +// userAvatar.setImage(avatarImg); +// Circle clip = new Circle(18, 18, 18); // x, y, radius +// userAvatar.setClip(clip); +// } catch (Exception e) { +// System.err.println("Could not load avatar: " + avatarPath); +// } +// } +// +// addSystemMessage("Chat with " + chatName + " opened."); +// Platform.runLater(() -> messageInput.requestFocus()); +// } // ----- actions ----- +// private void sendMessage() { +// String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); +// if (!text.isEmpty()) { +// addMessage("You", text); +// messageInput.clear(); +// // TODO: send to server +// } +// } + + private void sendMessage() { - String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); - if (!text.isEmpty()) { - addMessage("You", text); - messageInput.clear(); - // TODO: send to server + // 0) Read and validate input + String raw = messageInput.getText(); + String text = (raw == null) ? "" : raw.trim(); + if (text.isEmpty()) return; + + if (currentChat == null) { + addSystemMessage("No chat is selected."); + return; } + + // 1) Clear input immediately for good UX + messageInput.clear(); + + // 2) Snapshot chat info (must be final for lambdas) + final UUID targetChatId = currentChat.getId(); + final String targetType = currentChat.getType(); // "private" | "group" | "channel" + final String contentToSend = text; // effectively final + + // 3) Build the SAME JSON as your console method (for TEXT only) + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "send_message"); + req.put("receiver_type", targetType); + req.put("receiver_id", targetChatId.toString()); + req.put("content", contentToSend); + req.put("message_type", "TEXT"); + + // 4) Send on a background thread + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + Platform.runLater(() -> addSystemMessage("Send failed: " + ex.getMessage())); + return; + } + + // 5) Check status like your console method + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + String err = (resp != null) ? resp.optString("message", "No response") : "No response"; + Platform.runLater(() -> addSystemMessage("Send failed: " + err)); + return; + } + + // 6) Extract fields (your console reads data.message_id; handle both shapes) + org.json.JSONObject data = resp.optJSONObject("data"); + String messageId = null; + String sendAtIso = null; + if (data != null) { + // If server returns { data: { message_id, send_at, ... } } + messageId = data.optString("message_id", null); + + // Some servers nest: { data: { message: {...} } } + if (messageId == null) { + org.json.JSONObject msgObj = data.optJSONObject("message"); + if (msgObj != null) { + messageId = msgObj.optString("message_id", null); + sendAtIso = msgObj.optString("send_at", null); + } + } else { + sendAtIso = data.optString("send_at", null); + } + } + if (messageId == null) messageId = java.util.UUID.randomUUID().toString(); + + final java.time.LocalDateTime ts = + (sendAtIso != null && !sendAtIso.isBlank()) ? parseWhen(sendAtIso) + : java.time.LocalDateTime.now(); + + final String fMessageId = messageId; + final java.time.LocalDateTime fTs = ts; + + // 7) Update UI on FX thread (render outgoing bubble + index for reply previews) + Platform.runLater(() -> { + // If user switched chats while sending, don’t render here + if (currentChat == null || !currentChat.getId().equals(targetChatId)) return; + + addBubble( + true, // outgoing + "You", // display name + "TEXT", // message type + contentToSend, // content + fTs, // timestamp + fMessageId, // message_id + "", "", "", // forwarded_from, forwarded_by, reply_to_id + false, // edited + null // reactions + ); + + //Real time + var mc = MainController.getInstance(); + if (mc != null) { + String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend); + mc.onChatUpdated(targetChatId, targetType, fTs, /*isIncoming*/ false, preview); + } + + // Keep it in msgIndex for reply previews + org.json.JSONObject idx = new org.json.JSONObject(); + idx.put("message_id", fMessageId); + idx.put("message_type", "TEXT"); + idx.put("content", contentToSend); + idx.put("sender_name", "You"); + idx.put("sender_id", (me != null) ? me.toString() : ""); + idx.put("send_at", fTs.toString()); + msgIndex.put(fMessageId, idx); + }); + }).start(); } private void openFileChooser() { @@ -210,22 +464,24 @@ public class ChatPageController { // ----- UI helpers ----- - /** 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"; - msg.setStyle( - "-fx-background-color: " + bubbleColor + ";" + - "-fx-text-fill: " + textColor + ";" + - "-fx-padding: 6 10; -fx-background-radius: 10;" - ); - - messageContainer.getChildren().add(msg); - } +// /** +// * 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"; +// msg.setStyle( +// "-fx-background-color: " + bubbleColor + ";" + +// "-fx-text-fill: " + textColor + ";" + +// "-fx-padding: 6 10; -fx-background-radius: 10;" +// ); +// +// messageContainer.getChildren().add(msg); +// } private void addSystemMessage(String content) { Label sys = new Label(content); @@ -235,7 +491,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 +532,584 @@ public class ChatPageController { } return new Image(url.toExternalForm()); } + + public void showChat(ChatEntry entry) { + this.currentChat = entry; + + chatTitle.setText(entry.getName()); + + // آواتار پیش‌فرض بر اساس نوع + // ChatPageController.showChat(...) + if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { + Image img = AvatarLocalResolver.load(entry.getImageUrl()); + if (img != null) { + userAvatar.setImage(img); + } else { + // ⬇️ فال‌بک بر اساس نوع + setDefaultHeaderAvatarByType(entry.getType()); + } + } else { + setDefaultHeaderAvatarByType(entry.getType()); + } +// userAvatar.setClip(new Circle(20, 20, 20)); + AvatarFX.circleClip(userAvatar, 36); + + + + fetchAndRenderHeader(entry); + + // پیام‌ها + 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_UI"); + 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; + + 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(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(); + + // برای reply-preview: ایندکس کردن پیام‌ها با message_id + msgIndex.clear(); + for (int i = 0; i < list.length(); i++) { + org.json.JSONObject m = list.getJSONObject(i); + String mid = str(m, "message_id"); + if (!mid.isEmpty()) msgIndex.put(mid, m); + } + + String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid")) + ? Session.currentUser.getString("internal_uuid") : ""; + + for (int i = 0; i < list.length(); i++) { + org.json.JSONObject m = list.getJSONObject(i); + + String senderId = str(m, "sender_id"); + String senderName = str(m, "sender_name"); + String type = str(m, "message_type"); + String content = str(m, "content"); + String whenStr = str(m, "send_at"); + String msgId = str(m, "message_id"); + + // فوروارد / ریپلای / ادیت / ری‌اکشن + String fwdFrom = nz(str(m, "forwarded_from")); + String fwdBy = nz(str(m, "forwarded_by")); + String replyToId = nz(str(m, "reply_to_id")); + boolean edited = bool(m, "is_edited"); + org.json.JSONArray reactions = arr(m, "reactions"); + + boolean outgoing = senderId.equalsIgnoreCase(myId); + if (senderName == null || senderName.isBlank()) { + senderName = outgoing ? "You" + : (senderId == null || senderId.isBlank() + ? "Unknown" + : senderId.substring(0, Math.min(8, senderId.length()))); + } + + java.time.LocalDateTime ts = parseWhen(whenStr); + + // نمایش + addBubble(outgoing, senderName, type, content, ts, msgId, + fwdFrom, fwdBy, replyToId, edited, reactions); + } + + // کمی فاصله بین پیام‌ها + messageContainer.setSpacing(8); + 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) { + 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 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); +// } + + 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 + ) { + String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt); + if (edited) metaText += " (edited)"; + Label meta = new Label(metaText); + meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); + meta.setWrapText(true); + + String t = type == null ? "" : type.trim().toUpperCase(); + boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); + String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t); + + Label msg = new Label(bodyText); + 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); // spacing عمودی داخل حباب + bubble.getChildren().add(meta); + + // Forward header (اختیاری) + if (hasVal(forwardedFrom) || hasVal(forwardedBy)) { + bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); + } + + // 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(4); + row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT + : javafx.geometry.Pos.CENTER_LEFT); + + row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6)); + + 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 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); + 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 (hasVal(rSender)) 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 String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "…" : s; } + + public boolean isSameChat(UUID chatId, String type) { + return currentChat != null + && currentChat.getId().equals(chatId) + && currentChat.getType().equalsIgnoreCase(type); + } + + public void onRealTimeNewMessage(JSONObject m) { + try { + String chatIdStr = str(m,"receiver_id"); + String chatType = str(m,"receiver_type"); + if (chatIdStr.isEmpty() || chatType.isEmpty()) return; + + UUID chatId = UUID.fromString(chatIdStr); + if (!isSameChat(chatId, chatType)) { + System.out.println("[UI] RT msg for another chat: " + chatId); + return; + } + + // id → message_id fallback + if (!m.has("message_id") && m.has("id")) { + m.put("message_id", m.getString("id")); + } + + String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") + : (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown"); + + String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT"; + String content = str(m,"content"); + String whenIso = str(m,"send_at"); + String msgId = str(m,"message_id"); + + LocalDateTime ts = parseWhen(whenIso); + if (ts == null) ts = LocalDateTime.now(); + + addBubble(false, senderName, type, content, ts, msgId, + str(m,"forwarded_from"), str(m,"forwarded_by"), str(m,"reply_to_id"), + bool(m,"is_edited"), arr(m,"reactions")); + + if (hasVal(msgId)) msgIndex.put(msgId, m); + + if (currentChat != null) markAsRead(currentChat); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private void fetchAndRenderHeader(ChatEntry entry) { + JSONObject req = new JSONObject(); + req.put("action", "get_header_info"); + req.put("receiver_id", entry.getId().toString()); + req.put("receiver_type", entry.getType()); // باید "private" باشه + + // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) + UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری + if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { + req.put("viewer_id", viewerId.toString()); + } + + new Thread(() -> { + JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { ex.printStackTrace(); return; } + + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + System.err.println("get_header_info failed: " + (resp != null ? resp.optString("message") : "null resp")); + return; + } + JSONObject data = resp.optJSONObject("data"); + if (data == null) return; + + Platform.runLater(() -> renderHeaderFromData(entry, data)); + }).start(); + } + + private void renderHeaderFromData(ChatEntry entry, JSONObject data) { + String t = entry.getType() == null ? "" : entry.getType().toLowerCase(); + switch (t) { + case "private" -> updatePrivateHeader(entry, data); + case "group" -> updateGroupHeader(entry, data); + case "channel" -> updateChannelHeader(entry, data); + default -> chatStatus.setText(""); + } + } + + private void updatePrivateHeader(ChatEntry entry, JSONObject data) { + String name = nz(data.optString("profile_name", entry.getName())); + chatTitle.setText(name); + + // other_user_id برای ریل‌تایم status + String other = data.optString("other_user_id", ""); + if (!other.isBlank()) { + try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} + } + + // تصویر + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + Image im = AvatarLocalResolver.load(img); // ⬅️ + if (im != null) userAvatar.setImage(im); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + + chatStatus.setText(userStatusText( + data.optBoolean("online", false), + data.optString("last_seen", null) + )); + + } + + private void updateGroupHeader(ChatEntry entry, JSONObject data) { + chatTitle.setText(nz(data.optString("group_name", entry.getName()))); + + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + Image im = AvatarLocalResolver.load(img); // ⬅️ + if (im != null) userAvatar.setImage(im); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + + + int members = data.optInt("member_count", 0); + int online = data.optInt("online_count", -1); + chatStatus.setText(online >= 0 ? (members + " members, " + online + " online") + : (members + " members")); + } + + private void updateChannelHeader(ChatEntry entry, JSONObject data) { + chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); + + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + Image im = AvatarLocalResolver.load(img); // ⬅️ + if (im != null) userAvatar.setImage(im); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + + + int subs = data.optInt("member_count", 0); + chatStatus.setText(subs + " subscribers"); + } + + public void onUserStatusChanged(String userUuid, String status, String lastSeenIso) { + if (currentChat == null || !"private".equalsIgnoreCase(currentChat.getType())) return; + var other = currentChat.getOtherUserId(); + if (other == null || !other.toString().equalsIgnoreCase(userUuid)) return; + + if ("online".equalsIgnoreCase(status)) { + chatStatus.setText("online"); + } else { + if (hasVal(lastSeenIso)) { + var ts = parseWhen(lastSeenIso); + chatStatus.setText(ts != null ? ("last seen " + formatWhen(ts)) : "last seen recently"); + } else { + chatStatus.setText("last seen recently"); + } + } + } + + + + private String userStatusText(boolean online, String lastSeenIso) { + if (online) return "online"; + + LocalDateTime ts = parseWhen(lastSeenIso); + if (ts == null) return "Last seen recently"; + + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + return FMT_HHMM.format(ts); + } + + long days = ChronoUnit.DAYS.between(d, today); + if (days > 30) { + return "Last seen long time ago"; + } + return "recently"; + } + + private void setDefaultHeaderAvatarByType(String type){ + String path = switch (type == null ? "" : type.toLowerCase()) { + case "channel" -> "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; + case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; + default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + }; + userAvatar.setImage(new Image( + java.util.Objects.requireNonNull(getClass().getResourceAsStream(path)) + )); + } } 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..c3a5124 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -9,13 +9,18 @@ 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; import java.io.IOException; +import static org.to.telegramfinalproject.UI.AppRouter.showRegister; + public class LoginController { @FXML private TextField usernameField; @@ -23,7 +28,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,63 +53,131 @@ 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."); +// } +// } + + + @FXML private void handleLogin() { - String username = usernameField.getText(); - String password = passwordField.getText(); - + String u = usernameField.getText().trim(); + String p = passwordField.getText(); // 1. Check for empty fields - if (username.isEmpty() || password.isEmpty()) { + if (u.isEmpty() || p.isEmpty()) { showError("Please fill in all required fields."); return; } // 2. Check if username exists userDatabase userDb = new userDatabase(); - if (!userDb.existsByUsername(username)) { + if (!userDb.existsByUsername(u)) { showError("This username doesn’t exist."); return; } // 3. Check if password is correct - User user = userDb.findByUsername(username); - if (!PasswordHashing.verify(password, user.getPassword())) { + User user = userDb.findByUsername(u); + if (!PasswordHashing.verify(p, 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); + setUiBusy(true); - 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")); + new Thread(() -> { + try { + var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + var handler = cli.getHandler(); - // Simulate successful login (since main.fxml isn’t ready) - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!"); - alert.show(); + + handler.login(u,p); + + 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"); + showRegister(); } private void switchScene(String fxmlFile) { @@ -127,6 +200,7 @@ public class LoginController { PauseTransition pause = new PauseTransition(Duration.millis(50)); pause.setOnFinished(event -> { errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); // 🔴 force red errorLabel.setVisible(true); }); pause.play(); 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/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index f544cd9..1d3b577 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -9,25 +9,43 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.scene.shape.Circle; import javafx.util.Duration; - +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; +import java.time.LocalDateTime; +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 VBox noResultsBox; + @FXML private ImageView noResultIcon; + @FXML private ScrollPane globalSearchScroll; + @FXML private VBox globalSearchResultsContainer; + 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; @@ -41,6 +59,14 @@ public class MainController { // === Sidebar === @FXML private Pane overlay; @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; @@ -57,10 +83,82 @@ public class MainController { public static MainController getInstance() { return instance; } + private final Map itemControllers = new HashMap<>(); + + + //For realtime handling + public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs, + boolean isIncoming, String lastPreview) { + + boolean exists = + (Session.chatList != null && Session.chatList.stream().anyMatch(c -> chatId.equals(c.getId()))) || + (Session.activeChats != null && Session.activeChats.stream().anyMatch(c -> chatId.equals(c.getId()))) || + (Session.archivedChats != null && Session.archivedChats.stream().anyMatch(c -> chatId.equals(c.getId()))); + if (!exists) return; + + boolean isOpen = Session.currentChatId != null && + Session.currentChatId.equals(chatId.toString()); + + forEachChat(chatId, chat -> { + chat.setLastMessageTime(lastTs); + if (lastPreview != null) chat.setLastMessagePreview(lastPreview); + if (isIncoming && !isOpen) chat.setUnreadCount(chat.getUnreadCount() + 1); // ✅ یکدست با unreadCount + }); + + java.util.Comparator byTimeDesc = (c1, c2) -> { + var t1 = c1.getLastMessageTime(); + var t2 = c2.getLastMessageTime(); + if (t1 == null && t2 == null) return 0; + if (t1 == null) return 1; + if (t2 == null) return -1; + return t2.compareTo(t1); + }; + if (Session.chatList != null) Session.chatList.sort(byTimeDesc); + if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc); + if (Session.archivedChats != null) Session.archivedChats.sort(byTimeDesc); + + refreshChatListUI(); + refreshBadgesIfAny(); + } + + + + + // MainController + private void forEachChat(UUID chatId, java.util.function.Consumer fn) { + java.util.List> lists = java.util.List.of( + Session.chatList != null ? Session.chatList : java.util.List.of(), + Session.activeChats != null ? Session.activeChats : java.util.List.of(), + Session.archivedChats != null ? Session.archivedChats : java.util.List.of() + ); + for (var lst : lists) { + for (var c : lst) { + if (chatId.equals(c.getId())) { + fn.accept(c); + } + } + } + } + + + private void refreshChatListUI() { + Platform.runLater(() -> { + chatListContainer.getChildren().clear(); + itemControllers.clear(); + populateChatListFromSession(); // همون متدی که نودها رو می‌سازه و می‌چسبونه + }); + } + + + private void refreshBadgesIfAny() { + // آپدیت نشان‌ها اگر لازم است + } + @FXML public void initialize() { - addSampleChats(); + // Populate chat list + populateChatListFromSession(); // Register the scene for automatic CSS updates Platform.runLater(() -> { @@ -70,17 +168,15 @@ public class MainController { // Listen for theme changes to update icons & labels manually ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> { if (newVal) { - // Dark mode ON updateIconsForDarkMode(); updateLabelsForDarkMode(); } else { - // Light mode ON updateIconsForLightMode(); updateLabelsForLightMode(); } }); - // Smooth scroll feel + // Smooth scroll feel for chat list scrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); scrollPane.setPannable(true); scrollPane.setFitToWidth(true); @@ -99,23 +195,47 @@ 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 + // Search bar enter action + searchBar.setOnAction(e -> { + String keyword = searchBar.getText().trim(); + if (keyword.isEmpty()) return; - 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 - ); + if (currentSearchMode == SearchMode.GLOBAL) { + performGlobalSearch(keyword); + } else if (currentSearchMode == SearchMode.CHAT && currentChatId != null) { + performChatSearch(keyword, currentChatId); } }); + + // Typing in search bar shows global search panel + searchBar.textProperty().addListener((obs, o, n) -> { + if (n != null && !n.isBlank()) { + if (currentSearchMode == SearchMode.GLOBAL) { + showGlobalSearchPanel(); + } else if (currentSearchMode == SearchMode.CHAT) { + showSearchPanel(); + } + } + }); + + chatSearchResults.setOnMouseClicked(e -> { + int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); + if (idx >= 0 && idx < searchBacking.size()) { + openSearchResult(searchBacking.get(idx)); + } + }); + + // Smooth scroll feel for global search results + globalSearchScroll.getStylesheets().add( + getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm() + ); + globalSearchScroll.setPannable(true); + globalSearchScroll.setFitToWidth(true); + globalSearchScroll.setFitToHeight(false); + globalSearchScroll.getContent().setOnScroll(event -> { + double deltaY = event.getDeltaY() * 0.003; // match chat list smoothness + globalSearchScroll.setVvalue(globalSearchScroll.getVvalue() - deltaY); + }); } // Called from ChatPageController when user clicks search button @@ -123,6 +243,8 @@ public class MainController { scrollPane.setVisible(false); scrollPane.setManaged(false); + currentSearchMode = SearchMode.CHAT; + chatSearchPane.setVisible(true); chatSearchPane.setManaged(true); @@ -134,59 +256,185 @@ public class MainController { chatSearchPane.setVisible(false); chatSearchPane.setManaged(false); + currentSearchMode = SearchMode.GLOBAL; + currentChatId = null; + scrollPane.setVisible(true); scrollPane.setManaged(true); + + searchBar.clear(); } - 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); + public void showGlobalSearchPanel() { + scrollPane.setVisible(false); + scrollPane.setManaged(false); + + chatSearchPane.setVisible(false); + chatSearchPane.setManaged(false); + + globalSearchPane.setVisible(true); + globalSearchPane.setManaged(true); } - 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, "/org/to/telegramfinalproject/Avatars/default_profile.png"); + @FXML + public void closeGlobalSearch() { + globalSearchPane.setVisible(false); + globalSearchPane.setManaged(false); - chatItem.setOnMouseClicked(e -> openChat(name)); - chatListContainer.getChildren().add(chatItem); - } catch (IOException e) { - e.printStackTrace(); + scrollPane.setVisible(true); + scrollPane.setManaged(true); + + searchBar.clear(); + } + + private String formatChatTime(java.time.LocalDateTime ts) { + if (ts == null) return ""; + var today = java.time.LocalDate.now(); + var d = ts.toLocalDate(); + + if (d.isEqual(today)) { + //today + return FMT_HHMM.format(ts); + //yesterday + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL; + } else { + return FMT_DATE_TIME.format(ts); } } - private void openChat(String chatName) { + 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; + + 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( +// "/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 timeText = formatChatTime(chat.getLastMessageTime()); +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node chatItem = loader.load(); +// ChatItemController controller = loader.getController(); +// +// cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png"); +// item.setOnMouseClicked(e -> openChat(chat)); +// chatListContainer.getChildren().add(item); +// +// itemControllers.put(chat.getId(), cc); +// +// } catch (Exception ex) { +// ex.printStackTrace(); +// } +// } + + + private void addChatNode(ChatEntry chat) { + try { + FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); + Node item = fx.load(); + ChatItemController cc = fx.getController(); + + String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); + + String timeText = chat.getLastMessageTime() == null + ? "" + : formatChatTime(chat.getLastMessageTime()); + + // If chat has a profile picture, pass it; otherwise null + String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty()) + ? chat.getImageUrl() + : null; + + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType()); 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 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); + this.chatPageController = controller; + Session.currentChatId = chat.getId().toString(); - chatDisplayArea.getChildren().clear(); - chatDisplayArea.getChildren().add(chatPage); + chatDisplayArea.getChildren().setAll(chatPage); + chat.setUnreadCount(0); + ChatItemController item = itemControllers.get(chat.getId()); + if (item != null) item.setUnread(0); - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); } } @@ -199,25 +447,56 @@ 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(); } @@ -263,11 +542,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() { @@ -286,4 +567,411 @@ public class MainController { public void closeOverlay(Node overlayNode) { mainRoot.getChildren().remove(overlayNode); } + + + // ===== Search UI state ===== + private final java.util.List searchBacking = new java.util.ArrayList<>(); + + @FXML private AnchorPane chatHost; // کانتینر مناسب در مین برای قرار دادن ChatPage + + private ChatPageController chatPageController; + + + + public ChatPageController getChatPageController() { + return chatPageController; + } + + + private enum SRType { USER, GROUP, CHANNEL, MESSAGE } + + private static class SearchResult { + 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() { + 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; + } + } + + 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()) { + 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; + } + + showGlobalSearchPanel(); + + // Request to server + 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(results); +// globalSearchResults.setItems(view); +// globalSearchResults.setVisible(true); +// globalSearchResults.setManaged(true); +// } + + private void renderSearchResults(List results) { + globalSearchResultsContainer.getChildren().clear(); + + if (results.isEmpty()) { + noResultsBox.setVisible(true); + noResultsBox.setManaged(true); + globalSearchScroll.setVisible(false); + globalSearchScroll.setManaged(false); + return; + } + + noResultsBox.setVisible(false); + noResultsBox.setManaged(false); + globalSearchScroll.setVisible(true); + globalSearchScroll.setManaged(true); + + for (SearchResult r : results) { + HBox container = new HBox(10); + ImageView avatar = new ImageView(); + avatar.setFitWidth(40); + avatar.setFitHeight(40); + avatar.setSmooth(true); + avatar.setPreserveRatio(true); + + // 🔑 Clip it into a circle + Circle clip = new Circle(20, 20, 20); // x,y = center, radius = 20 + avatar.setClip(clip); + + Label title = new Label(r.title); + title.getStyleClass().add("global-search-title"); + + Label subtitle = new Label( + r.type == SRType.MESSAGE + ? (r.subtitle != null ? r.subtitle : "") + : "Press to see messages" + ); + subtitle.getStyleClass().add("global-search-subtitle"); + + VBox texts = new VBox(2, title, subtitle); + container.getChildren().addAll(avatar, texts); + + // Default profile per type + switch (r.type) { + case USER -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png")))); + case GROUP -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_group_profile.png")))); + case CHANNEL -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_channel_profile.png")))); + case MESSAGE -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png")))); + } + + container.setOnMouseClicked(e -> openSearchResult(r)); + globalSearchResultsContainer.getChildren().add(container); + } + } + + private void openSearchResult(SearchResult r) { + switch (r.type) { + case USER: { + + java.util.UUID chatId = findExistingPrivateChatId(r.uuid); + if (chatId == null) { + chatId = fetchOrCreatePrivateChat(r.uuid); + 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); + break; + } + + case GROUP: + case CHANNEL: { + 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)); + 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) { + 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())) { + 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 diff --git a/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java b/src/main/java/org/to/telegramfinalproject/UI/RegisterController.java index a8f29bb..1bd6791 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; @@ -38,18 +40,16 @@ public class RegisterController { @FXML public void initialize() { - // Sync password and confirm fields with their visible counterparts - visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); - visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty()); try { - connection = new ClientConnection("localhost", 8000); - } catch (Exception e) { - System.out.println("Could not connect to server: " + e.getMessage()); + org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + } catch (Exception ignored) { } + visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); + visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty()); } - @FXML + @FXML private void togglePasswordVisibility() { passwordVisible = !passwordVisible; visiblePasswordField.setVisible(passwordVisible); @@ -69,69 +69,152 @@ public class RegisterController { toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁"); } +// @FXML +// private void handleRegister() { +// String userID = userIdField.getText().trim(); +// String username = usernameField.getText().trim(); +// String profileName = profileNameField.getText().trim(); +// String password = passwordField.getText(); +// String confirmPass = confirmPasswordField.getText(); +// +// userDatabase userDb = new userDatabase(); +// String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"; +// +// // 1. Empty fields +// if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) { +// showError("Please fill in all required fields."); +// return; +// } +// +// // 2. Username exists +// if (userDb.existsByUsername(username)) { +// showError("This username is already taken."); +// return; +// } +// +// // 3. User ID exists +// if (userDb.existsByUserId(userID)) { +// showError("This user ID is already taken."); +// return; +// } +// +// // 4. Password mismatch +// if (!password.equals(confirmPass)) { +// showError("Passwords do not match."); +// return; +// } +// +// // 5. Weak password +// if (!password.matches(passwordRegex)) { +// showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); +// return; +// } +// +// // 6. Attempt registration +// try { +// JSONObject request = new JSONObject(); +// request.put("action", "register"); +// request.put("user_id", userID); +// request.put("username", username); +// request.put("password", password); +// request.put("profile_name", profileName); +// connection.send(request.toString()); +// +// // Simulate successful registration (since main.fxml isn’t ready) +// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!"); +// alert.show(); +// +// } catch (Exception ex) { +// showError("Failed to register. Please try again later."); +// } +// } + + + @FXML private void handleRegister() { - String userID = userIdField.getText().trim(); - String username = usernameField.getText().trim(); + String userID = userIdField.getText().trim(); + String username = usernameField.getText().trim(); String profileName = profileNameField.getText().trim(); - String password = passwordField.getText(); + String password = passwordField.getText(); String confirmPass = confirmPasswordField.getText(); userDatabase userDb = new userDatabase(); String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"; - // 1. Empty fields + // اعتبارسنجی‌ها if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) { - showError("Please fill in all required fields."); - return; + showError("Please fill in all required fields."); return; } + if (userDb.existsByUsername(username)) { showError("This username is already taken."); return; } + if (userDb.existsByUserId(userID)) { showError("This user ID is already taken."); return; } + if (!password.equals(confirmPass)) { showError("Passwords do not match."); return; } + if (!password.matches(passwordRegex)) { showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); return; } - // 2. Username exists - if (userDb.existsByUsername(username)) { - showError("This username is already taken."); - return; - } + // UI را موقتاً disable کن (اختیاری) + setBusy(true); - // 3. User ID exists - if (userDb.existsByUserId(userID)) { - showError("This user ID is already taken."); - return; - } + new Thread(() -> { + try { + // 1) مطمئن شو کانکشن/لیسنر روشن است + var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + var handler = cli.getHandler(); - // 4. Password mismatch - if (!password.equals(confirmPass)) { - showError("Passwords do not match."); - return; - } + // 2) Register + var regReq = new org.json.JSONObject() + .put("action", "register") + .put("user_id", userID) + .put("username", username) + .put("password", password) + .put("profile_name", profileName); - // 5. Weak password - if (!password.matches(passwordRegex)) { - showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); - return; - } + var regResp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(regReq); + if (regResp == null || !"success".equalsIgnoreCase(regResp.optString("status"))) { + javafx.application.Platform.runLater(() -> { + setBusy(false); + showError(regResp != null ? regResp.optString("message","Register failed.") : "Register failed."); + }); + return; + } - // 6. Attempt registration - try { - JSONObject request = new JSONObject(); - request.put("action", "register"); - request.put("user_id", userID); - request.put("username", username); - request.put("password", password); - request.put("profile_name", profileName); - connection.send(request.toString()); + // 3) Auto-Login با همان کانکشن + handler.login(username, password); - // Simulate successful registration (since main.fxml isn’t ready) - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!"); - alert.show(); + javafx.application.Platform.runLater(() -> { + setBusy(false); + if (org.to.telegramfinalproject.Client.Session.currentUser == null || !handler.wasSuccess()) { + showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage()); + return; + } + org.to.telegramfinalproject.UI.AppRouter.showMain(); + }); - } catch (Exception ex) { - showError("Failed to register. Please try again later."); - } + } catch (Exception ex) { + javafx.application.Platform.runLater(() -> { + setBusy(false); + showError("Failed to register/login: " + ex.getMessage()); + }); + } + }, "register-thread").start(); } + private void setBusy(boolean b){ + userIdField.setDisable(b); + usernameField.setDisable(b); + profileNameField.setDisable(b); + passwordField.setDisable(b); + visiblePasswordField.setDisable(b); + confirmPasswordField.setDisable(b); + visibleConfirmPasswordField.setDisable(b); + togglePasswordBtn.setDisable(b); + toggleConfirmBtn.setDisable(b); + } + + + @FXML private void switchToLogin() throws IOException { - switchScene("login_view.fxml"); + showLogin(); } private void switchScene(String fxmlFile) { @@ -154,6 +237,7 @@ public class RegisterController { PauseTransition pause = new PauseTransition(Duration.millis(50)); pause.setOnFinished(event -> { errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); // 🔴 force red errorLabel.setVisible(true); }); pause.play(); 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(); - } - } -} - diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 1641fb6..c5c67f0 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -14,6 +14,9 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.AvatarLocalResolver; + import java.io.IOException; import java.net.URL; @@ -22,6 +25,9 @@ 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; @@ -46,8 +52,10 @@ 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); + AvatarFX.circleClip(profileImage, 56); + setupButtonActions(); setupToggleAction(); @@ -210,4 +218,58 @@ 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..."); } + + + + 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/default_user_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; +// } +// } + + + 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); + + // اگر مسیر نسبی سرور (مثل /avatars/...) است + String fileUri = AvatarLocalResolver.resolve(src); + if (fileUri != null) return new Image(fileUri, true); + + // در غیر این صورت، فرض URL کامل + return new Image(src, true); + } catch (Exception ignored) { + return null; + } + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java index 3a1dea3..e13a0be 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); + // First: 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(); } +} 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 0000000..f9343f6 Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Avatars/default_channel_profile.png differ diff --git a/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png b/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png new file mode 100644 index 0000000..997616c Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Avatars/default_group_profile.png differ 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/Avatars/img.png b/src/main/resources/org/to/telegramfinalproject/Avatars/img.png new file mode 100644 index 0000000..281de74 Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Avatars/img.png differ 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 1226f89..22ad344 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; @@ -295,6 +301,86 @@ -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; +} + + /* ===== Overlay ===== */ .overlay-root { -fx-alignment: CENTER; 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 e172216..a739c75 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; @@ -294,6 +299,108 @@ -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; +} + + /* ===== Overlay ===== */ .overlay-root { -fx-alignment: CENTER; diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml index d6db77f..4299c06 100644 --- a/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/main.fxml @@ -44,10 +44,14 @@ - + + + + @@ -69,44 +73,82 @@ - + -