diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 8d7e6c2..af89ce9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1627,7 +1627,7 @@ public class ActionHandler { String input = scanner.nextLine().trim(); switch (input) { - case "1" -> sendMessage(chatId, "private"); + case "1" -> sendMessageInteractive(chatId, "private"); case "2" -> { viewMessagesInChat(chat); } case "3" -> { return false; } default -> System.out.println("Invalid choice."); @@ -3543,69 +3543,69 @@ public class ActionHandler { } - 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 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")); +// } +// +// } @@ -3895,7 +3895,7 @@ public class ActionHandler { } if(input.equalsIgnoreCase("S")){ - sendMessage(chat.getId(), chat.getType()); + sendMessageInteractive(chat.getId(), chat.getType()); } try { int index = Integer.parseInt(input); diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 841c227..000db70 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -5,6 +5,9 @@ import org.to.telegramfinalproject.Models.ChatEntry; import java.io.BufferedReader; import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; @@ -117,7 +120,7 @@ public class IncomingMessageListener implements Runnable { "removed_from_group", "removed_from_channel", "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated", "created_private_chat", - "message_reacted", "message_unreacted" -> true; + "message_reacted", "message_unreacted" , "chat_updated"-> true; default -> false; }; } @@ -170,58 +173,125 @@ 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()); +// }); +// 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()); +// }); +// 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()); +// }); +// +// 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 updateLastMessageTime(JSONObject msg) { try { UUID chatUUID = UUID.fromString(msg.getString("chat_id")); String newTime = msg.optString("last_message_time", null); + if (newTime == null || newTime.isBlank()) return; - 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()); - }); + updateOneList(Session.chatList, chatUUID, newTime); + updateOneList(Session.activeChats, chatUUID, newTime); + updateOneList(Session.archivedChats, chatUUID, newTime); - 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()); - }); - 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()); - }); - 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()); - }); + sortByLastMessageTime(Session.chatList); + sortByLastMessageTime(Session.activeChats); + sortByLastMessageTime(Session.archivedChats); 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 updateOneList(List list, UUID chatUUID, String newTime) { + if (list == null) return; + for (ChatEntry chat : list) { + if (chatUUID.equals(chat.getId())) { // ✅ internal UUID + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + break; + } + } + } + + private static void sortByLastMessageTime(java.util.List list) { + if (list == null) return; + list.sort((a, b) -> { + var ta = parseTs(String.valueOf(a.getLastMessageTime())); + var tb = parseTs(String.valueOf(b.getLastMessageTime())); + if (ta == null && tb == null) return 0; + if (ta == null) return 1; // nullها برن آخر + if (tb == null) return -1; + return tb.compareTo(ta); // نزولی (جدیدتر بالاتر) + }); + + // اگر «Saved Messages» رو می‌خوای همیشه بالا پین کنی، این ۴ خط رو نگه دار؛ + // اگر نه، حذفش کن. + for (int i = 0; i < list.size(); i++) { + if (list.get(i).isSavedMessages()) { + list.add(0, list.remove(i)); + break; + } + } + } + + private static java.time.LocalDateTime parseTs(String s) { + if (s == null) return null; + s = s.trim(); + if (s.isEmpty() || s.equalsIgnoreCase("null")) return null; + try { return java.time.OffsetDateTime.parse(s).toLocalDateTime(); } catch (Exception ignore) {} + try { return java.time.LocalDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception ignore) {} + return null; + } + + + private void handleAdminRoleChanged(JSONObject data) throws IOException { String chatType = data.optString("chat_type", ""); String chatId = data.optString("group_id", @@ -368,4 +438,7 @@ public class IncomingMessageListener implements Runnable { } } } + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3037223..2cf2c98 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -3046,6 +3046,59 @@ public class ClientHandler implements Runnable { out.println(ack.toString()); out.flush(); + + // بعد از out.flush(); و فقط اگر ok==true + if (ok) { + try { + // 1) دریافت پیام از DB تا send_at و... دقیق باشد + Message m = MessageDatabase.findById(messageId); // اگر چنین متدی نداری، با پارامترهای همین متد بساز/پر کن + + // 2) لیست دریافت‌کنندگان بر اساس نوع چت + List receivers = getReceiversForChat(receiverId, rType.toLowerCase()); + + + // 3) ساخت payload شامل اتچمنت (media) + User sender = userDatabase.findByInternalUUID(senderId); + JSONObject payload = new JSONObject() + .put("action", "new_message") + .put("data", new JSONObject() + .put("id", m.getMessage_id().toString()) + .put("chat_id", receiverId.toString()) + .put("chat_type", rType.toLowerCase()) + .put("sender_id", senderId.toString()) + .put("sender_name", sender != null ? sender.getProfile_name() : JSONObject.NULL) + .put("message_type", messageType.toLowerCase()) + .put("text", (text == null || text.isEmpty()) ? JSONObject.NULL : text) + .put("media", new JSONObject() + .put("media_id", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) + .put("file_name", fileName) + .put("mime_type", mimeType) + .put("size_bytes", fileSize) + .put("url", fileUrl) + .put("thumbnail_url", JSONObject.NULL) + .put("width", safeWidth) + .put("height", safeHeight) + .put("duration_ms", 0) + ) + .put("send_at", m.getSend_at().toString()) + .put("status", "SENT") + ); + + // 4) ارسال به همه اعضا (از جمله خودِ فرستنده اگر می‌خواهی UI آن هم یکپارچه آپدیت شود) + for (UUID uid : receivers) { + RealTimeEventDispatcher.sendToUser(uid, payload); + } + + // (اختیاری) رویداد آپدیت چت‌لیست برای sort بر اساس آخرین پیام + RealTimeEventDispatcher.notifyChatUpdated(receiverId, rType, m); + + } catch (Exception ex) { + ex.printStackTrace(); + // اگر ذخیره شد ولی Broadcast شکست خورد، می‌توانی Log کنی یا Retry سبک انجام دهی + } + } + + } catch (Exception e) { e.printStackTrace(); out.println(new JSONObject().put("status","error").put("message","exception").toString()); diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index db8c6a7..6834bef 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -1,10 +1,7 @@ package org.to.telegramfinalproject.Server; import org.json.JSONObject; -import org.to.telegramfinalproject.Database.ChannelDatabase; -import org.to.telegramfinalproject.Database.ContactDatabase; -import org.to.telegramfinalproject.Database.GroupDatabase; -import org.to.telegramfinalproject.Database.userDatabase; +import org.to.telegramfinalproject.Database.*; import org.to.telegramfinalproject.Models.Message; import org.to.telegramfinalproject.Models.User; @@ -12,7 +9,9 @@ import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.UUID; public class RealTimeEventDispatcher { @@ -425,4 +424,81 @@ public class RealTimeEventDispatcher { } + public static void notifyChatUpdated(UUID chatId, String chatType, Message lastMsg) { + if (chatId == null || chatType == null) return; + + final String type = chatType.toLowerCase(Locale.ROOT); + + List receivers; + switch (type) { + case "private": + receivers = PrivateChatDatabase.getMembers(chatId); + break; + + case "group": + receivers = GroupDatabase.getMemberUUIDs(chatId); + break; + case "channel": + receivers = ChannelDatabase.getSubscriberUUIDs(chatId); + break; + default: + receivers = Collections.emptyList(); + } + if (receivers == null || receivers.isEmpty()) return; + + // 2) ساخت خلاصه آخرین پیام برای نمایش در لیست چت + String senderName = null; + if (lastMsg != null && lastMsg.getSender_id() != null) { + User u = userDatabase.findByInternalUUID(lastMsg.getSender_id()); + if (u != null) senderName = u.getProfile_name(); + } + + String messageType = lastMsg != null && lastMsg.getMessage_type() != null + ? lastMsg.getMessage_type().toLowerCase(Locale.ROOT) : "text"; + + // preview ساده: برای مدیا، برچسب کوتاه؛ برای متن، کوتاه‌سازی + String preview; + if (!"text".equals(messageType)) { + switch (messageType) { + case "image": preview = "[Photo]"; break; + case "video": preview = "[Video]"; break; + case "audio": preview = "[Audio]"; break; + case "file": preview = "[File]"; break; + default: preview = "[Media]"; + } + } else { + String t = lastMsg != null ? nullToEmpty(lastMsg.getContent()) : ""; + preview = t.length() > 80 ? t.substring(0, 80) + "…" : t; + } + + String sendAt = (lastMsg != null && lastMsg.getSend_at() != null) + ? lastMsg.getSend_at().toString() + : java.time.OffsetDateTime.now().toString(); + + // 3) payload رویداد chat_updated + JSONObject payload = new JSONObject() + .put("action", "chat_updated") + .put("data", new JSONObject() + .put("chat_id", chatId.toString()) + .put("chat_type", type) + .put("last_message", new JSONObject() + .put("id", lastMsg != null ? lastMsg.getMessage_id().toString() : JSONObject.NULL) + .put("sender_id", lastMsg != null ? lastMsg.getSender_id().toString() : JSONObject.NULL) + .put("sender_name", senderName != null ? senderName : JSONObject.NULL) + .put("message_type", messageType) + .put("preview", preview) + .put("send_at", sendAt) + ) + .put("last_message_time", sendAt) + .put("update_reason", "new_message") // برای کلاینت مفید است + ); + + // 4) ارسال به همه اعضای چت + for (UUID uid : receivers) { + sendToUser(uid, payload); + } + } + + private static String nullToEmpty(String s) { return s == null ? "" : s; } + }