From ff6f771e5c0664ecdebcc3b703e0a4299411e965 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 27 Aug 2025 23:22:07 +0330 Subject: [PATCH] Connect backend to UI --- .../Client/ActionHandler.java | 188 ++++++++------ .../Client/TelegramClient.java | 217 +++++++++++++--- .../Database/MessageDatabase.java | 69 +++++ .../Models/ChatEntry.java | 24 ++ .../telegramfinalproject/Models/JsonUtil.java | 6 + .../telegramfinalproject/Models/Message.java | 5 + .../Server/ClientHandler.java | 132 ++++------ .../to/telegramfinalproject/UI/AppRouter.java | 37 +++ .../UI/ChatItemController.java | 7 + .../UI/ChatPageController.java | 238 ++++++++++++++++-- .../UI/IntroController.java | 43 ++-- .../UI/LoginController.java | 146 +++++++---- .../UI/MainController.java | 180 ++++++++++--- .../UI/SidebarMenuController.java | 9 + .../UI/TelegramApplication.java | 61 ++++- 15 files changed, 1044 insertions(+), 318 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/UI/AppRouter.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index d27b6cb..a4a510a 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -27,6 +27,14 @@ public class ActionHandler { public static volatile boolean forceExitChat = false; public static ActionHandler instance; + //use for UI + private volatile String lastStatus = "error"; // success | error + private volatile String lastMessage = ""; + + public String getLastStatus() { return lastStatus; } + public String getLastMessage() { return lastMessage; } + public boolean wasSuccess() { return "success".equalsIgnoreCase(lastStatus); } + @@ -42,6 +50,17 @@ public class ActionHandler { } + public void login(String username , String password){ + JSONObject request = new JSONObject(); + request.put("action", "login"); + request.put("user_id", JSONObject.NULL); + request.put("username", username); + request.put("password", password); + request.put("profile_name", JSONObject.NULL); + + this.send(request); + + } public void loginHandler() { System.out.println("Login form: \n"); System.out.print("Username: "); @@ -197,7 +216,7 @@ public class ActionHandler { } - private void addContact(UUID contactId) { + public void addContact(UUID contactId) { JSONObject req = new JSONObject(); req.put("action", "add_contact"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -206,7 +225,7 @@ public class ActionHandler { } - private void joinGroupOrChannel(String type, String uuid) { + public void joinGroupOrChannel(String type, String uuid) { JSONObject req = new JSONObject(); req.put("action", "join_" + type); req.put("user_id", Session.getUserUUID()); @@ -216,7 +235,7 @@ public class ActionHandler { - private ChatEntry fetchChatInfo(String receiverId, String receiverType) { + public ChatEntry fetchChatInfo(String receiverId, String receiverType) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", receiverId); @@ -256,7 +275,7 @@ public class ActionHandler { } - private void refreshChatList() { + public void refreshChatList() { JSONObject req = new JSONObject(); req.put("action", "get_chat_list"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -325,21 +344,38 @@ public class ActionHandler { } private ChatEntry parseChatEntry(JSONObject chat) { - ChatEntry entry = new ChatEntry( - UUID.fromString(chat.getString("internal_id")), - chat.getString("id"), - chat.getString("name"), - chat.optString("image_url", ""), - chat.getString("type"), - chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")), - chat.optBoolean("is_owner", false), - chat.optBoolean("is_admin", false) - ); + UUID internalId = null; + try { internalId = UUID.fromString(chat.getString("internal_id")); } catch (Exception ignored) {} - if (chat.has("other_user_id") && !chat.isNull("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); + String id = chat.optString("id", ""); + String name = chat.optString("name", ""); + String imageUrl = chat.optString("image_url", ""); + String type = chat.optString("type", ""); + boolean isOwner = chat.optBoolean("is_owner", false); + boolean isAdmin = chat.optBoolean("is_admin", false); + + LocalDateTime lastTime = null; + String lts = chat.optString("last_message_time", null); + if (lts != null && !"null".equalsIgnoreCase(lts)) { + try { lastTime = LocalDateTime.parse(lts); } catch (Exception ignored) {} + } + + ChatEntry entry = new ChatEntry(internalId, id, name, imageUrl, type, lastTime, isOwner, isAdmin); + + entry.setUnreadCount(chat.optInt("unread_count", 0)); + String preview = chat.optString("last_message_preview", null); + if (preview != null && !"null".equalsIgnoreCase(preview)) { + entry.setLastMessagePreview(preview); + } + + String other = chat.optString("other_user_id", null); + if (other != null && !"null".equalsIgnoreCase(other)) { + try { entry.setOtherUserId(UUID.fromString(other)); } catch (Exception ignored) {} + } + + if (chat.has("is_saved_messages")) { + entry.setSavedMessages(chat.optBoolean("is_saved_messages", false)); } - entry.setSavedMessages(chat.optBoolean("is_saved_messages", false)); return entry; } @@ -433,7 +469,7 @@ public class ActionHandler { - private void send(JSONObject request) { + public void send(JSONObject request) { try { if (!request.has("action") || request.isNull("action")) { System.err.println("❌ Invalid request: missing action."); @@ -471,6 +507,12 @@ public class ActionHandler { return; + status = response.optString("status","error"); + String message = response.optString("message",""); + this.lastStatus = status; + this.lastMessage = message; + + switch (action) { case "login": case "register": @@ -1057,7 +1099,7 @@ public class ActionHandler { } - private void viewProfile(UUID targetId) { + public void viewProfile(UUID targetId) { JSONObject req = new JSONObject(); req.put("action", "view_profile"); req.put("target_id", targetId.toString()); @@ -1085,7 +1127,7 @@ public class ActionHandler { } - private void startPrivateChat(ContactEntry contact) { + public void startPrivateChat(ContactEntry contact) { UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); UUID contactId = contact.getContactId(); @@ -1115,10 +1157,10 @@ public class ActionHandler { ); entry.setOtherUserId(contactId); - Session.chatList.add(0, entry); // اضافه به اول لیست + Session.chatList.add(0, entry); System.out.println("✅ Chat with " + contact.getProfileName() + " started."); - openChat(entry); // 👈 مستقیم وارد چت شو (اختیاری) + openChat(entry); } @@ -1239,7 +1281,7 @@ public class ActionHandler { System.out.println("\nYour Chats:"); System.out.println("0. 📦 Archived Chats"); - // پیدا کردن Saved در لیست + //find save messages chat Integer savedIdxInActive = null; for (int i = 0; i < Session.activeChats.size(); i++) { if (Session.activeChats.get(i).isSavedMessages()) { @@ -1248,23 +1290,19 @@ public class ActionHandler { } } - // مپ شمارهٔ نمایش → ایندکس واقعی در activeChats Map displayToActive = new HashMap<>(); int displayIndex = 1; - // چاپ Saved (در صورت وجود) و ثبت در مپ if (savedIdxInActive != null) { ChatEntry saved = Session.activeChats.get(savedIdxInActive); String last = (saved.getLastMessageTime() == null) ? "No messages yet" : saved.getLastMessageTime().toString(); System.out.println(displayIndex + ". 💾 Saved Messages - Last: " + last); - // نکتهٔ کلیدی: مپ کن تا مثل بقیه با openChat باز شود displayToActive.put(displayIndex, savedIdxInActive); displayIndex++; } - // چاپ بقیهٔ چت‌ها + ثبت مپ for (int i = 0; i < Session.activeChats.size(); i++) { if (savedIdxInActive != null && i == savedIdxInActive) continue; @@ -1276,7 +1314,6 @@ public class ActionHandler { displayIndex++; } - // انتخاب System.out.print("Select a chat by number: "); int choice; try { @@ -1298,11 +1335,11 @@ public class ActionHandler { } ChatEntry selected = Session.activeChats.get(activeIdx); - openChat(selected); // برای Saved هم همین مسیر اجرا می‌شود + openChat(selected); } - private void showArchivedChats() { + public void showArchivedChats() { if (Session.archivedChats == null || Session.archivedChats.isEmpty()) { System.out.println("📭 No archived chats."); return; @@ -1340,7 +1377,7 @@ public class ActionHandler { - private void openChat(ChatEntry chat) { + public void openChat(ChatEntry chat) { //for private chats only if (chat.getType().equalsIgnoreCase("private")) { @@ -1573,7 +1610,7 @@ public class ActionHandler { // } - private boolean showPrivateChatMenu(ChatEntry chat) { + public boolean showPrivateChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; @@ -1677,7 +1714,7 @@ public class ActionHandler { } - private JSONObject getGroupPermissions(UUID groupId) { + public JSONObject getGroupPermissions(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_group_permissions"); req.put("group_id", groupId.toString()); @@ -1691,7 +1728,7 @@ public class ActionHandler { - private boolean showGroupChatMenu(ChatEntry chat) { + public boolean showGroupChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; System.out.println("🚪 Exiting chat due to real-time update."); @@ -1804,7 +1841,7 @@ public class ActionHandler { - private boolean showChannelChatMenu(ChatEntry chat) { + public boolean showChannelChatMenu(ChatEntry chat) { if (forceExitChat) { forceExitChat = false; System.out.println("🚪 Exiting chat due to real-time update."); @@ -1962,7 +1999,7 @@ public class ActionHandler { - private void transferOwnershipAndLeave(UUID groupId) { + public void transferOwnershipAndLeave(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_admins"); req.put("group_id", groupId.toString()); @@ -2042,7 +2079,7 @@ public class ActionHandler { } - private void removeMemberFromGroup(UUID groupId) { + public void removeMemberFromGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2103,7 +2140,7 @@ public class ActionHandler { - private void addAdminToChannel(UUID channelId) { + public void addAdminToChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2180,7 +2217,7 @@ public class ActionHandler { - private void addAdminToGroup(UUID groupId) { + public void addAdminToGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2242,7 +2279,7 @@ public class ActionHandler { } - private void viewGroupMembers(UUID groupId) { + public void viewGroupMembers(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_members"); req.put("group_id", groupId.toString()); @@ -2270,7 +2307,7 @@ public class ActionHandler { - private void removeSubscriberFromChannel(UUID channelId) { + public void removeSubscriberFromChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2330,7 +2367,7 @@ public class ActionHandler { } - private void transferChannelOwnershipAndLeave(UUID channelId) { + public void transferChannelOwnershipAndLeave(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -2413,7 +2450,7 @@ public class ActionHandler { - private void deleteChannel(UUID channelId) { + public void deleteChannel(UUID channelId) { System.out.print("Are you sure you want to delete the channel? (yes/no): "); String confirm = scanner.nextLine().trim().toLowerCase(); @@ -2436,7 +2473,7 @@ public class ActionHandler { } - private void deleteGroup(UUID groupId) { + public void deleteGroup(UUID groupId) { System.out.print("Are you sure you want to delete the group? (yes/no): "); String confirm = scanner.nextLine().trim().toLowerCase(); @@ -2458,7 +2495,7 @@ public class ActionHandler { } } - private void deleteChat(UUID targetId, boolean both) { + public void deleteChat(UUID targetId, boolean both) { JSONObject req = new JSONObject(); req.put("action", "delete_private_chat"); req.put("chat_id", targetId.toString()); @@ -2475,7 +2512,7 @@ public class ActionHandler { - private void toggleBlock(UUID userId) { + public void toggleBlock(UUID userId) { JSONObject req = new JSONObject(); @@ -2497,7 +2534,7 @@ public class ActionHandler { } - private void leaveChat(UUID id, String type) { + public void leaveChat(UUID id, String type) { JSONObject req = new JSONObject(); req.put("action", "leave_chat"); req.put("user_id", Session.getUserUUID()); @@ -2518,7 +2555,7 @@ public class ActionHandler { } - private void removeAdminFromGroup(UUID groupId) { + public void removeAdminFromGroup(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "view_group_admins"); req.put("group_id", groupId.toString()); @@ -2578,7 +2615,7 @@ public class ActionHandler { } - private void removeAdminFromChannel(UUID channelId) { + public void removeAdminFromChannel(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -2642,7 +2679,7 @@ public class ActionHandler { - private void addMemberToGroup(UUID groupId, UUID userId) { + public void addMemberToGroup(UUID groupId, UUID userId) { JSONObject req = new JSONObject(); req.put("action", "add_member_to_group"); req.put("group_id", groupId.toString()); @@ -2654,7 +2691,7 @@ public class ActionHandler { } } - private void addSubscriberToChannel(UUID channelId, UUID targetUserId) { + public void addSubscriberToChannel(UUID channelId, UUID targetUserId) { JSONObject req = new JSONObject(); req.put("action", "add_subscriber_to_channel"); req.put("channel_id", channelId.toString()); @@ -2670,7 +2707,7 @@ public class ActionHandler { } } - private void GroupInfo(UUID groupId){ + public void GroupInfo(UUID groupId){ JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", groupId.toString()); @@ -2699,7 +2736,7 @@ public class ActionHandler { } - private void editGroupInfo(UUID groupId) { + public void editGroupInfo(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", groupId.toString()); @@ -2775,7 +2812,7 @@ public class ActionHandler { - private void viewChannelSubscribers(UUID channelId) { + public void viewChannelSubscribers(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_subscribers"); req.put("channel_id", channelId.toString()); @@ -2797,7 +2834,7 @@ public class ActionHandler { } } - private void ChannelInfo(UUID channelInternalId){ + public void ChannelInfo(UUID channelInternalId){ JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", channelInternalId.toString()); @@ -2826,7 +2863,7 @@ public class ActionHandler { } - private void editChannelInfo(UUID channelInternalId) { + public void editChannelInfo(UUID channelInternalId) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", channelInternalId.toString()); @@ -2901,7 +2938,7 @@ public class ActionHandler { } - private JSONObject getResponse() { + public JSONObject getResponse() { try { return TelegramClient.responseQueue.take(); @@ -2931,7 +2968,7 @@ public class ActionHandler { - private void editAdminPermissions(UUID chatId, String chatType) { + public void editAdminPermissions(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", chatType.equals("group") ? "view_group_admins" : "view_channel_admins"); req.put(chatType + "_id", chatId.toString()); @@ -3028,7 +3065,7 @@ public class ActionHandler { } - private void viewChannelAdmins(UUID channelId) { + public void viewChannelAdmins(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "view_channel_admins"); req.put("channel_id", channelId.toString()); @@ -3053,7 +3090,7 @@ public class ActionHandler { } - private JSONObject getChannelPermissions(UUID channelId) { + public JSONObject getChannelPermissions(UUID channelId) { JSONObject req = new JSONObject(); req.put("action", "get_channel_permissions"); req.put("channel_id", channelId.toString()); @@ -3353,7 +3390,7 @@ public class ActionHandler { - private void startMenuRefresherThread() { + public void startMenuRefresherThread() { Thread refresher = new Thread(() -> { System.out.println("🟡 Refresher tick. refreshCurrentChatMenu = " + Session.refreshCurrentChatMenu); @@ -3382,7 +3419,7 @@ public class ActionHandler { - private void openForeignChat(ChatEntry chat) { + public void openForeignChat(ChatEntry chat) { System.out.println("🔍 Opening " + chat.getType() + " chat (not in your chat list)"); JSONObject req = new JSONObject(); @@ -3446,7 +3483,7 @@ public class ActionHandler { - private void viewUserProfile(UUID userId) { + public void viewUserProfile(UUID userId) { JSONObject req = new JSONObject(); req.put("action", "view_profile"); req.put("target_id", userId.toString()); @@ -3471,7 +3508,7 @@ public class ActionHandler { } } - private void viewGroupOrChannelInfo(UUID id, String type) { + public void viewGroupOrChannelInfo(UUID id, String type) { JSONObject req = new JSONObject(); req.put("action", "get_chat_info"); req.put("receiver_id", id.toString()); @@ -3496,7 +3533,7 @@ public class ActionHandler { public void toggleArchive(UUID chatId, String chatType) { - // پیدا کردن چت از Session.chatList + Optional optional = Session.chatList.stream() .filter(c -> c.getId().equals(chatId)) .findFirst(); @@ -3522,7 +3559,7 @@ public class ActionHandler { } - private void archiveChat(UUID chatId, String chatType) { + public void archiveChat(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", "archive_chat"); req.put("chat_id", chatId.toString()); @@ -3531,7 +3568,7 @@ public class ActionHandler { if (res != null) System.out.println(res.getString("message")); } - private void unarchiveChat(UUID chatId, String chatType) { + public void unarchiveChat(UUID chatId, String chatType) { JSONObject req = new JSONObject(); req.put("action", "unarchive_chat"); req.put("chat_id", chatId.toString()); @@ -3663,7 +3700,6 @@ public class ActionHandler { } } - // 🔹 فقط ارسال پیام با chat_id و receiver_type JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); messageJson.put("receiver_type", receiverType); @@ -3683,7 +3719,7 @@ public class ActionHandler { } } - private void refreshContactList() { + public void refreshContactList() { JSONObject req = new JSONObject(); req.put("action", "get_contact_list"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -3741,7 +3777,7 @@ public class ActionHandler { } } - private void viewMessagesInChat(ChatEntry chat) { + public void viewMessagesInChat(ChatEntry chat) { int offset = 0; int limit = 10; @@ -3910,7 +3946,7 @@ public class ActionHandler { } } - private void editMessage(UUID messageId) { + public void editMessage(UUID messageId) { System.out.print("📝 Enter new content: "); String newContent = scanner.nextLine().trim(); @@ -3934,7 +3970,7 @@ public class ActionHandler { } - private void reactToMessage(UUID messageId) { + public void reactToMessage(UUID messageId) { System.out.print("😊 Enter your reaction (e.g., ❤️, 👍, 😂): "); String reaction = scanner.nextLine().trim(); @@ -3957,7 +3993,7 @@ public class ActionHandler { } - private void forwardMessage(UUID originalMessageId) { + public void forwardMessage(UUID originalMessageId) { System.out.println("\n📤 Select a chat to forward this message to:"); List chatList = Session.chatList; @@ -4001,7 +4037,7 @@ public class ActionHandler { } - private void replyToMessage(UUID messageId) { + public void replyToMessage(UUID messageId) { System.out.print("💬 Enter your reply: "); String content = scanner.nextLine().trim(); @@ -4030,7 +4066,7 @@ public class ActionHandler { } - private void deleteMessage(UUID messageId) { + public void deleteMessage(UUID messageId) { System.out.println("\n🗑️ Delete Message Options:"); System.out.println("1. Delete for yourself (one-sided)"); System.out.println("2. Delete for everyone (global) [only if allowed]"); @@ -4078,7 +4114,7 @@ public class ActionHandler { } } - private void showSidebarMenu() { + public void showSidebarMenu() { System.out.println("\n--- Sidebar Menu ---"); System.out.println("1. View Profile"); System.out.println("2. New Group"); diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 8db405e..5b5efb6 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -1,3 +1,129 @@ +//package org.to.telegramfinalproject.Client; +// +//import org.json.JSONObject; +// +//import java.io.BufferedReader; +//import java.io.IOException; +//import java.io.InputStreamReader; +//import java.io.PrintWriter; +//import java.net.Socket; +//import java.util.Map; +//import java.util.Scanner; +//import java.util.UUID; +//import java.util.concurrent.BlockingQueue; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.LinkedBlockingQueue; +// +//public class TelegramClient { +// private static final String SERVER_HOST = "localhost"; +// private static final int SERVER_PORT = 8000; +// private static Socket socket; +// private BufferedReader in; +// private PrintWriter out; +// private final Scanner scanner; +// private ActionHandler handler; +// public static BlockingQueue responseQueue = new LinkedBlockingQueue<>(); +// public static UUID loggedInUserId = null; +// public static final Map> pendingResponses = new ConcurrentHashMap<>(); +// +// +// private static TelegramClient instance; +// +// public TelegramClient() { +// this.scanner = new Scanner(System.in); +// instance = this; +// } +// +// public static TelegramClient getInstance() { +// return instance; +// } +// +// public void start() { +// try { +// socket = new Socket(SERVER_HOST, SERVER_PORT); +// in = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// out = new PrintWriter(socket.getOutputStream(), true); +// System.out.println("✅ Connected to Telegram Server"); +// handler = new ActionHandler(out, in, scanner); +// +// Thread listenerThread = new Thread(new IncomingMessageListener(in)); +// listenerThread.setDaemon(true); +// listenerThread.start(); +// +// showMainMenu(); +// +// } catch (IOException e) { +// System.err.println("❌ Error connecting to server: " + e.getMessage()); +// } +// } +// +// private void showMainMenu() throws IOException { +// while (true) { +// System.out.println("Main Menu:"); +// System.out.println("1. Register"); +// System.out.println("2. Login"); +// System.out.println("3. Exit"); +// System.out.print("Choose an option: "); +// String choice = scanner.nextLine(); +// +// switch (choice) { +// case "1" -> handler.register(); +// case "2" -> { +// handler.loginHandler(); +// if (Session.currentUser != null) { +// System.out.println("✅ Login successful."); +// new Thread(new ActionHandler.ChatStateMonitor(out)).start(); +//// new Thread(new ActionHandler.CurrentChatMenuRefresher(this.handler)).start(); +// +// +// UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); +// loggedInUserId = internalId; +// +// handler.userMenu(internalId); +// } else { +// System.out.println("❌ Login failed."); +// } +// } +// case "3" -> { +// System.out.println("Exiting..."); +// return; +// } +// default -> System.out.println("Invalid choice."); +// } +// } +// } +// +// public static void send(JSONObject req) { +// try { +// responseQueue.clear(); // optional: clear old responses +// getInstance().out.println(req.toString()); +// System.out.println("📤 [SEND] " + req.toString(2)); +// +// } catch (Exception e) { +// System.err.println("❌ Error sending request: " + e.getMessage()); +// } +// } +// +// +// +// +// public static Socket getSocket() { +// return socket; +// } +// +// public static void main(String[] args) { +// new TelegramClient().start(); +// } +// +// public PrintWriter getOut() { +// return out; +// } +// +// +//} + + + package org.to.telegramfinalproject.Client; import org.json.JSONObject; @@ -7,6 +133,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Scanner; import java.util.UUID; @@ -17,46 +144,79 @@ import java.util.concurrent.LinkedBlockingQueue; public class TelegramClient { private static final String SERVER_HOST = "localhost"; private static final int SERVER_PORT = 8000; + + private static TelegramClient instance; + private static Socket socket; private BufferedReader in; private PrintWriter out; + + private final Scanner scanner; private ActionHandler handler; - public static BlockingQueue responseQueue = new LinkedBlockingQueue<>(); - public static UUID loggedInUserId = null; + + public static final BlockingQueue responseQueue = new LinkedBlockingQueue<>(); public static final Map> pendingResponses = new ConcurrentHashMap<>(); + public static UUID loggedInUserId = null; - - private static TelegramClient instance; + private volatile boolean listenerStarted = false; public TelegramClient() { this.scanner = new Scanner(System.in); instance = this; } - public static TelegramClient getInstance() { + public static synchronized TelegramClient getInstance() { + if (instance == null) instance = new TelegramClient(); return instance; } - public void start() { + public void startConsole() { try { - socket = new Socket(SERVER_HOST, SERVER_PORT); - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out = new PrintWriter(socket.getOutputStream(), true); - System.out.println("✅ Connected to Telegram Server"); - handler = new ActionHandler(out, in, scanner); - - Thread listenerThread = new Thread(new IncomingMessageListener(in)); - listenerThread.setDaemon(true); - listenerThread.start(); - + connectIfNeeded(); + initHandlerIfNeeded(); + startListenerOnce(); showMainMenu(); - } catch (IOException e) { System.err.println("❌ Error connecting to server: " + e.getMessage()); } } + public void start() { startConsole(); } + + //UI only + public static synchronized TelegramClient getOrInitForUI() throws IOException { + TelegramClient cli = getInstance(); + cli.connectIfNeeded(); + cli.initHandlerIfNeeded(); + cli.startListenerOnce(); + return cli; + } + + private synchronized void connectIfNeeded() throws IOException { + if (socket != null && socket.isConnected() && !socket.isClosed()) return; + + socket = new Socket(SERVER_HOST, SERVER_PORT); + in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + out = new PrintWriter(socket.getOutputStream(), true); + System.out.println("✅ Connected to Telegram Server"); + } + + private synchronized void initHandlerIfNeeded() { + if (handler == null) { + handler = new ActionHandler(out, in, scanner); + } + } + + private void startListenerOnce() { + if (listenerStarted) return; + listenerStarted = true; + Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener"); + listenerThread.setDaemon(true); + listenerThread.start(); + } + + //console private void showMainMenu() throws IOException { while (true) { System.out.println("Main Menu:"); @@ -73,8 +233,6 @@ public class TelegramClient { if (Session.currentUser != null) { System.out.println("✅ Login successful."); new Thread(new ActionHandler.ChatStateMonitor(out)).start(); -// new Thread(new ActionHandler.CurrentChatMenuRefresher(this.handler)).start(); - UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); loggedInUserId = internalId; @@ -95,28 +253,21 @@ public class TelegramClient { public static void send(JSONObject req) { try { - responseQueue.clear(); // optional: clear old responses + responseQueue.clear(); getInstance().out.println(req.toString()); System.out.println("📤 [SEND] " + req.toString(2)); - } catch (Exception e) { System.err.println("❌ Error sending request: " + e.getMessage()); } } - - - - public static Socket getSocket() { - return socket; - } + public BufferedReader getIn() { return in; } + public PrintWriter getOut() { return out; } + public ActionHandler getHandler() { return handler; } + public static Socket getSocket() { return socket; } public static void main(String[] args) { - new TelegramClient().start(); + new TelegramClient().startConsole(); } - - public PrintWriter getOut() { - return out; - } - } + diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 1e6efb1..5085ed5 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1057,4 +1057,73 @@ public class MessageDatabase { } } + + public static Message getLastMessage(UUID targetId, String type) { + final String sql = + "SELECT * FROM messages " + + "WHERE receiver_type = ? AND receiver_id = ? " + + "ORDER BY send_at DESC LIMIT 1"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, type.toLowerCase()); // "private" | "group" | "channel" + ps.setObject(2, targetId); + + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? mapRow(rs) : null; + } + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + public static int getUnreadCount(UUID me, UUID targetId, String type) { + final String sql = + "SELECT COUNT(*) FROM messages " + + "WHERE receiver_type = ? AND receiver_id = ? " + + "AND sender_id <> ? " + + "AND (status IS NULL OR status <> 'SEEN')"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, type.toLowerCase()); + ps.setObject(2, targetId); + ps.setObject(3, me); + + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + + + } + + private static Message mapRow(ResultSet rs) throws SQLException { + Message m = new Message(); + m.setMessage_id((UUID) rs.getObject("message_id")); + m.setSender_id((UUID) rs.getObject("sender_id")); + m.setReceiver_id((UUID) rs.getObject("receiver_id")); + m.setReceiver_type(rs.getString("receiver_type")); + if ("private".equalsIgnoreCase(m.getReceiver_type())) { + m.setReceiver_id(m.getReceiver_id()); + } else { + m.setReceiver_id(null); + } + m.setMessage_type(rs.getString("message_type")); + m.setContent(rs.getString("content")); + Timestamp ts = rs.getTimestamp("send_at"); + if (ts != null) m.setSend_at(ts.toLocalDateTime()); + m.setIs_edited(rs.getBoolean("is_edited")); + m.setStatus(rs.getString("status")); + Object rtid = rs.getObject("reply_to_id"); + m.setReply_to_id(rtid == null ? null : (UUID) rtid); + return m; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 8f57ff8..9275b96 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -22,6 +22,17 @@ public class ChatEntry { private JSONObject permissions; + + + +// private UUID otherUserId; + private int unreadCount; + private String lastMessagePreview; + private String lastMessageType; // TEXT / IMAGE / AUDIO / VIDEO / ... + private UUID lastMessageSenderId; + + + public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime) { this.internalId = internalId; this.displayId = displayId; @@ -149,4 +160,17 @@ public class ChatEntry { public void setSavedMessages(boolean savedMessages) { this.savedMessages = savedMessages; } + + + public int getUnreadCount() { return unreadCount; } + public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; } + + public String getLastMessagePreview() { return lastMessagePreview; } + public void setLastMessagePreview(String lastMessagePreview) { this.lastMessagePreview = lastMessagePreview; } + + public String getLastMessageType() { return lastMessageType; } + public void setLastMessageType(String lastMessageType) { this.lastMessageType = lastMessageType; } + + public UUID getLastMessageSenderId() { return lastMessageSenderId; } + public void setLastMessageSenderId(UUID lastMessageSenderId) { this.lastMessageSenderId = lastMessageSenderId; } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index 7bb8791..63e270f 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -186,6 +186,12 @@ public class JsonUtil { obj.put("is_owner", entry.isOwner()); obj.put("is_admin", entry.isAdmin()); obj.put("is_saved_messages", entry.isSavedMessages()); + obj.put("unread_count", entry.getUnreadCount()); + obj.put("last_message_preview", entry.getLastMessagePreview()); + obj.put("last_message_type", entry.getLastMessageType()); + obj.put("last_message_sender_id", + entry.getLastMessageSenderId() != null ? entry.getLastMessageSenderId().toString() : JSONObject.NULL); + jsonArray.put(obj); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index cfbed22..27c9a15 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -125,6 +125,9 @@ public class Message { this.edited_at = editedAt; } + public Message() { + + } // ✅ Getters & Setters @@ -203,4 +206,6 @@ public class Message { return is_deleted_globally; } + public void receiver_id(UUID receiverId) {this.receiver_id = receiverId; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 06072e2..b5bb596 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -125,78 +125,6 @@ public class ClientHandler implements Runnable { } - -// for (Contact contact : contacts) { -// User target = userDatabase.findByInternalUUID(contact.getContact_id()); -// if (target == null) continue; -// LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(user.getInternal_uuid(), target.getInternal_uuid(), "private"); -// -//// chatList.add(new ChatEntry( -//// target.getInternal_uuid(), // internal UUID -//// target.getUser_id(), // public display ID -//// target.getProfile_name(), -//// target.getImage_url(), -//// "private", -//// last, -//// false, -//// false -//// )); -// -// UUID targetId = target.getInternal_uuid(); -// -// ChatEntry entry = new ChatEntry( -// targetId, -// target.getUser_id(), -// target.getProfile_name(), -// target.getImage_url(), -// "private", -// last, -// false, -// false -// ); -// -// -// -// } - - -// -// List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); -// for (PrivateChat chat : privateChats) { -// UUID otherId = chat.getUser1_id().equals(currentUser.getInternal_uuid()) ? -// chat.getUser2_id() : chat.getUser1_id(); -// -// User otherUser = userDatabase.findByInternalUUID(otherId); -// if (otherUser == null) continue; -// -// LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); -// -// -// ChatEntry entry = new ChatEntry( -// chat.getChat_id(), -// otherUser.getUser_id(), -// otherUser.getProfile_name(), -// otherUser.getImage_url(), -// "private", -// lastMessageTime, -// false, -// false -// ); -// entry.setOtherUserId(otherId); -// -// if (currentUser.getInternal_uuid() == otherId) { -// entry.setSavedMessages(true); -// } -// -// if (archivedChatIds.contains(chat.getChat_id())) { -// archivedChatList.add(entry); -// chatList.add(entry); -// } else { -// activeChatList.add(entry); -// chatList.add(entry); -// } -// } - List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); ChatEntry savedEntry = null; @@ -229,10 +157,12 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); if (isSelf) { - entry.setSavedMessages(true); // فلگ مهم - savedEntry = entry; // برای بردن به اول لیست + entry.setSavedMessages(true); + savedEntry = entry; } + enrichChatEntry(entry, user.getInternal_uuid()); + if (!isSelf && archivedChatIds.contains(chat.getChat_id())) { archivedChatList.add(entry); } else { @@ -270,6 +200,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, user.getInternal_uuid()); if (archivedChatIds.contains(group.getInternal_uuid())) { archivedChatList.add(entry); @@ -309,6 +240,8 @@ public class ClientHandler implements Runnable { isAdmin ); + enrichChatEntry(entry, user.getInternal_uuid()); + if (archivedChatIds.contains(channel.getInternal_uuid())) { archivedChatList.add(entry); chatList.add(entry); @@ -774,9 +707,9 @@ public class ClientHandler implements Runnable { ChatEntry entry = new ChatEntry( chat.getChat_id(), - otherUser.getUser_id(), - otherUser.getProfile_name(), - otherUser.getImage_url(), + isSavedMessages ? "Saved Messages" : otherUser.getUser_id(), + isSavedMessages ? "Saved Messages" : otherUser.getProfile_name(), + isSavedMessages ? null : otherUser.getImage_url(), "private", lastMessageTime, false, @@ -784,18 +717,20 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); + // Mark it as saved messages if it's the special self-chat if (isSavedMessages) { entry.setSavedMessages(true); } + enrichChatEntry(entry, currentUser.getInternal_uuid()); - if (archivedChatIds.contains(chat.getChat_id())) { + if (archivedChatIds.contains(chat.getChat_id()) && !isSavedMessages) { archivedChatList.add(entry); - chatList.add(entry); } else { activeChatList.add(entry); - chatList.add(entry); } + chatList.add(entry); + } @@ -826,6 +761,8 @@ public class ClientHandler implements Runnable { isAdmin ); + enrichChatEntry(entry, currentUser.getInternal_uuid()); + if (archivedChatIds.contains(group.getInternal_uuid())) { archivedChatList.add(entry); chatList.add(entry); @@ -863,6 +800,7 @@ public class ClientHandler implements Runnable { isOwner, isAdmin ); + enrichChatEntry(entry, currentUser.getInternal_uuid()); if (archivedChatIds.contains(channel.getInternal_uuid())) { archivedChatList.add(entry); @@ -2855,4 +2793,38 @@ public class ClientHandler implements Runnable { + + private void enrichChatEntry(ChatEntry e, UUID me) { + // آخرین پیام + Message last = MessageDatabase.getLastMessage(e.getId(), e.getType()); + if (last != null) { + e.setLastMessageTime(String.valueOf(last.getSend_at())); + e.setLastMessagePreview(buildPreview(last)); + e.setLastMessageType(last.getMessage_type()); + e.setLastMessageSenderId(last.getSender_id()); + } + + // تعداد نخوانده‌ها + int unread = MessageDatabase.getUnreadCount(me, e.getId(), e.getType()); + e.setUnreadCount(unread); + } + + private String buildPreview(Message m) { + String t = m.getMessage_type(); + if ("TEXT".equalsIgnoreCase(t)) { + String c = m.getContent(); + if (c == null) return ""; + // پیش‌نمایش کوتاه + return c.length() > 120 ? c.substring(0, 120) + "…" : c; + } + switch (t) { + case "IMAGE": return "[Image]"; + case "AUDIO": return "[Audio]"; + case "VIDEO": return "[Video]"; + case "FILE": return "[File]"; + default: return "[Message]"; + } + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java b/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java new file mode 100644 index 0000000..4c8ae53 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/AppRouter.java @@ -0,0 +1,37 @@ +// org.to.telegramfinalproject.UI.AppRouter +package org.to.telegramfinalproject.UI; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public final class AppRouter { + private static Stage stage; + private static Scene scene; + + private AppRouter() {} + + public static void init(Stage st, Scene sc) { + stage = st; + scene = sc; + } + + public static void showIntro() { setRoot("/org/to/telegramfinalproject/Fxml/intro.fxml"); } + public static void showLogin() { setRoot("/org/to/telegramfinalproject/Fxml/login_view.fxml"); } + public static void showRegister(){ setRoot("/org/to/telegramfinalproject/Fxml/register_view.fxml"); } + public static void showMain() { setRoot("/org/to/telegramfinalproject/Fxml/main.fxml"); } + + private static void setRoot(String fxmlPath) { + try { + System.out.println("Router: setRoot -> " + fxmlPath); + FXMLLoader fx = new FXMLLoader(AppRouter.class.getResource(fxmlPath)); + Parent root = fx.load(); + if (scene != null) scene.setRoot(root); + else if (stage != null) stage.setScene(new Scene(root, 1480, 820)); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index e1e4d84..286e59b 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -23,4 +23,11 @@ public class ChatItemController { unreadCount.setVisible(unread > 0); unreadCount.setText(String.valueOf(unread)); } + + public void setUnread(int unread) { + boolean show = unread > 0; + unreadCount.setVisible(show); + unreadCount.setManaged(show); + if (show) unreadCount.setText(String.valueOf(unread)); + } } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 6c0aa7c..f63b5b7 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -10,38 +10,62 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; import javafx.stage.FileChooser; +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Models.ChatEntry; +import org.json.JSONArray; +import org.json.JSONObject; import java.io.File; +import java.util.UUID; public class ChatPageController { // ===== messages area ===== - @FXML private VBox messageContainer; - @FXML private ScrollPane messageScrollPane; + @FXML + private VBox messageContainer; + @FXML + private ScrollPane messageScrollPane; // ===== input area ===== - @FXML private TextArea messageInput; - @FXML private Button sendButton; + @FXML + private TextArea messageInput; + @FXML + private Button sendButton; - @FXML private Button attachmentButton; - @FXML private ImageView attachmentIcon; // inside the attachment button + @FXML + private Button attachmentButton; + @FXML + private ImageView attachmentIcon; // inside the attachment button // ===== header ===== - @FXML private ImageView userAvatar; // 36x36 in the FXML - @FXML private Label chatTitle; // contact/group title - @FXML private Label chatStatus; // last seen / online + @FXML + private ImageView userAvatar; // 36x36 in the FXML + @FXML + private Label chatTitle; // contact/group title + @FXML + private Label chatStatus; // last seen / online - @FXML private Button searchInChatButton; // magnifier button - @FXML private ImageView searchIcon; + @FXML + private Button searchInChatButton; // magnifier button + @FXML + private ImageView searchIcon; - @FXML private Button moreButton; // 3-dots button - @FXML private ImageView moreIcon; - @FXML private ContextMenu moreMenu; - @FXML private MenuItem viewProfileItem; - @FXML private MenuItem deleteChatItem; + @FXML + private Button moreButton; // 3-dots button + @FXML + private ImageView moreIcon; + @FXML + private ContextMenu moreMenu; + @FXML + private MenuItem viewProfileItem; + @FXML + private MenuItem deleteChatItem; // ===== send icon ===== - @FXML private ImageView sendIcon; + @FXML + private ImageView sendIcon; // ===== state ===== private String chatName; @@ -49,9 +73,26 @@ public class ChatPageController { // Where your icons live private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; + private ChatEntry currentChat; + private UUID me; + + private void initCurrentUserId() { + try { + // از سشنی که سمت کلاینت داری: + String meStr = org.to.telegramfinalproject.Client.Session + .currentUser.getString("internal_uuid"); + me = UUID.fromString(meStr); + } catch (Exception ignore) { + me = null; // اگر به هر دلیلی نبود، خروجی‌ها رو ورودی فرض نکن + } + } + @FXML public void initialize() { + + initCurrentUserId(); + // Send button if (sendButton != null) { sendButton.setOnAction(e -> sendMessage()); @@ -162,7 +203,9 @@ public class ChatPageController { MainController.getInstance().showSearchPanel(); } - /** Called by main controller when opening a chat. */ + /** + * Called by main controller when opening a chat. + */ public void setChat(String chatName, String avatarPath) { this.chatName = chatName; @@ -210,14 +253,16 @@ public class ChatPageController { // ----- UI helpers ----- - /** Add a normal message bubble (very simple for now). */ + /** + * Add a normal message bubble (very simple for now). + */ public void addMessage(String sender, String content) { Label msg = new Label(sender + ": " + content); msg.setWrapText(true); boolean dark = themeManager.isDarkMode(); String bubbleColor = dark ? "#20405a" : "#4fa8f0"; - String textColor = dark ? "#e8f1f8" : "#0f141a"; + String textColor = dark ? "#e8f1f8" : "#0f141a"; msg.setStyle( "-fx-background-color: " + bubbleColor + ";" + "-fx-text-fill: " + textColor + ";" + @@ -235,7 +280,9 @@ public class ChatPageController { messageScrollPane.setVvalue(1.0); } - /** Update all header/footer icons according to current theme. */ + /** + * Update all header/footer icons according to current theme. + */ private void syncIconsWithTheme() { boolean dark = themeManager.isDarkMode(); // We use “_light” icons on dark backgrounds, and “_dark” on light backgrounds. @@ -274,4 +321,153 @@ public class ChatPageController { } return new Image(url.toExternalForm()); } + + public void showChat(ChatEntry entry) { + this.currentChat = entry; + + // Header + chatTitle.setText(entry.getName()); + chatStatus.setText(""); // اگر last seen داری اینجا بگذار + if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { + try { + userAvatar.setImage(new Image(entry.getImageUrl())); // یا لود از ریسورس خودت + userAvatar.setClip(new Circle(18, 18, 18)); + } catch (Exception ignored) { + } + } + + messageContainer.getChildren().clear(); + loadMessages(entry); + + // مارک به‌عنوان خوانده + markAsRead(entry); + + // فوکوس روی ورودی + Platform.runLater(() -> messageInput.requestFocus()); + } + + private void loadMessages(ChatEntry entry) { + JSONObject req = new JSONObject(); + req.put("action", "get_messages"); + req.put("receiver_id", String.valueOf(entry.getId())); + req.put("receiver_type", entry.getType()); + req.put("limit", 50); + + new Thread(() -> { + JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + if (resp == null) return; + + // بدون opt* : + String status = ""; + try { + status = resp.getString("status"); + } catch (Exception ignore) { + } + if (!"success".equals(status)) return; + + JSONObject data = null; + try { + data = resp.getJSONObject("data"); + } catch (Exception ignore) { + } + if (data == null) return; + + JSONArray arr = null; + try { + arr = data.getJSONArray("messages"); + } catch (Exception ignore) { + } + if (arr == null) return; + + JSONArray finalArr = arr; + Platform.runLater(() -> renderMessages(finalArr)); + }).start(); + } + + + private void renderMessages(JSONArray arr) { + messageContainer.getChildren().clear(); + for (int i = 0; i < arr.length(); i++) { + JSONObject m = arr.getJSONObject(i); + String senderId = m.optString("sender_id", ""); + String type = m.optString("message_type", "TEXT"); + String content = m.optString("content", ""); + + boolean outgoing = false; + if (me != null && senderId != null && !senderId.isEmpty()) { + try { + outgoing = me.equals(UUID.fromString(senderId)); + } catch (Exception ignore) { + } + } + + String text; + switch (type.toUpperCase()) { + case "TEXT": + text = content; + break; + case "IMAGE": + text = "[Image]"; + break; + case "AUDIO": + text = "[Audio]"; + break; + case "VIDEO": + text = "[Video]"; + break; + case "FILE": + text = "[File]"; + break; + default: + text = "[Message]"; + } + addBubble(outgoing, text); + } + messageScrollPane.layout(); + messageScrollPane.setVvalue(1.0); + } + + + private void markAsRead(ChatEntry entry) { + JSONObject readReq = new JSONObject(); + readReq.put("action", "mark_as_read"); + readReq.put("receiver_id", entry.getId().toString()); // ⛳️ internal_id + readReq.put("receiver_type", entry.getType()); + ActionHandler.sendWithResponse(readReq); + } + + private void addBubble(boolean outgoing, String content) { + Label msg = new Label(content); + msg.setWrapText(true); + + boolean dark = themeManager.isDarkMode(); + String mine = dark ? "#2b7cff" : "#d8ecff"; + String theirs = dark ? "#2c333a" : "#ffffff"; + String bg = outgoing ? mine : theirs; + + msg.setStyle( + "-fx-background-color:" + bg + ";" + + "-fx-padding:8 12;" + + "-fx-background-radius:12;" + + "-fx-max-width: 520;" + ); + msg.setMinHeight(Region.USE_PREF_SIZE); + + javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(msg); + row.setFillHeight(true); + row.setSpacing(6); + row.setAlignment(outgoing + ? javafx.geometry.Pos.CENTER_RIGHT + : javafx.geometry.Pos.CENTER_LEFT); + + messageContainer.getChildren().add(row); + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/IntroController.java b/src/main/java/org/to/telegramfinalproject/UI/IntroController.java index 25a5c93..3d25621 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/IntroController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/IntroController.java @@ -118,25 +118,34 @@ public class IntroController { slider.setDaemon(true); slider.start(); } +// +// @FXML +// private void handleStartMessaging(ActionEvent event) { +// try { +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/login_view.fxml")); +// Scene loginScene = new Scene(loader.load()); +// +// // Get the current stage and its dimensions +// Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); +// double currentWidth = stage.getWidth(); +// double currentHeight = stage.getHeight(); +// +// // Set the new scene and apply the previous size +// stage.setScene(loginScene); +// stage.setWidth(currentWidth); +// stage.setHeight(currentHeight); +// stage.show(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } @FXML private void handleStartMessaging(ActionEvent event) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/login_view.fxml")); - Scene loginScene = new Scene(loader.load()); - - // Get the current stage and its dimensions - Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); - double currentWidth = stage.getWidth(); - double currentHeight = stage.getHeight(); - - // Set the new scene and apply the previous size - stage.setScene(loginScene); - stage.setWidth(currentWidth); - stage.setHeight(currentHeight); - stage.show(); - } catch (IOException e) { - e.printStackTrace(); - } + AppRouter.showLogin(); // همون Scene می‌مونه، فقط Root عوض میشه } + + @FXML private void goRegister() { AppRouter.showRegister(); } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java index 4c35011..c1d12a3 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -9,7 +9,10 @@ import javafx.scene.control.*; import javafx.stage.Stage; import javafx.util.Duration; import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.ClientConnection; +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Client.TelegramClient; import org.to.telegramfinalproject.Database.userDatabase; import org.to.telegramfinalproject.Models.User; import org.to.telegramfinalproject.Security.PasswordHashing; @@ -23,7 +26,7 @@ public class LoginController { @FXML private PasswordField passwordField; @FXML private TextField visiblePasswordField; - @FXML private Button togglePasswordBtn; + @FXML private Button toggleVisibilityBtn; @FXML private Label errorLabel; private ClientConnection connection; @@ -48,60 +51,119 @@ public class LoginController { visiblePasswordField.setManaged(passwordVisible); passwordField.setVisible(!passwordVisible); passwordField.setManaged(!passwordVisible); - togglePasswordBtn.setText(passwordVisible ? "👁" : "👁"); + toggleVisibilityBtn.setText(passwordVisible ? "👁" : "👁"); } +// @FXML +// private void handleLogin() { +// String username = usernameField.getText(); +// String password = passwordField.getText(); +// +// // 1. Check for empty fields +// if (username.isEmpty() || password.isEmpty()) { +// showError("Please fill in all required fields."); +// return; +// } +// +// // 2. Check if username exists +// userDatabase userDb = new userDatabase(); +// if (!userDb.existsByUsername(username)) { +// showError("This username doesn’t exist."); +// return; +// } +// +// // 3. Check if password is correct +// User user = userDb.findByUsername(username); +// if (!PasswordHashing.verify(password, user.getPassword())) { +// showError("Incorrect password."); +// return; +// } +// +// // 4. Attempt to send to server +// try { +// JSONObject request = new JSONObject(); +// request.put("action", "login"); +// request.put("user_id", JSONObject.NULL); +// request.put("username", username); +// request.put("password", password); +// request.put("profile_name", JSONObject.NULL); +// +// if (connection != null) { +// connection.send(request.toString()); +// String responseStr = connection.receive(); +// JSONObject response = new JSONObject(responseStr); +// System.out.println("Status: " + response.getString("status")); +// System.out.println("Message: " + response.getString("message")); +// +// // Simulate successful login (since main.fxml isn’t ready) +// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!"); +// alert.show(); +// } +// +// } catch (Exception ex) { +// showError("Unable to connect to server. Please try again later."); +// } +// } + + + + // LoginController.java (متد اصلی) @FXML private void handleLogin() { - String username = usernameField.getText(); - String password = passwordField.getText(); + String u = usernameField.getText().trim(); + String p = passwordField.getText(); + if (u.isEmpty() || p.isEmpty()) { showError("Please fill in all required fields."); return; } - // 1. Check for empty fields - if (username.isEmpty() || password.isEmpty()) { - showError("Please fill in all required fields."); - return; - } + setUiBusy(true); - // 2. Check if username exists - userDatabase userDb = new userDatabase(); - if (!userDb.existsByUsername(username)) { - showError("This username doesn’t exist."); - return; - } + new Thread(() -> { + try { + var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI(); + var handler = cli.getHandler(); - // 3. Check if password is correct - User user = userDb.findByUsername(username); - if (!PasswordHashing.verify(password, user.getPassword())) { - showError("Incorrect password."); - return; - } + // بساز و بفرست — همون send خودت که Session رو پر می‌کند +// org.json.JSONObject req = new org.json.JSONObject() +// .put("action","login") +// .put("username", u) +// .put("password", p); +// +// handler.send(req); // ⬅️ بلاکینگ؛ پس درستش کردیم که تو Thread هست - // 4. Attempt to send to server - try { - JSONObject request = new JSONObject(); - request.put("action", "login"); - request.put("user_id", JSONObject.NULL); - request.put("username", username); - request.put("password", password); - request.put("profile_name", JSONObject.NULL); - if (connection != null) { - connection.send(request.toString()); - String responseStr = connection.receive(); - JSONObject response = new JSONObject(responseStr); - System.out.println("Status: " + response.getString("status")); - System.out.println("Message: " + response.getString("message")); + handler.login(u,p); - // Simulate successful login (since main.fxml isn’t ready) - Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!"); - alert.show(); + javafx.application.Platform.runLater(() -> { + setUiBusy(false); + + if (!handler.wasSuccess() || Session.currentUser == null) { + showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage()); + return; + } + + // موفق: Session از داخل send پر شده + goMain(); // بدون Alert → مستقیم به main.fxml + }); + + } catch (Exception ex) { + javafx.application.Platform.runLater(() -> { + setUiBusy(false); + showError("Connection error."); + }); } - - } catch (Exception ex) { - showError("Unable to connect to server. Please try again later."); - } + }, "login-thread").start(); } + private void goMain() { + AppRouter.showMain(); + } + + + private void setUiBusy(boolean b) { + usernameField.setDisable(b); + passwordField.setDisable(b); + } + + @FXML private void switchToRegister() throws IOException { switchScene("register_view.fxml"); diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 12bfc5d..18e3422 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -13,8 +13,15 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.to.telegramfinalproject.Models.ChatEntry; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; public class MainController { @@ -57,10 +64,14 @@ public class MainController { public static MainController getInstance() { return instance; } + private final Map itemControllers = new HashMap<>(); + @FXML public void initialize() { - addSampleChats(); +// addSampleChats(); + populateChatListFromSession(); + // Register the scene for automatic CSS updates Platform.runLater(() -> { @@ -138,58 +149,155 @@ public class MainController { scrollPane.setManaged(true); } - private void addSampleChats() { - addChat("Archived Chats", "Your archived chats", "10:45", 0); - addChat("Saved Messages", "Keep messages for later", "Yesterday", 0); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); - addChat("Alice", "Hey, how are you?", "14:12", 3); - addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// private void addSampleChats() { +// addChat("Archived Chats", "Your archived chats", "10:45", 0); +// addChat("Saved Messages", "Keep messages for later", "Yesterday", 0); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// addChat("Alice", "Hey, how are you?", "14:12", 3); +// addChat("Bob", "Let's meet tomorrow", "12:08", 1); +// +// +// } + + + private final DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm"); + private final DateTimeFormatter dayFmt = DateTimeFormatter.ofPattern("dd/MM"); // همون سال + private final DateTimeFormatter fullDateFmt= DateTimeFormatter.ofPattern("yyyy/MM/dd"); // سال متفاوت + + private String formatListTimestamp(LocalDateTime ts) { + if (ts == null) return ""; + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + // امروز → فقط ساعت + return timeFmt.format(ts); + } + // اگر همان سال است → تاریخ کوتاه + ساعت + if (d.getYear() == today.getYear()) { + return dayFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 15/08 13:28 + } + // سال متفاوت → تاریخ کامل + ساعت + return fullDateFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 2024/12/31 21:10 } - private void addChat(String name, String lastMsg, String time, int unread) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); - Node chatItem = loader.load(); - ChatItemController controller = loader.getController(); - controller.setChatData(name, lastMsg, time, unread); - chatItem.setOnMouseClicked(e -> openChat(name)); - chatListContainer.getChildren().add(chatItem); - } catch (IOException e) { - e.printStackTrace(); + + private void populateChatListFromSession() { + chatListContainer.getChildren().clear(); + + var list = (org.to.telegramfinalproject.Client.Session.activeChats != null + && !org.to.telegramfinalproject.Client.Session.activeChats.isEmpty()) + ? org.to.telegramfinalproject.Client.Session.activeChats + : org.to.telegramfinalproject.Client.Session.chatList; + + if (list == null || list.isEmpty()) return; + + for (ChatEntry c : list) { + addChatNode(c); } } - private void openChat(String chatName) { + +// private void addChatNode(ChatEntry chat ) { +// try { +// FXMLLoader fx = new FXMLLoader(getClass().getResource( +// "/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node item = fx.load(); +// ChatItemController cc = fx.getController(); +// +// String lastPreview = ""; +// String time = (chat.getLastMessageTime() != null) +// ? timeFmt.format(chat.getLastMessageTime()) : ""; +// +// cc.setChatData(chat.getName(), lastPreview, time, 0); +// +// item.setOnMouseClicked(e -> openChat(String.valueOf(chat))); +// chatListContainer.getChildren().add(item); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + private void addChatNode(ChatEntry chat) { + try { + FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); + Node item = fx.load(); + ChatItemController cc = fx.getController(); + + String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); + String time = formatListTimestamp(chat.getLastMessageTime()); + + cc.setChatData(chat.getName(), preview, time, chat.getUnreadCount()); + item.setOnMouseClicked(e -> openChat(chat)); + chatListContainer.getChildren().add(item); + + itemControllers.put(chat.getId(), cc); + + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + + private String mapTypeToLabel(String t) { + switch (t.toUpperCase()) { + case "IMAGE": return "[Image]"; + case "AUDIO": return "[Audio]"; + case "VIDEO": return "[Video]"; + case "FILE": return "[File]"; + default: return "[Message]"; + } + } + + +// private void addChat(String name, String lastMsg, String time, int unread) { +// try { +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); +// Node chatItem = loader.load(); +// ChatItemController controller = loader.getController(); +// controller.setChatData(name, lastMsg, time, unread); +// +// chatItem.setOnMouseClicked(e -> openChat(name)); +// chatListContainer.getChildren().add(chatItem); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + + private void openChat(ChatEntry chat) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); Node chatPage = loader.load(); ChatPageController controller = loader.getController(); - controller.setChat("Alice", "/org/to/telegramfinalproject/Avatars/profile_test.png"); + controller.showChat(chat); // ✅ متد جدید در ChatPageController + chatDisplayArea.getChildren().setAll(chatPage); - chatDisplayArea.getChildren().clear(); - chatDisplayArea.getChildren().add(chatPage); + // اختیاری: صفر کردن badge و مارک‌کردن به‌عنوان خوانده در UI + ChatItemController item = itemControllers.get(chat.getId()); + if (item != null) item.setUnread(0); - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); } } + @FXML private void toggleSidebar() { if (isSidebarOpen) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 2d08572..6cfb82a 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.UI; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.Image; @@ -12,12 +13,16 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Duration; + import java.net.URL; public class SidebarMenuController { private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/"; + + + @FXML private VBox sidebarRoot; @FXML private ImageView profileImage; @FXML private Label usernameLabel; @@ -176,4 +181,8 @@ public class SidebarMenuController { private void openSettings() { System.out.println("Opening Settings..."); } private void openTelegramFeatures() { System.out.println("Opening Telegram Features..."); } private void openTelegramQnA() { System.out.println("Opening Telegram Q&A..."); } + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java index 3a1dea3..6374ef5 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java +++ b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java @@ -1,4 +1,36 @@ -package org.to.telegramfinalproject.UI; +package org.to.telegramfinalproject.UI;//package org.to.telegramfinalproject.UI; +// +// +//import javafx.application.Application; +//import javafx.fxml.FXMLLoader; +//import javafx.scene.Scene; +//import javafx.scene.image.Image; +//import javafx.stage.Stage; +// +//import java.io.IOException; +// +//public class TelegramApplication extends Application { +// @Override +// public void start(Stage stage) throws IOException { +// FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/main.fxml")); +// Scene scene = new Scene(fxmlLoader.load(), 1480, 820); +// +// scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm()); +// +// stage.setTitle("Telegram"); +// stage.setScene(scene); +// +// // Add icon to the stage +// Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png")); +// stage.getIcons().add(icon); +// +// stage.show(); +// } +// public static void main(String[] args) { +// launch(); +// } +// +//} import javafx.application.Application; @@ -6,28 +38,31 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import org.to.telegramfinalproject.UI.AppRouter; import java.io.IOException; public class TelegramApplication extends Application { @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/main.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 1480, 820); + // اول intro.fxml + FXMLLoader fx = new FXMLLoader( + TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/intro.fxml")); + Scene scene = new Scene(fx.load(), 1480, 820); - scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm()); + scene.getStylesheets().add( + getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm() + ); stage.setTitle("Telegram"); + stage.getIcons().add(new Image( + TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png") + )); stage.setScene(scene); - - // Add icon to the stage - Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png")); - stage.getIcons().add(icon); - stage.show(); - } - public static void main(String[] args) { - launch(); + + AppRouter.init(stage, scene); } -} \ No newline at end of file + public static void main(String[] args) { launch(); } +}