diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index ed59769..0c94d1c 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -2,7 +2,9 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchResultModel; @@ -25,6 +27,8 @@ public class ActionHandler { public static ActionHandler instance; + + private void handleRealTime(JSONObject json) throws IOException { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); @@ -440,8 +444,12 @@ public class ActionHandler { Session.currentUser = response.getJSONObject("data"); JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); + JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); + JSONArray Active = Session.currentUser.getJSONArray("active_chat_list"); + JSONArray contactList = Session.currentUser.getJSONArray("contact_list"); List chatList = new ArrayList<>(); + for (Object obj : chatListJson) { JSONObject chat = (JSONObject) obj; @@ -454,15 +462,80 @@ public class ActionHandler { chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")), chat.optBoolean("is_owner", false), chat.optBoolean("is_admin", false) + + ); - + if (chat.has("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); + } chatList.add(entry); } + List archivedChats = new ArrayList<>(); + for (Object obj : Archived){ + JSONObject chat = (JSONObject) obj; + 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) + ); + if (chat.has("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + } + archivedChats.add(entry); + + } + + List activeChats = new ArrayList<>(); + for (Object obj : Active){ + JSONObject chat = (JSONObject) obj; + + 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) + ); + if (chat.has("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + } + + activeChats.add(entry); + + } + + Session.contactEntries.clear(); + for (Object obj : contactList) { + JSONObject c = (JSONObject) obj; + + ContactEntry entry = new ContactEntry( + UUID.fromString(c.getString("contact_id")), + c.getString("user_id"), + c.getString("profile_name"), + c.optString("image_url", ""), + c.optBoolean("is_blocked", false) + ); + Session.contactEntries.add(entry); + } + + + + + Session.activeChats = activeChats; + Session.archivedChats = archivedChats; Session.chatList = chatList; break; @@ -661,12 +734,14 @@ public class ActionHandler { System.out.println("\n🔓 Messages fetched:"); System.out.println("─────────────────────────────────────────────"); for (int i = 0; i < messages.length(); i++) { + JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); + String senderName = m.optString("sender_name", "Other"); String content = m.getString("content"); String time = m.getString("send_at"); - String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : "Other"; + String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; System.out.println("[" + time + "] " + label + ": " + content); } System.out.println("─────────────────────────────────────────────"); @@ -703,8 +778,9 @@ public class ActionHandler { System.out.println("2. Search"); System.out.println("3. Create Channel"); System.out.println("4. Create group"); - System.out.println("5. Show sidebar menu"); - System.out.println("6. Logout"); + System.out.println("5. View contacts"); + System.out.println("6. Show sidebar menu"); + System.out.println("7. Logout"); System.out.print("Choose an option: "); String choice = scanner.nextLine(); @@ -717,8 +793,9 @@ public class ActionHandler { case "2" -> search(); case "3" -> createChannel(); case "4" -> createGroup(); - case "5" -> showSidebarMenu(); - case "6" -> { + case "5" -> showContactList(); + case "6" -> showSidebarMenu(); + case "7" -> { logout(); return; } @@ -727,39 +804,262 @@ public class ActionHandler { } } - public void showChatListAndSelect() { - - - List chatList = Session.getChatList(); - if (chatList.isEmpty()) { - System.out.println("📭 You have no chats."); + public void showContactList() { + List contacts = Session.contactEntries; + if (contacts.isEmpty()) { + System.out.println("📭 You have no contacts."); return; } - System.out.println("\nYour Chats:"); - for (int i = 0; i < chatList.size(); i++) { - ChatEntry entry = chatList.get(i); - String last = entry.getLastMessageTime() == null ? "No messages yet" : entry.getLastMessageTime().toString(); - System.out.printf("%d. [%s] %s - Last: %s\n", i + 1, entry.getType(), entry.getName(), last); + System.out.println("👥 Your Contacts:"); + for (int i = 0; i < contacts.size(); i++) { + System.out.println((i + 1) + ". " + contacts.get(i)); } - System.out.print("Select a chat by number: "); - int choice = Integer.parseInt(scanner.nextLine()); - if (choice < 1 || choice > chatList.size()) { + System.out.print("Select a contact (0 to go back): "); + int choice = scanner.nextInt(); + scanner.nextLine(); + + if (choice == 0) return; + if (choice < 1 || choice > contacts.size()) { System.out.println("❌ Invalid choice."); return; } - openChat(chatList.get(choice - 1)); + ContactEntry selected = contacts.get(choice - 1); + System.out.println("\n📇 What do you want to do with " + selected.getProfileName() + "?"); + System.out.println("1. View Profile"); + System.out.println("2. Send Message"); + System.out.print("Enter your choice: "); + int action = scanner.nextInt(); + scanner.nextLine(); + + switch (action) { + case 1 -> viewProfile(selected.getContactId()); + case 2 -> startPrivateChat(selected); + default -> System.out.println("❌ Invalid option."); + } + } + + private void viewProfile(UUID targetId) { + JSONObject req = new JSONObject(); + req.put("action", "view_profile"); + req.put("target_id", targetId.toString()); + + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch profile."); + return; + } + + JSONObject data = res.getJSONObject("data"); + String profileName = data.getString("profile_name"); + String userId = data.getString("user_id"); + String bio = data.optString("bio", "(no bio)"); + String imageUrl = data.optString("image_url", "(no image)"); + boolean isOnline = data.getBoolean("is_online"); + String lastSeen = data.getString("last_seen"); + + System.out.println("\n📄 Profile Info:"); + System.out.println("Name: " + profileName); + System.out.println("User ID: " + userId); + System.out.println("Bio: " + bio); + System.out.println("Image: " + imageUrl); + System.out.println("Status: " + (isOnline ? "🟢 Online" : "🔘 Last seen at " + lastSeen)); + } + + + private void startPrivateChat(ContactEntry contact) { + UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); + UUID contactId = contact.getContactId(); + + JSONObject req = new JSONObject(); + req.put("action", "get_or_create_private_chat"); + req.put("user1", myId.toString()); + req.put("user2", contactId.toString()); + + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("❌ Failed to create or fetch private chat."); + return; + } + + JSONObject data = res.getJSONObject("data"); + UUID chatId = UUID.fromString(data.getString("chat_id")); + + ChatEntry entry = new ChatEntry( + chatId, + contact.getUserId(), + contact.getProfileName(), + contact.getImageUrl(), + "private", + null, + false, + false + ); + entry.setOtherUserId(contactId); + + Session.chatList.add(0, entry); // اضافه به اول لیست + System.out.println("✅ Chat with " + contact.getProfileName() + " started."); + + openChat(entry); // 👈 مستقیم وارد چت شو (اختیاری) } +// public void showChatListAndSelect() { +// +// +// List chatList = Session.getChatList(); +// if (chatList.isEmpty()) { +// System.out.println("📭 You have no chats."); +// return; +// } +// +// System.out.println("\nYour Chats:"); +// for (int i = 0; i < chatList.size(); i++) { +// ChatEntry entry = chatList.get(i); +// String last = entry.getLastMessageTime() == null ? "No messages yet" : entry.getLastMessageTime().toString(); +// System.out.printf("%d. [%s] %s - Last: %s\n", i + 1, entry.getType(), entry.getName(), last); +// } +// +// System.out.print("Select a chat by number: "); +// int choice = Integer.parseInt(scanner.nextLine()); +// if (choice < 1 || choice > chatList.size()) { +// System.out.println("❌ Invalid choice."); +// return; +// } +// +// openChat(chatList.get(choice - 1)); +// } + + + public void showChatListAndSelect() { + if (Session.activeChats == null || Session.activeChats.isEmpty()) { + System.out.println("No active chats."); + return; + } + + System.out.println("\nYour Chats:"); + System.out.println("0. 📦 Archived Chats"); + + for (int i = 0; i < Session.activeChats.size(); i++) { + ChatEntry entry = Session.activeChats.get(i); + String time = (entry.getLastMessageTime() == null) + ? "No messages yet" + : entry.getLastMessageTime().toString(); + System.out.println((i + 1) + ". [" + entry.getType() + "] " + + entry.getName() + " - Last: " + time); + } + + System.out.print("Select a chat by number: "); + int choice = Integer.parseInt(scanner.nextLine()); + + if (choice == 0) { + showArchivedChats(); + return; + } + + int index = choice - 1; + + if (index < 0 || index >= Session.activeChats.size()) { + System.out.println("Invalid selection."); + return; + } + + ChatEntry selected = Session.activeChats.get(index); + openChat(selected); + } + + + + private void showArchivedChats() { + if (Session.archivedChats == null || Session.archivedChats.isEmpty()) { + System.out.println("📭 No archived chats."); + return; + } + + System.out.println("\n📦 Archived Chats:"); + for (int i = 0; i < Session.archivedChats.size(); i++) { + ChatEntry entry = Session.archivedChats.get(i); + String time = (entry.getLastMessageTime() == null) + ? "No messages yet" + : entry.getLastMessageTime().toString(); + System.out.println((i + 1) + ". [" + entry.getType() + "] " + + entry.getName() + " - Last: " + time); + } + + System.out.print("Select a chat by number (or 0 to return): "); + int choice = Integer.parseInt(scanner.nextLine()); + + if (choice == 0) { + System.out.println("🔙 Returning to main chat list..."); + return; + } + + int index = choice - 1; + + if (index < 0 || index >= Session.archivedChats.size()) { + System.out.println("❌ Invalid selection."); + return; + } + + ChatEntry selected = Session.archivedChats.get(index); + openChat(selected); + } + + + + private void openChat(ChatEntry chat) { + + //for private chats only + if (chat.getType().equalsIgnoreCase("private")) { + JSONObject reqTarget = new JSONObject(); + reqTarget.put("action", "get_private_chat_target"); + reqTarget.put("chat_id", chat.getId()); + + JSONObject resTarget = sendWithResponse(reqTarget); + if (resTarget == null || !resTarget.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch target user for private chat."); + return; + } + String otherUserId = resTarget.getJSONObject("data").getString("target_id"); + chat.setOtherUserId(UUID.fromString(otherUserId)); + } + + + JSONObject req = new JSONObject(); req.put("action", "get_messages"); - req.put("receiver_id", chat.getId()); + req.put("receiver_id",chat.getId()); req.put("receiver_type", chat.getType()); +// String type = chat.getType(); +// UUID chatId = chat.getId(); +// if (type.equals("private")) { +// UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); +// UUID otherUserId = chat.getId(); +// +// JSONObject getChatIdReq = new JSONObject(); +// getChatIdReq.put("action", "get_or_create_private_chat"); +// getChatIdReq.put("user1", myId.toString()); +// getChatIdReq.put("user2", otherUserId.toString()); +// +// JSONObject chatIdRes = sendWithResponse(getChatIdReq); +// if (chatIdRes == null || !chatIdRes.getString("status").equals("success")) { +// System.out.println("❌ Failed to fetch private chat ID."); +// return; +// } +// +// Session.currentPrivateChatUserId = otherUserId; +// chatId = UUID.fromString(chatIdRes.getJSONObject("data").getString("chat_id")); +// chat.setId(String.valueOf(chatId)); +// +// } + + + + JSONObject res = sendWithResponse(req); if (res == null || !res.getString("status").equals("success")) { @@ -773,14 +1073,24 @@ public class ActionHandler { for (int i = 0; i < messages.length(); i++) { JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); + String senderName = m.optString("sender_name", "Other"); String content = m.getString("content"); String time = m.getString("send_at"); - String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : "Other"; + String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; System.out.println("[" + time + "] " + label + ": " + content); } System.out.println("─────────────────────────────────────────────"); + if (chat.getType().equals("private") && chat.getOtherUserId() == null) { + for (ContactEntry contact : Session.contactEntries) { + if (contact.getContactId().equals(chat.getId())) { + chat.setOtherUserId(contact.getContactId()); + break; + } + } + } + boolean stayInChat = true; @@ -832,9 +1142,24 @@ public class ActionHandler { return false; } + + JSONObject reqTarget = new JSONObject(); + reqTarget.put("action", "get_private_chat_target"); + reqTarget.put("chat_id", chat.getId()); + + JSONObject resTarget = sendWithResponse(reqTarget); + if (resTarget == null || !resTarget.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch target user for private chat."); + return false; + } + + String otherUserId = resTarget.getJSONObject("data").getString("target_id"); + chat.setOtherUserId(UUID.fromString(otherUserId)); + + JSONObject req = new JSONObject(); req.put("action", "view_profile"); - req.put("target_id", chat.getId()); // internal UUID + req.put("target_id", chat.getOtherUserId()); // internal UUID JSONObject res = sendWithResponse(req); @@ -858,25 +1183,26 @@ public class ActionHandler { System.out.println("3. Delete chat (one-sided)"); System.out.println("4. Delete chat (both sides)"); System.out.println("5. View profile"); - System.out.println("6. Back"); + System.out.println("6. Archive/Unarchived"); + System.out.println("7. Back"); String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessageTo(chat.getId(), "private"); - case "2" -> toggleBlock(chat.getId()); + case "1" -> sendMessage(chat.getId(), "private"); + case "2" -> toggleBlock(chat.getOtherUserId()); case "3" -> { deleteChat(chat.getId(), false); - return false; + return true; } case "4" -> { deleteChat(chat.getId(), true); - return false; + return true; } case "5" -> { JSONObject reqProfile = new JSONObject(); reqProfile.put("action", "view_profile"); - reqProfile.put("target_id", chat.getId()); + reqProfile.put("target_id", chat.getOtherUserId()); JSONObject resProfile = sendWithResponse(reqProfile); @@ -899,7 +1225,13 @@ public class ActionHandler { return true; } - case "6" -> { + case "6" ->{ + toggleArchive(chat.getId() , "private"); + + return true; + } + + case "7" -> { return false; } default -> System.out.println("Invalid choice."); @@ -907,6 +1239,7 @@ public class ActionHandler { return true; } + private JSONObject getGroupPermissions(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_group_permissions"); @@ -963,11 +1296,13 @@ public class ActionHandler { System.out.println("11. Edit Admin Permissions"); } + System.out.println("12. Archive/Unarchived"); + System.out.println("0. Back to Chat List"); String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessageTo(chat.getId(), "group"); + case "1" -> sendMessage(chat.getId(), "group"); case "2" -> viewGroupMembers(chat.getId()); case "3" -> { if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false))) @@ -1012,6 +1347,10 @@ public class ActionHandler { } } + case "12"->{ + toggleArchive(chat.getId() , "group"); + } + case "0" -> { return false; } @@ -1078,13 +1417,14 @@ public class ActionHandler { System.out.println("11. Edit Admin Permissions"); } + System.out.println("12. Archive/Unarchived"); System.out.println("0. Back to Chat List"); String input = scanner.nextLine(); switch (input) { case "1" -> { if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) { - sendMessageTo(chat.getId(), "channel"); + sendMessage(chat.getId(), "channel"); } else { System.out.println("❌ You don't have permission to post."); } @@ -1157,6 +1497,10 @@ public class ActionHandler { } } + case "12"->{ + toggleArchive(chat.getId() , "channel"); + } + case "0" -> { return false; @@ -1672,7 +2016,7 @@ public class ActionHandler { private void deleteChat(UUID targetId, boolean both) { JSONObject req = new JSONObject(); req.put("action", "delete_private_chat"); - req.put("target_id", targetId.toString()); + req.put("chat_id", targetId.toString()); req.put("both", both); send(req); @@ -1687,6 +2031,8 @@ public class ActionHandler { private void toggleBlock(UUID userId) { + + JSONObject req = new JSONObject(); req.put("action", "toggle_block"); req.put("user_id", Session.getUserUUID()); @@ -1849,31 +2195,7 @@ public class ActionHandler { - private void sendMessageTo(UUID id, String type) { - System.out.print("Enter message: "); - String text = scanner.nextLine().trim(); - if (text.isEmpty()) { - System.out.println("Message cannot be empty."); - return; - } - - JSONObject req = new JSONObject(); - req.put("action", "send_message"); - req.put("sender_id", Session.getUserUUID()); - req.put("receiver_id", id.toString()); - req.put("receiver_type", type); - req.put("text", text); - - JSONObject res = sendWithResponse(req); - if (res == null) return; - - if (res.getBoolean("success")) { - System.out.println("✅ Message sent."); - } else { - System.out.println("❌ Failed to send message."); - } - } private void addMemberToGroup(UUID groupId, UUID userId) { JSONObject req = new JSONObject(); @@ -2328,6 +2650,9 @@ public class ActionHandler { System.err.println("❌ Invalid request: missing action."); return null; } + System.out.println("📤 Sending request to server: " + request.toString(2)); + System.out.println("📤 [sendWithResponse] Action: " + request.optString("action", "unknown") + ", Full: " + request.toString(2)); + String requestId = UUID.randomUUID().toString(); request.put("request_id", requestId); @@ -2391,13 +2716,22 @@ public class ActionHandler { return; } + System.out.println("\n💬 Your Chats:"); + System.out.println("0. 📦 Archived chats"); + int index = 1; - for (ChatEntry chat : Session.chatList) { - System.out.printf("%d. [%s] %s (%s)\n", index++, chat.getType(), chat.getName(), chat.getDisplayId()); + for (ChatEntry chat : Session.activeChats) { + String time = (chat.getLastMessageTime() == null) + ? "No messages yet" + : chat.getLastMessageTime().toString(); + + System.out.printf("%d. [%s] %s (%s) - Last: %s\n", + index++, chat.getType(), chat.getName(), chat.getDisplayId(), time); } } + public static class ChatStateMonitor implements Runnable { private final PrintWriter out; @@ -2714,6 +3048,173 @@ public class ActionHandler { } } + + + public void toggleArchive(UUID chatId, String chatType) { + // پیدا کردن چت از Session.chatList + Optional optional = Session.chatList.stream() + .filter(c -> c.getId().equals(chatId)) + .findFirst(); + + if (optional.isEmpty()) { + System.out.println("❌ Chat not found."); + return; + } + + ChatEntry chat = optional.get(); + + if (chat.isArchived()) { + unarchiveChat(chatId, chatType); + chat.setArchived(false); + System.out.println("✅ Chat unarchived."); + } else { + archiveChat(chatId, chatType); + chat.setArchived(true); + System.out.println("✅ Chat archived."); + } + + Session.refreshChatLists(); + } + + + private void archiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "archive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + private void unarchiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "unarchive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + +// public void sendMessage(UUID receiverId, String receiverType) { +// Scanner scanner = new Scanner(System.in); +// +// System.out.print("Enter your message: "); +// String content = scanner.nextLine(); +// +// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); +// String messageType = scanner.nextLine(); +// Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); +// while (!allowedTypes.contains(messageType.toUpperCase())) { +// System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); +// messageType = scanner.nextLine(); +// } +// messageType = messageType.toUpperCase(); +// +// JSONArray attachmentsArray = new JSONArray(); +// +// System.out.print("Do you want to attach files? (yes/no): "); +// if (scanner.nextLine().equalsIgnoreCase("yes")) { +// while (true) { +// System.out.print("File URL: "); +// String fileUrl = scanner.nextLine(); +// +// System.out.print("File Type (IMAGE / VIDEO / FILE): "); +// String fileType = scanner.nextLine(); +// +// JSONObject fileJson = new JSONObject(); +// fileJson.put("file_url", fileUrl); +// fileJson.put("file_type", fileType); +// attachmentsArray.put(fileJson); +// +// System.out.print("Add another file? (yes/no): "); +// if (!scanner.nextLine().equalsIgnoreCase("yes")) { +// break; +// } +// } +// } +// +// +// +// JSONObject messageJson = new JSONObject(); +// messageJson.put("action", "send_message"); +// messageJson.put("receiver_type", receiverType); +// messageJson.put("content", content); +// messageJson.put("message_type", messageType); +// if (receiverType.equals("private")) { +// messageJson.put("receiver_user_id", receiverId.toString()); +// } else { +// messageJson.put("receiver_id", receiverId.toString()); +// } +// +// if (!attachmentsArray.isEmpty()) { +// messageJson.put("attachments", attachmentsArray); +// } +// +// JSONObject response = sendWithResponse(messageJson); +// if (response != null && response.getString("status").equals("success")) { +// System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); +// } else { +// System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); +// } +// +// } + + + + public void sendMessage(UUID chatId, String receiverType) { + Scanner scanner = new Scanner(System.in); + + System.out.print("Enter your message: "); + String content = scanner.nextLine(); + + System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); + String messageType = scanner.nextLine().toUpperCase(); + Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedTypes.contains(messageType)) { + System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE): "); + messageType = scanner.nextLine().toUpperCase(); + } + + JSONArray attachmentsArray = new JSONArray(); + System.out.print("Do you want to attach files? (yes/no): "); + if (scanner.nextLine().equalsIgnoreCase("yes")) { + while (true) { + System.out.print("File URL: "); + String fileUrl = scanner.nextLine(); + System.out.print("File Type (IMAGE / VIDEO / FILE): "); + String fileType = scanner.nextLine().toUpperCase(); + + JSONObject fileJson = new JSONObject(); + fileJson.put("file_url", fileUrl); + fileJson.put("file_type", fileType); + attachmentsArray.put(fileJson); + + System.out.print("Add another file? (yes/no): "); + if (!scanner.nextLine().equalsIgnoreCase("yes")) break; + } + } + + // 🔹 فقط ارسال پیام با chat_id و receiver_type + JSONObject messageJson = new JSONObject(); + messageJson.put("action", "send_message"); + messageJson.put("receiver_type", receiverType); + messageJson.put("receiver_id", chatId.toString()); + messageJson.put("content", content); + messageJson.put("message_type", messageType); + + if (!attachmentsArray.isEmpty()) { + messageJson.put("attachments", attachmentsArray); + } + + JSONObject response = sendWithResponse(messageJson); + if (response != null && response.getString("status").equals("success")) { + System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id")); + } else { + System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response")); + } + } + private void showSidebarMenu() { System.out.println("\n--- Sidebar Menu ---"); System.out.println("1. View Profile"); @@ -2760,4 +3261,7 @@ public class ActionHandler { default -> System.out.println("Invalid action."); } } -} \ No newline at end of file + +} + + diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index e10e03e..5168ba9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -107,16 +107,22 @@ public class IncomingMessageListener implements Runnable { } case "chat_updated" -> { - System.out.println("\n🔄 Group/Channel info updated."); - new Thread(() -> { - try { - handleAdminRoleChanged(msg); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); + System.out.println("\n🔄 Chat info updated."); + + 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" -> { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { @@ -139,6 +145,68 @@ public class IncomingMessageListener implements Runnable { System.out.print(">> "); } + private void updateLastMessageTime(JSONObject msg) { + try { + UUID chatUUID = UUID.fromString(msg.getString("chat_id")); + String newTime = msg.optString("last_message_time", null); + + Session.chatList.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.activeChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.archivedChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.chatList.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.activeChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.archivedChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + + + + + if (Session.inChatListMenu) { + ActionHandler.displayChatList(); + System.out.print("Select a chat by number: "); + } + + } catch (Exception e) { + System.out.println("❌ Failed to update last message time: " + e.getMessage()); + } + } + + private void handleAdminRoleChanged(JSONObject data) throws IOException { String chatType = data.getString("chat_type"); String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null))); @@ -230,11 +298,26 @@ public class IncomingMessageListener implements Runnable { private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { - System.out.println("\n🔔 New Message:"); - System.out.println("From: " + msg.getString("sender")); - System.out.println("Time: " + msg.getString("time")); - System.out.println("Content: " + msg.getString("content")); + System.out.println("\n🔔 New Message Received:"); + String senderName = msg.optString("sender_name", "Unknown"); + String content = msg.optString("content", "(empty)"); + String sendAt = msg.optString("send_at", "-"); + + String receiverId = msg.optString("receiver_id", ""); + String receiverType = msg.optString("receiver_type", ""); + + boolean isInCurrentChat = Session.inChatMenu && + Session.currentChatId != null && + Session.currentChatId.equals(receiverId); + + if (isInCurrentChat) { + System.out.println(senderName + ": " + content + " (" + sendAt + ")"); + } else { + System.out.println("💬 Message from " + senderName + " in " + receiverType + " chat: " + content); + Session.forceRefreshChatList = true; + } } + case "message_edited" -> { System.out.println("\n✏️ Message Edited:"); System.out.println("ID: " + msg.getString("message_id")); diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index f849e41..6e5f658 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -3,7 +3,9 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.ContactEntry; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -13,6 +15,9 @@ import java.util.UUID; public class Session { public static JSONObject currentUser; public static List chatList = new ArrayList<>(); + public static List archivedChats = new ArrayList<>(); + public static List activeChats = new ArrayList<>(); + public static UUID currentPrivateChatUserId = null; public static volatile boolean forceRefreshChatList = false; public static volatile boolean backToChatList = false; public static boolean inChatListMenu = false; @@ -21,7 +26,7 @@ public class Session { public static volatile boolean refreshCurrentChatMenu = false; public static String currentChatId = null; public static ChatEntry currentChatEntry = null; - + public static List contactEntries = new ArrayList<>(); @@ -33,26 +38,77 @@ public class Session { throw new RuntimeException("❌ No UUID found in currentUser!"); } +// public static void updateChatList(JSONArray chatArray) { +// chatList.clear(); +// for (int i = 0; i < chatArray.length(); i++) { +// JSONObject obj = chatArray.getJSONObject(i); +// ChatEntry entry = new ChatEntry( +// UUID.fromString(obj.getString("internal_id")), +// obj.optString("id", ""), // displayId +// obj.optString("name", ""), // name +// obj.optString("image_url", ""), +// obj.getString("type"), +// null, // last message time (if needed, parse it) +// obj.optBoolean("is_owner", false), +// obj.optBoolean("is_admin", false) +// ); +// entry.setPermissions(obj.optJSONObject("permissions")); +// chatList.add(entry); +// } +// } + + public static void updateChatList(JSONArray chatArray) { chatList.clear(); for (int i = 0; i < chatArray.length(); i++) { JSONObject obj = chatArray.getJSONObject(i); + + LocalDateTime lastMessageTime = null; + if (obj.has("last_message_time") && !obj.isNull("last_message_time")) { + String timeStr = obj.getString("last_message_time"); + if (!timeStr.isBlank()) { + lastMessageTime = LocalDateTime.parse(timeStr); + } + } + ChatEntry entry = new ChatEntry( UUID.fromString(obj.getString("internal_id")), obj.optString("id", ""), // displayId obj.optString("name", ""), // name obj.optString("image_url", ""), obj.getString("type"), - null, // last message time (if needed, parse it) + lastMessageTime, obj.optBoolean("is_owner", false), obj.optBoolean("is_admin", false) ); - entry.setPermissions(obj.optJSONObject("permissions")); // اگر permissions وجود داره + + entry.setPermissions(obj.optJSONObject("permissions")); chatList.add(entry); } + + chatList.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); + }); + + activeChats = chatList.stream().filter(c -> !c.isArchived()).toList(); + archivedChats = chatList.stream().filter(ChatEntry::isArchived).toList(); } + public static List getChatList() { return chatList; } + + public static void refreshChatLists() { + Session.activeChats = Session.chatList.stream() + .filter(c -> !c.isArchived()) + .toList(); + + Session.archivedChats = Session.chatList.stream() + .filter(ChatEntry::isArchived) + .toList(); + } } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java new file mode 100644 index 0000000..7ad71ba --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java @@ -0,0 +1,72 @@ +package org.to.telegramfinalproject.Database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ArchivedChatDatabase { + public static boolean archiveChat(UUID userId, UUID chatId, String chatType) { + String sql = "INSERT INTO archived_chats (user_id, chat_id, chat_type) VALUES (?, ?, ?) ON CONFLICT DO NOTHING"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + ps.setString(3, chatType); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + public static boolean unarchiveChat(UUID userId, UUID chatId) { + String sql = "DELETE FROM archived_chats WHERE user_id = ? AND chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean isChatArchived(UUID userId, UUID chatId) { + String sql = "SELECT 1 FROM archived_chats WHERE user_id = ? AND chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + ResultSet rs = ps.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static List getArchivedChats(UUID userId) { + List list = new ArrayList<>(); + String sql = "SELECT chat_id FROM archived_chats WHERE user_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + list.add(UUID.fromString(rs.getString("chat_id"))); + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 22ef33b..fcdaf7c 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1,5 +1,6 @@ package org.to.telegramfinalproject.Database; +import org.to.telegramfinalproject.Models.FileAttachment; import org.to.telegramfinalproject.Models.Message; import java.sql.*; @@ -12,41 +13,106 @@ import java.util.stream.Collectors; public class MessageDatabase { - public static void save(Message message) { - String sql = """ - INSERT INTO messages ( - message_id, sender_id, receiver_type, receiver_id, content, - message_type, file_url, send_at, status, - reply_to_id, is_edited, original_message_id, forwarded_by, forwarded_from - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """; + + + + public static boolean insertMessage(UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType) { + String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " + + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = ConnectionDb.connect(); - PreparedStatement stmt = conn.prepareStatement(sql)) { + PreparedStatement ps = conn.prepareStatement(sql)) { - stmt.setObject(1, message.getMessage_id()); - stmt.setObject(2, message.getSender_id()); - stmt.setString(3, message.getReceiver_type()); - stmt.setObject(4, message.getReceiver_id()); - stmt.setString(5, message.getContent()); - stmt.setString(6, message.getMessage_type()); - stmt.setString(7, message.getFile_url()); - stmt.setObject(8, message.getSend_at()); - stmt.setString(9, message.getStatus()); - stmt.setObject(10, message.getReply_to_id()); - stmt.setBoolean(11, message.isIs_edited()); - stmt.setObject(12, message.getOriginal_message_id()); - stmt.setObject(13, message.getForwarded_by()); - stmt.setObject(14, message.getForwarded_from()); + ps.setObject(1, messageId); + ps.setObject(2, senderId); + ps.setString(3, receiverType); + ps.setObject(4, receiverId); + ps.setString(5, content); + ps.setString(6, messageType); - stmt.executeUpdate(); + return ps.executeUpdate() > 0; } catch (SQLException e) { - System.err.println("❌ Error saving message: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + + + public static boolean insertAttachments(UUID messageId, List attachments) { + String sql = "INSERT INTO message_attachments (attachment_id, message_id, file_url, file_type) " + + "VALUES (?, ?, ?, ?)"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + for (FileAttachment att : attachments) { + ps.setObject(1, UUID.randomUUID()); + ps.setObject(2, messageId); + ps.setString(3, att.getFileUrl()); + ps.setString(4, att.getFileType()); + ps.addBatch(); + } + + ps.executeBatch(); + return true; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static void markGloballyDeleted(UUID chatId) { + String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.executeUpdate(); + } catch (SQLException e) { e.printStackTrace(); } } + public static void logDeletedMessagesFor(UUID chatId, UUID userId) { + List messages = MessageDatabase.privateChatHistory(chatId); + for (Message message : messages) { + if (!isMessageDeleted(message.getMessage_id(), userId)) { + String sql = """ + INSERT INTO deleted_messages (message_id, user_id) + VALUES (?, ?) + ON CONFLICT (message_id, user_id) DO NOTHING + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, message.getMessage_id()); + ps.setObject(2, userId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean isMessageDeleted(UUID messageId, UUID userId) { + String sql = "SELECT 1 FROM deleted_messages WHERE message_id = ? AND user_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + ResultSet rs = ps.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + + public void markMessageAsRead(UUID messageId, UUID userId) { String sql = "INSERT INTO message_receipts (message_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; try (Connection conn = ConnectionDb.connect(); @@ -100,7 +166,6 @@ public class MessageDatabase { UUID.fromString(rs.getString("receiver_id")), rs.getString("content"), rs.getString("message_type"), - rs.getString("file_url"), rs.getTimestamp("send_at").toLocalDateTime(), rs.getString("status"), rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, @@ -118,6 +183,31 @@ public class MessageDatabase { return messages; } + public static List getAttachments(UUID messageId) { + List attachments = new ArrayList<>(); + String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, messageId); + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + attachments.add(new FileAttachment( + rs.getString("file_url"), + rs.getString("file_type") + )); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return attachments; + } + + public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { String sql = """ @@ -177,7 +267,6 @@ public class MessageDatabase { UUID.fromString(rs.getString("receiver_id")), rs.getString("content"), rs.getString("message_type"), - rs.getString("file_url"), rs.getTimestamp("send_at").toLocalDateTime(), rs.getString("status"), (UUID) rs.getObject("reply_to_id"), @@ -217,25 +306,18 @@ public class MessageDatabase { return result; } - public static List privateChatHistory(UUID user1, UUID user2) { + public static List privateChatHistory(UUID chatId) { List result = new ArrayList<>(); String sql = """ SELECT * FROM messages WHERE receiver_type = 'private' - AND ( - (sender_id = ? AND receiver_id = ?) - OR (sender_id = ? AND receiver_id = ?) - ) + AND receiver_id = ? ORDER BY send_at """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setObject(1, user1); - stmt.setObject(2, user2); - stmt.setObject(3, user2); - stmt.setObject(4, user1); - + stmt.setObject(1, chatId); ResultSet rs = stmt.executeQuery(); while (rs.next()) { result.add(extractMessage(rs)); @@ -248,6 +330,7 @@ public class MessageDatabase { + public static List groupChatHistory(UUID groupId) { List result = new ArrayList<>(); String sql = """ @@ -360,4 +443,42 @@ public class MessageDatabase { } + public List getMessagesForPrivateChat(UUID user1, UUID user2) { + UUID chatId = PrivateChatDatabase.findChatIdByUsers(user1, user2); + if (chatId == null) return new ArrayList<>(); + return findByReceiver("private", chatId); + } + + public static List findByReceiver(String receiverType, UUID receiverId) { + List messages = new ArrayList<>(); + String sql = "SELECT * FROM messages WHERE receiver_type = ? AND receiver_id = ? ORDER BY send_at"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, receiverType); + ps.setObject(2, receiverId); + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + UUID messageId = (UUID) rs.getObject("message_id"); + UUID senderId = (UUID) rs.getObject("sender_id"); + UUID recId = (UUID) rs.getObject("receiver_id"); + String type = rs.getString("receiver_type"); + String content = rs.getString("content"); + String messageType = rs.getString("message_type"); + LocalDateTime sendAt = rs.getTimestamp("send_at").toLocalDateTime(); + + Message message = new Message(messageId, senderId, recId, type, content, messageType, sendAt); + messages.add(message); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return messages; + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index 2114f4a..4165c2f 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -1,5 +1,12 @@ package org.to.telegramfinalproject.Database; +import org.to.telegramfinalproject.Models.PrivateChat; + +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -8,6 +15,24 @@ import java.util.UUID; public class PrivateChatDatabase { + + public static List getMembers(UUID privateChatId) { + String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, privateChatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + return new ArrayList<>(Arrays.asList(user1, user2)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + public static UUID getOrCreateSavedMessagesChat(UUID userId) { String query = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; try (Connection conn = ConnectionDb.connect()) { @@ -35,4 +60,230 @@ public class PrivateChatDatabase { return null; } } -} \ No newline at end of file + + + public static UUID findChatIdByUsers(UUID user1, UUID user2) { + UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2; + UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1; + + String sql = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, u1); + ps.setObject(2, u2); + + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("chat_id"); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static UUID getOrCreateChat(UUID user1, UUID user2) { + UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2; + UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1; + + String select = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(select)) { + ps.setObject(1, u1); + ps.setObject(2, u2); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return UUID.fromString(rs.getString("chat_id")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + UUID newChatId = UUID.randomUUID(); + String insert = "INSERT INTO private_chat(chat_id, user1_id, user2_id) VALUES (?, ?, ?)"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(insert)) { + ps.setObject(1, newChatId); + ps.setObject(2, u1); + ps.setObject(3, u2); + ps.executeUpdate(); + return newChatId; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + + public static List findChatsOfUser(UUID userId) { + List chats = new ArrayList<>(); + String sql = "SELECT * FROM private_chat WHERE user1_id = ? OR user2_id = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, userId); + ps.setObject(2, userId); + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + UUID chatId = (UUID) rs.getObject("chat_id"); + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + + chats.add(new PrivateChat(chatId, user1, user2)); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return chats; + } + + + public static UUID getOtherUserInChat(UUID chatId, UUID currentUserId) { + String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + return currentUserId.equals(user1) ? user2 : user1; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + private static PrivateChat extractPrivateChat(ResultSet rs) throws SQLException { + UUID chatId = UUID.fromString(rs.getString("chat_id")); + UUID user1 = rs.getObject("user1_id", UUID.class); + UUID user2 = rs.getObject("user2_id", UUID.class); + boolean user1Deleted = rs.getBoolean("user1_deleted"); + boolean user2Deleted = rs.getBoolean("user2_deleted"); + LocalDateTime createdAt = rs.getTimestamp("created_at").toLocalDateTime(); + + return new PrivateChat(chatId, user1, user2, user1Deleted, user2Deleted, createdAt); + } + + public static void markChatDeleted(UUID userId, UUID chatId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN true ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN true ELSE user2_deleted END + WHERE chat_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, userId); + stmt.setObject(3, chatId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void unmarkChatDeleted(UUID userId, UUID chatId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN false ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN false ELSE user2_deleted END + WHERE chat_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, userId); + stmt.setObject(3, chatId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void markUser1Deleted(UUID chatId) { + updateBoolField(chatId, "user1_deleted", true); + } + + public static void markUser2Deleted(UUID chatId) { + updateBoolField(chatId, "user2_deleted", true); + } + + public static void markBothDeleted(UUID chatId) { + String sql = "UPDATE private_chat SET user1_deleted = true, user2_deleted = true WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private static void updateBoolField(UUID chatId, String field, boolean value) { + String sql = "UPDATE private_chat SET " + field + " = ? WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setBoolean(1, value); + ps.setObject(2, chatId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public static PrivateChat findById(UUID chatId) { + String sql = "SELECT * FROM private_chat WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1_id = (UUID) rs.getObject("user1_id"); + UUID user2_id = (UUID) rs.getObject("user2_id"); + boolean user1_deleted = rs.getBoolean("user1_deleted"); + boolean user2_deleted = rs.getBoolean("user2_deleted"); + LocalDateTime created_at = rs.getTimestamp("created_at").toLocalDateTime(); + + return new PrivateChat(chatId, user1_id, user2_id, user1_deleted, user2_deleted, created_at); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static void clearDeletedFlag(UUID senderId, UUID chatId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN FALSE ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN FALSE ELSE user2_deleted END + WHERE chat_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, senderId); + ps.setObject(2, senderId); + ps.setObject(3, chatId); + int rows = ps.executeUpdate(); + System.out.println("✅ clearDeletedFlag updated rows = " + rows); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + + + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index dc9d7dc..0d41a1b 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -313,6 +313,8 @@ public class userDatabase { return null; } + + public List searchUsers(String keyword, UUID currentUserId) { String query = """ SELECT * FROM users diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 7491297..8176576 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -12,8 +12,10 @@ public class ChatEntry { private String imageUrl; private String type; private LocalDateTime lastMessageTime; + private boolean archived = false; + private UUID otherUser; + - // 🔹 نقش‌ها private boolean isOwner = false; private boolean isAdmin = false; private JSONObject permissions; @@ -28,7 +30,6 @@ public class ChatEntry { this.lastMessageTime = lastMessageTime; } - // ✅ کانستراکتور اضافه‌شده برای پشتیبانی از نقش‌ها (اختیاری، برای استفاده‌های جدید) public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime, boolean isOwner, boolean isAdmin) { this(internalId, displayId, name, imageUrl, type, lastMessageTime); this.isOwner = isOwner; @@ -41,7 +42,6 @@ public class ChatEntry { } - // 🟩 گتر و ستر جدید public boolean isOwner() { return isOwner; } @@ -108,4 +108,36 @@ public class ChatEntry { + public boolean isArchived() { + return archived; + } + + public void setArchived(boolean archived) { + this.archived = archived; + } + +// public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); +// } + + + public void setLastMessageTime(String newTime) { + if (newTime == null || newTime.isBlank()) { + this.lastMessageTime = null; + return; + } + + try { + this.lastMessageTime = LocalDateTime.parse(newTime); + } catch (Exception e) { + System.out.println("❌ Failed to parse lastMessageTime: " + newTime); + this.lastMessageTime = null; + } + } + + public void setOtherUserId(UUID otherId) {this.otherUser = otherId; + } + + public UUID getOtherUserId(){ + return otherUser; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java new file mode 100644 index 0000000..c0e4341 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java @@ -0,0 +1,44 @@ +package org.to.telegramfinalproject.Models; + +import java.util.UUID; + +public class ContactEntry { + private UUID contactId; // internal UUID + private String userId; // public ID + private String profileName; + private String imageUrl; + private boolean isBlocked; + + public ContactEntry(UUID contactId, String userId, String profileName, String imageUrl, boolean isBlocked) { + this.contactId = contactId; + this.userId = userId; + this.profileName = profileName; + this.imageUrl = imageUrl; + this.isBlocked = isBlocked; + } + + public UUID getContactId() { + return contactId; + } + + public String getUserId() { + return userId; + } + + public String getProfileName() { + return profileName; + } + + public String getImageUrl() { + return imageUrl; + } + + public boolean isBlocked() { + return isBlocked; + } + + @Override + public String toString() { + return profileName + " (" + userId + ")" + (isBlocked ? " [Blocked]" : ""); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java new file mode 100644 index 0000000..430e457 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -0,0 +1,19 @@ +package org.to.telegramfinalproject.Models; + +public class FileAttachment { + private String fileUrl; + private String fileType; + + public FileAttachment(String fileUrl, String fileType) { + this.fileUrl = fileUrl; + this.fileType = fileType; + } + + public String getFileUrl() { + return fileUrl; + } + + public String getFileType() { + return fileType; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index a9ed199..1b97b58 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -54,7 +54,6 @@ public class JsonUtil { obj.put("receiver_id", message.getReceiver_id().toString()); obj.put("content", message.getContent()); obj.put("message_type", message.getMessage_type()); - obj.put("file_url", message.getFile_url()); obj.put("send_at", message.getSend_at().toString()); obj.put("status", message.getStatus()); obj.put("reply_to_id", message.getReply_to_id() != null ? message.getReply_to_id().toString() : JSONObject.NULL); diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index 00e5764..5160d82 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.Models; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; public class Message { @@ -10,7 +11,6 @@ public class Message { private UUID receiver_id; private String content; private String message_type; // TEXT, IMAGE, FILE, ... - private String file_url; private LocalDateTime send_at; private String status; // SENT, DELIVERED, READ private UUID reply_to_id; @@ -18,9 +18,10 @@ public class Message { private UUID original_message_id; private UUID forwarded_by; private UUID forwarded_from; + private List attachments; public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content, - String message_type, String file_url, LocalDateTime send_at, String status, + String message_type, LocalDateTime send_at, String status, UUID reply_to_id, boolean is_edited, UUID original_message_id, UUID forwarded_by, UUID forwarded_from) { this.message_id = message_id; @@ -29,7 +30,6 @@ public class Message { this.receiver_id = receiver_id; this.content = content; this.message_type = message_type; - this.file_url = file_url; this.send_at = send_at; this.status = status; this.reply_to_id = reply_to_id; @@ -39,6 +39,16 @@ public class Message { this.forwarded_from = forwarded_from; } + public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, String content, String messageType, LocalDateTime now) { + this.message_id = messageId; + this.sender_id = senderId; + this.receiver_id = receiverId; + this.receiver_type = receiverType; + this.content = content; + this.message_type = messageType; + this.send_at = now; + } + public UUID getMessage_id() { return message_id; @@ -85,14 +95,6 @@ public class Message { this.message_type = message_type; } - public String getFile_url() { - return file_url; - } - - public void setFile_url(String file_url) { - this.file_url = file_url; - } - public LocalDateTime getSend_at() { return send_at; } @@ -148,4 +150,11 @@ public class Message { public void setForwarded_from(UUID forwarded_from) { this.forwarded_from = forwarded_from; } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } + public List getAttachments() { + return attachments; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java index 372600b..17ca2a9 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java +++ b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java @@ -5,17 +5,29 @@ import java.util.UUID; public class PrivateChat { private final UUID chat_id; + private boolean user1_deleted; + private boolean user2_deleted; private UUID user1_id; private UUID user2_id; private LocalDateTime created_at; - public PrivateChat(UUID chat_id, UUID user1_id, UUID user2_id, LocalDateTime created_at){ + public PrivateChat(UUID chat_id, UUID user1_id, UUID user2_id){ this.chat_id = chat_id; this.user1_id =user1_id; this.user2_id =user2_id; this.created_at =created_at; } + public PrivateChat(UUID chatId, UUID user1, UUID user2, boolean user1Deleted, boolean user2Deleted, LocalDateTime createdAt) { + this.chat_id = chatId; + this.user1_id = user1; + this.user2_id = user2; + this.user1_deleted = user1Deleted; + this.user2_deleted = user2Deleted; + this.created_at = createdAt; + } + + public void setUser1_id(UUID user1_id){this.user1_id =user1_id;} public void setUser2_id(UUID user2_id){this.user2_id =user2_id;} public void setCreated_at(LocalDateTime created_at){this.created_at = created_at;} diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 32f7274..7480ff7 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -87,36 +87,126 @@ public class ClientHandler implements Runnable { List groups = GroupDatabase.getGroupsByUser(user.getInternal_uuid()); List channels = ChannelDatabase.getChannelsByUser(user.getInternal_uuid()); List unreadMessages = MessageDatabase.getUnreadMessages(user.getInternal_uuid()); + List archivedChatIds = ArchivedChatDatabase.getArchivedChats(user.getInternal_uuid()); + + user.setContactList(contacts); user.setGroupList(groups); user.setChannelList(channels); user.setUnreadMessages(unreadMessages); + + List chatList = new ArrayList<>(); + List archivedChatList = new ArrayList<>(); + List activeChatList = new ArrayList<>(); + + + JSONArray contactList = new JSONArray(); 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(), + JSONObject c = new JSONObject(); + c.put("user_id", contact.getUser_id().toString()); + c.put("contact_id", contact.getContact_id().toString()); + c.put("is_blocked", contact.getIs_blocked()); + + c.put("profile_name", target.getProfile_name()); + c.put("image_url", target.getImage_url()); + + contactList.put(c); + } + + + +// 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", - last, + lastMessageTime, false, false - )); + ); + entry.setOtherUserId(otherId); + + if (archivedChatIds.contains(chat.getChat_id())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + + for (Group group : groups) { LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); boolean isOwner = GroupDatabase.isOwner(group.getInternal_uuid(), user.getInternal_uuid()); boolean isAdmin = GroupDatabase.isAdmin(group.getInternal_uuid(), user.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// group.getInternal_uuid(), +// group.getGroup_id(), +// group.getGroup_name(), +// group.getImage_url(), +// "group", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( group.getInternal_uuid(), group.getGroup_id(), group.getGroup_name(), @@ -125,7 +215,17 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(group.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } + + } for (Channel channel : channels) { @@ -133,7 +233,18 @@ public class ClientHandler implements Runnable { boolean isOwner = ChannelDatabase.isOwner(channel.getInternal_uuid(), user.getInternal_uuid()); boolean isAdmin = ChannelDatabase.isAdmin(channel.getInternal_uuid(), user.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// channel.getInternal_uuid(), +// channel.getChannel_id(), +// channel.getChannel_name(), +// channel.getImage_url(), +// "channel", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( channel.getInternal_uuid(), channel.getChannel_id(), channel.getChannel_name(), @@ -142,9 +253,29 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(channel.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + activeChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + archivedChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + chatList.sort((a, b) -> { if (a.getLastMessageTime() == null) return 1; @@ -154,6 +285,9 @@ public class ClientHandler implements Runnable { JSONObject userData = JsonUtil.userToJson(user); userData.put("chat_list", JsonUtil.chatListToJson(chatList)); + userData.put("archived_chat_list", JsonUtil.chatListToJson(archivedChatList)); + userData.put("active_chat_list", JsonUtil.chatListToJson(activeChatList)); + userData.put("contact_list", contactList); response = new ResponseModel("success", "Welcome " + user.getProfile_name(), userData); } break; @@ -518,33 +652,85 @@ public class ClientHandler implements Runnable { List contacts = ContactDatabase.getContacts(currentUser.getInternal_uuid()); List groups = GroupDatabase.getGroupsByUser(currentUser.getInternal_uuid()); List channels = ChannelDatabase.getChannelsByUser(currentUser.getInternal_uuid()); - + List archivedChatIds = ArchivedChatDatabase.getArchivedChats(currentUser.getInternal_uuid()); List chatList = new ArrayList<>(); + List archivedChatList = new ArrayList<>(); + List activeChatList = new ArrayList<>(); - for (Contact contact : contacts) { - User target = userDatabase.findByInternalUUID(contact.getContact_id()); - if (target == null) continue; - LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(currentUser.getInternal_uuid(), target.getInternal_uuid(), "private"); - chatList.add(new ChatEntry( - target.getInternal_uuid(), - target.getUser_id(), - target.getProfile_name(), - target.getImage_url(), + + + +// for (Contact contact : contacts) { +// User target = userDatabase.findByInternalUUID(contact.getContact_id()); +// if (target == null) continue; +// +// LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(currentUser.getInternal_uuid(), target.getInternal_uuid(), "private"); +// +// chatList.add(new ChatEntry( +// target.getInternal_uuid(), +// 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", - last, + lastMessageTime, false, false - )); + ); + entry.setOtherUserId(otherId); + + if (archivedChatIds.contains(chat.getChat_id())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + for (Group group : groups) { LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); boolean isOwner = GroupDatabase.isOwner(group.getInternal_uuid(), currentUser.getInternal_uuid()); boolean isAdmin = GroupDatabase.isAdmin(group.getInternal_uuid(), currentUser.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// group.getInternal_uuid(), +// group.getGroup_id(), +// group.getGroup_name(), +// group.getImage_url(), +// "group", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( group.getInternal_uuid(), group.getGroup_id(), group.getGroup_name(), @@ -553,7 +739,17 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(group.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } + + } for (Channel channel : channels) { @@ -561,7 +757,18 @@ public class ClientHandler implements Runnable { boolean isOwner = ChannelDatabase.isOwner(channel.getInternal_uuid(), currentUser.getInternal_uuid()); boolean isAdmin = ChannelDatabase.isAdmin(channel.getInternal_uuid(), currentUser.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// channel.getInternal_uuid(), +// channel.getChannel_id(), +// channel.getChannel_name(), +// channel.getImage_url(), +// "channel", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( channel.getInternal_uuid(), channel.getChannel_id(), channel.getChannel_name(), @@ -570,17 +777,39 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(channel.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + activeChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + archivedChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + chatList.sort((a, b) -> { if (a.getLastMessageTime() == null) return 1; if (b.getLastMessageTime() == null) return -1; return b.getLastMessageTime().compareTo(a.getLastMessageTime()); }); - JSONObject data = new JSONObject(); data.put("chat_list", JsonUtil.chatListToJson(chatList)); + data.put("archived_chat_list", JsonUtil.chatListToJson(archivedChatList)); + data.put("active_chat_list", JsonUtil.chatListToJson(activeChatList)); response = new ResponseModel("success", "Chat list updated.", data); break; @@ -1029,13 +1258,17 @@ public class ClientHandler implements Runnable { switch (receiverType) { case "private" -> { - User otherUser = new userDatabase().findByInternalUUID(UUID.fromString(receiverId)); - if (otherUser == null) { - response = new ResponseModel("error", "User not found."); + UUID chatId = UUID.fromString(receiverId); + List members = PrivateChatDatabase.getMembers(chatId); + if (!members.contains(currentUser.getInternal_uuid())) { + response = new ResponseModel("error", "You're not a member of this private chat."); break; } - messages = MessageDatabase.privateChatHistory(currentUser.getInternal_uuid(), otherUser.getInternal_uuid()); + + messages = MessageDatabase.privateChatHistory(chatId); } + + case "group" -> { Group group = GroupDatabase.findByInternalUUID(UUID.fromString(receiverId)); if (group == null) { @@ -1070,6 +1303,9 @@ public class ClientHandler implements Runnable { obj.put("receiver_type", m.getReceiver_type()); obj.put("content", m.getContent()); obj.put("send_at", m.getSend_at().toString()); + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown"; + obj.put("sender_name", senderName); messageArray.put(obj); } @@ -1196,26 +1432,33 @@ public class ClientHandler implements Runnable { - case "delete_private_chat" : { + case "delete_private_chat": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); break; } - UUID targetId = UUID.fromString(requestJson.getString("target_id")); + + // دریافت شناسه چت و نوع حذف (یک‌طرفه یا دوطرفه) + UUID targetId = UUID.fromString(requestJson.getString("chat_id")); boolean both = requestJson.getBoolean("both"); - //RealTime + // 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 = PrivateChatService.deletePrivateChat(currentUser.getInternal_uuid(), targetId, both); + + // فراخوانی متد حذف چت + response = handleDeleteChat(requestJson); break; } + case "get_group_permissions": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); @@ -1707,6 +1950,85 @@ public class ClientHandler implements Runnable { // break; // } // + + case "archive_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + String chatType = requestJson.getString("chat_type"); + + boolean success = ArchivedChatDatabase.archiveChat(currentUser.getInternal_uuid(), chatId, chatType); + response = success + ? new ResponseModel("success", "Chat archived successfully.") + : new ResponseModel("error", "Failed to archive chat."); + break; + } + + + case "unarchive_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + + boolean success = ArchivedChatDatabase.unarchiveChat(currentUser.getInternal_uuid(), chatId); + response = success + ? new ResponseModel("success", "Chat unarchived successfully.") + : new ResponseModel("error", "Failed to unarchive chat."); + break; + } + + case "send_message" : { + response = handleSendMessage(requestJson); + } + break; + + + case "get_or_create_private_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + try { + UUID user1 = UUID.fromString(requestJson.getString("user1")); + UUID user2 = UUID.fromString(requestJson.getString("user2")); + + UUID chatId = PrivateChatDatabase.getOrCreateChat(user1, user2); + + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + response = new ResponseModel("success", "Private chat ID retrieved.", data); + } catch (Exception e) { + e.printStackTrace(); + response = new ResponseModel("error", "Invalid data or internal error."); + } + + break; + } + + + + case "get_private_chat_target": { + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + userId = currentUser.getInternal_uuid(); + + UUID targetId = PrivateChatDatabase.getOtherUserInChat(chatId, userId); + if (targetId == null) { + response = new ResponseModel("error", "Could not find other user."); + } else { + JSONObject data = new JSONObject(); + data.put("target_id", targetId.toString()); + response = new ResponseModel("success", "Target fetched.", data); + } + break; + } + case "get_user_profile": { // Get the current user's UUID UUID user_UUID = this.currentUser.getInternal_uuid(); @@ -1844,4 +2166,129 @@ public class ClientHandler implements Runnable { } + + + private ResponseModel handleSendMessage(JSONObject json) { + + try { + if (currentUser == null) + return new ResponseModel("error", "Unauthorized. Please login first."); + + UUID messageId = UUID.randomUUID(); + UUID senderId = currentUser.getInternal_uuid(); + String receiverType = json.getString("receiver_type"); + UUID receiverId; + receiverId = UUID.fromString(json.getString("receiver_id")); + + if(Objects.equals(receiverType, "private")){ + PrivateChatDatabase.clearDeletedFlag(senderId, receiverId); + } + + + String content = json.optString("content", ""); + String messageType = json.optString("message_type", "TEXT"); + + boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType); + if (!inserted) + return new ResponseModel("error", "Failed to insert message."); + + if (json.has("attachments")) { + JSONArray attachmentsArray = json.getJSONArray("attachments"); + List attachments = new ArrayList<>(); + + for (int i = 0; i < attachmentsArray.length(); i++) { + JSONObject attJson = attachmentsArray.getJSONObject(i); + attachments.add(new FileAttachment( + attJson.getString("file_url"), + attJson.getString("file_type") + )); + } + + boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments); + if (!attInserted) + return new ResponseModel("error", "Message inserted but failed to attach files."); + } + + // Send real-time message + Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); + List receivers = getReceiversForChat(receiverId, receiverType); + receivers.remove(senderId); + RealTimeEventDispatcher.sendNewMessage(msg, receivers); + + // Update chat list (last_message_time) + JSONObject chatUpdate = new JSONObject(); + chatUpdate.put("chat_id", receiverId.toString()); + chatUpdate.put("chat_type", receiverType); + chatUpdate.put("last_message_time", LocalDateTime.now().toString()); + + JSONObject chatPayload = new JSONObject(); + chatPayload.put("action", "chat_updated"); + chatPayload.put("data", chatUpdate); + + for (UUID receiver : receivers) { + RealTimeEventDispatcher.sendToUser(receiver, chatPayload); + } + + JSONObject data = new JSONObject(); + data.put("message_id", messageId.toString()); + return new ResponseModel("success", "Message sent successfully.", data); + + } catch (Exception e) { + e.printStackTrace(); + return new ResponseModel("error", "Exception occurred while sending message."); + } + } + + + private List getReceiversForChat(UUID receiverId, String receiverType) { + switch (receiverType) { + case "private": + return PrivateChatDatabase.getMembers(receiverId); + case "group": + return GroupDatabase.getMemberUUIDs(receiverId); + case "channel": + return ChannelDatabase.getSubscriberUUIDs(receiverId); + default: + return new ArrayList<>(); + } + } + + + private ResponseModel handleDeleteChat(JSONObject json) { + try { + if (currentUser == null) + return new ResponseModel("error", "Unauthorized"); + + UUID chatId = UUID.fromString(json.getString("chat_id")); + boolean bothSides = json.optBoolean("both_sides", json.optBoolean("both", false)); + PrivateChat chat = PrivateChatDatabase.findById(chatId); + if (chat == null) return new ResponseModel("error", "Chat not found."); + + UUID self = currentUser.getInternal_uuid(); + UUID other = chat.getUser1_id().equals(self) ? chat.getUser2_id() : chat.getUser1_id(); + + if (bothSides) { + PrivateChatDatabase.markBothDeleted(chatId); + MessageDatabase.markGloballyDeleted(chatId); + MessageDatabase.logDeletedMessagesFor(chatId, self); + MessageDatabase.logDeletedMessagesFor(chatId, other); + return new ResponseModel("success", "Chat deleted for both sides."); + } else { + if (chat.getUser1_id().equals(self)) + PrivateChatDatabase.markUser1Deleted(chatId); + else + PrivateChatDatabase.markUser2Deleted(chatId); + + MessageDatabase.logDeletedMessagesFor(chatId, self); + return new ResponseModel("success", "Chat deleted (one-sided)."); + } + + } catch (Exception e) { + e.printStackTrace(); + return new ResponseModel("error", "Exception while deleting chat."); + } + } + + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index 3c96120..c9de28d 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Server; import org.json.JSONObject; import org.to.telegramfinalproject.Database.ChannelDatabase; import org.to.telegramfinalproject.Database.GroupDatabase; +import org.to.telegramfinalproject.Database.userDatabase; import org.to.telegramfinalproject.Models.Message; import org.to.telegramfinalproject.Models.User; @@ -151,7 +152,6 @@ public class RealTimeEventDispatcher { data.put("sender", sender.getUser_id()); data.put("receiver_type", msg.getReceiver_type()); data.put("receiver_id", msg.getReceiver_id()); - data.put("file_url", msg.getFile_url()); data.put("file_type", msg.getMessage_type()); // IMAGE, FILE, VIDEO... data.put("time", msg.getSend_at().toString()); @@ -305,4 +305,30 @@ public class RealTimeEventDispatcher { broadcastToUsers(affectedUsers, event); } + + public static void sendNewMessage(Message message, List receivers) { + JSONObject payload = new JSONObject(); + payload.put("action", "new_message"); + + JSONObject data = new JSONObject(); + data.put("id", message.getMessage_id().toString()); + data.put("sender_id", message.getSender_id().toString()); + data.put("receiver_id", message.getReceiver_id().toString()); + data.put("receiver_type", message.getReceiver_type()); + data.put("content", message.getContent()); + data.put("send_at", message.getSend_at().toString()); + + User sender = userDatabase.findByInternalUUID(message.getSender_id()); + if (sender != null) { + data.put("sender_name", sender.getProfile_name()); + } + + payload.put("data", data); + + for (UUID userId : receivers) { + sendToUser(userId, payload); + } + } + + } diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index f07532e..e4b7d1c 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -27,24 +27,28 @@ CREATE TABLE IF NOT EXISTS messages ( receiver_id UUID NOT NULL , content TEXT, message_type VARCHAR(20) DEFAULT 'TEXT' CHECK (message_type IN ('TEXT','IMAGE','FILE','VIDEO','AUDIO','STICKER','GIF')), - file_url TEXT, --(Bouns) send_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20), -- SEND,DELIVERED,READ reply_to_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bouns) - is_edited BOOLEAN DEFAULT FALSE, --(Bonus) - original_message_id UUID REFERENCES messages(message_id) - ON DELETE SET NULL, --(Bonus) + is_edited BOOLEAN DEFAULT FALSE, --(Bonus) + edited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + original_message_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bonus) forwarded_by UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) - forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL --(Bouns) + forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) + is_deleted_globally BOOLEAN DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS private_chat ( chat_id UUID PRIMARY KEY, user1_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, user2_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, + user1_deleted BOOLEAN DEFAULT FALSE, + user2_deleted BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_users UNIQUE (user1_id, user2_id) ); + + CREATE TABLE IF NOT EXISTS groups ( internal_uuid UUID PRIMARY KEY, group_id VARCHAR(70) UNIQUE, @@ -67,7 +71,7 @@ CREATE TABLE group_members ( CREATE TABLE IF NOT EXISTS channels ( channel_id VARCHAR(70) UNIQUE, - internal_uuid UUID PRIMARY KEY,, + internal_uuid UUID PRIMARY KEY, channel_name VARCHAR(100), creator_id UUID REFERENCES users(internal_uuid), image_url TEXT, @@ -119,3 +123,30 @@ CREATE TABLE archived_chats ( ); +CREATE TABLE deleted_messages ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + + +CREATE TABLE IF NOT EXISTS message_reactions ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + emoji TEXT NOT NULL, + reacted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + +--Multi attachments +CREATE TABLE IF NOT EXISTS message_attachments ( + attachment_id UUID PRIMARY KEY, + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_type VARCHAR(20) CHECK (file_type IN ('IMAGE','VIDEO','AUDIO','FILE','GIF','STICKER')), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + +