diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 3e74765..3c7e815 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -520,7 +520,7 @@ public class ActionHandler { chat.optBoolean("is_admin", false) ); if (chat.has("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); } archivedChats.add(entry); @@ -541,7 +541,7 @@ public class ActionHandler { chat.optBoolean("is_admin", false) ); if (chat.has("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); } activeChats.add(entry); @@ -3415,6 +3415,7 @@ public class ActionHandler { [number] - Interact with message N - Next page (older messages) 0 - Back to chat menu + S - Send message ➤ Choice: """); String input = scanner.nextLine().trim(); @@ -3425,6 +3426,9 @@ public class ActionHandler { continue; } + if(input.equalsIgnoreCase("S")){ + sendMessage(chat.getId(), chat.getType()); + } try { int index = Integer.parseInt(input); if (index < 1 || index > messages.length()) { diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index ed83bf7..e6882a2 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -74,12 +74,12 @@ public class IncomingMessageListener implements Runnable { private boolean isRealTimeEvent(String action) { return switch (action) { - case "new_message", "message_edited", "message_deleted", + case "new_message", "message_edited", "message_deleted_global", "user_status_changed", "added_to_group", "added_to_channel", "update_group_or_channel", "chat_deleted", "blocked_by_user", "unblocked_by_user", "message_seen", "removed_from_group", "removed_from_channel", - "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" -> true; + "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true; default -> false; }; } @@ -294,36 +294,52 @@ public class IncomingMessageListener implements Runnable { private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { - 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 senderName = msg.optString("sender_name","Unknown"); + String content = msg.optString("content","(empty)"); + String sendAt = msg.optString("send_at","-"); + String chatId = msg.optString("receiver_id", msg.optString("chat_id","")); + String kind = msg.optString("kind","plain"); + JSONObject meta = msg.optJSONObject("meta"); - String receiverId = msg.optString("receiver_id", ""); - String receiverType = msg.optString("receiver_type", ""); + String prefix = ""; + if ("reply".equals(kind) && meta != null) { + var rt = meta.optJSONObject("reply_to"); + prefix = "[reply → " + (rt!=null?rt.optString("excerpt",""):"") + "] "; + } else if ("forward".equals(kind) && meta != null) { + var ff = meta.optJSONObject("forwarded_from"); + prefix = "[forwarded from " + (ff!=null?ff.optString("sender_name","unknown"):"unknown") + "] "; + } boolean isInCurrentChat = Session.inChatMenu && - Session.currentChatId != null && - Session.currentChatId.equals(receiverId); + Session.currentChatId != null && Session.currentChatId.equals(chatId); if (isInCurrentChat) { - System.out.println(senderName + ": " + content + " (" + sendAt + ")"); + System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")"); } else { - System.out.println("💬 Message from " + senderName + " in " + receiverType + " chat: " + content); + System.out.println("💬 Message from " + senderName + ": " + prefix + content); Session.forceRefreshChatList = true; } } + case "message_edited" -> { System.out.println("\n✏️ Message Edited:"); System.out.println("ID: " + msg.getString("message_id")); System.out.println("New Content: " + msg.getString("new_content")); System.out.println("Edit Time: " + msg.getString("edited_at")); } - case "message_deleted" -> { + case "message_deleted_global" -> { System.out.println("\n🗑️ Message Deleted:"); System.out.println("Message ID: " + msg.getString("message_id")); } + case "message_reacted", "message_unreacted" -> { + String mid = msg.getString("message_id"); + String emoji = msg.getString("emoji"); + JSONObject counts = msg.optJSONObject("counts"); + int n = msg.optInt("count_for_emoji", 0); + System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + " → " + n); + } + case "user_status_changed" -> { System.out.println("\n🔄 User Status Changed:"); System.out.println("User: " + msg.getString("user_id")); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 404f2dc..db997ee 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -318,6 +318,52 @@ public class MessageDatabase { } } + public static String getExcerpt(UUID messageId) { + return getExcerpt(messageId, 80); + } + + public static String getExcerpt(UUID messageId, int maxLen) { + String sql = "SELECT content, message_type, is_deleted_globally FROM messages WHERE message_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, messageId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return "Message isn't available"; + + String content = rs.getString("content"); + String type = rs.getString("message_type"); // e.g. TEXT, IMAGE, VIDEO, AUDIO, FILE, STICKER... + boolean deleted = rs.getBoolean("is_deleted_globally"); + + if (deleted) return "This message was deleted"; + + if (type == null || type.equalsIgnoreCase("TEXT")) { + if (content == null || content.isBlank()) return "(empty)"; + return shorten(content, maxLen); + } + + //for media + switch (type.toUpperCase()) { + case "IMAGE": return "[Photo]"; + case "VIDEO": return "[Video]"; + case "AUDIO": return "[Audio]"; + case "VOICE": return "[Voice]"; + case "FILE": return "[File]"; + case "STICKER":return "[Sticker]"; + default: return "[" + type + "]"; + } + } + } catch (SQLException e) { + e.printStackTrace(); + return "Error"; + } + } + + private static String shorten(String s, int maxLen) { + if (s.length() <= maxLen) return s; + return s.substring(0, Math.max(0, maxLen - 1)).trim() + "…"; + } + public void markMessageAsRead(UUID messageId, UUID userId) { diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java index 35fdb5a..d027a5a 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java @@ -1,5 +1,7 @@ package org.to.telegramfinalproject.Database; +import org.json.JSONObject; + import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -44,4 +46,31 @@ public class MessageReactionDatabase { } return reactions; } + public static JSONObject getCountsAsJson(UUID messageId) { + String sql = """ + SELECT emoji, COUNT(*) AS c + FROM message_reactions + WHERE message_id = ? + GROUP BY emoji + """; + + JSONObject counts = new JSONObject(); + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, messageId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String emoji = rs.getString("emoji"); + int c = rs.getInt("c"); + counts.put(emoji, c); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return counts; // مثال: {"❤️":2,"👍":1} + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index 6cae138..e82b57c 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -275,4 +275,12 @@ public class PrivateChatDatabase { return null; } + public static UUID getOtherParticipant(UUID chatId, UUID me) { + List members = getMembers(chatId); + for (UUID u : members) { + if (!u.equals(me)) return u; + } + return null; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3a0e510..98fd941 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -2134,10 +2134,14 @@ public class ClientHandler implements Runnable { yield c != null ? c.getChannel_name() : "Unknown channel"; } case "private" -> { - UUID otherId = m.getSender_id().equals(currentUser.getInternal_uuid()) ? m.getReceiver_id() : m.getSender_id(); + UUID otherId = PrivateChatDatabase.getOtherParticipant( + m.getReceiver_id(), // chat_id + currentUser.getInternal_uuid() // my user uuid + ); User other = userDatabase.findByInternalUUID(otherId); yield other != null ? other.getProfile_name() : "Unknown user"; } + default -> "Unknown"; }; obj.put("receiver_name", receiverName); @@ -2228,10 +2232,16 @@ public class ClientHandler implements Runnable { } boolean success = MessageDatabase.markAsGloballyDeleted(messageId); - if (success) + if (success) { response = new ResponseModel("success", "Message deleted globally."); - else + Message updated = MessageDatabase.findById(messageId); + List receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null); + RealTimeEventDispatcher.notifyMessageDeletedGlobal(updated.getReceiver_id(), messageId, receivers); + + } + else{ response = new ResponseModel("error", "Failed to delete message globally."); + } break; } @@ -2260,6 +2270,10 @@ public class ClientHandler implements Runnable { response = new ResponseModel("error", "Failed to update message."); } else { response = new ResponseModel("success", "Message edited."); + Message updated = MessageDatabase.findById(msgId); + List receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null); + RealTimeEventDispatcher.notifyMessageEdited(updated.getReceiver_id(), updated.getMessage_id(), newContent, LocalDateTime.now(), receivers); + } break; } @@ -2284,6 +2298,15 @@ public class ClientHandler implements Runnable { ); boolean saved = MessageDatabase.saveReplyMessage(message); + + String excerpt = MessageDatabase.getExcerpt(replyToId); + JSONObject meta = new JSONObject().put("reply_to", new JSONObject() + .put("id", replyToId.toString()) + .put("excerpt", excerpt)); + + List receivers = Receivers.resolveFor(receiverType, receiverId, senderId); + RealTimeEventDispatcher.sendNewMessage(message, receivers, "reply", meta); + response = saved ? new ResponseModel("success", "Reply sent") : new ResponseModel("error", "Failed to send reply"); @@ -2322,9 +2345,17 @@ public class ClientHandler implements Runnable { ); boolean success = MessageDatabase.saveForwardedMessage(forwarded); + if (success) { response = new ResponseModel("success", "Message forwarded."); - // (اختیاری) ارسال ریل تایم به اعضای چت مقصد + JSONObject meta = new JSONObject().put("forwarded_from", new JSONObject() + .put("chat_id", original.getReceiver_id().toString()) + .put("message_id", original.getMessage_id().toString()) + .put("sender_id", original.getSender_id().toString()) + .put("sender_name", userDatabase.findByInternalUUID(original.getSender_id()).getProfile_name())); + + List receivers = Receivers.resolveFor(targetChatType, targetChatId, currentUser.getInternal_uuid()); + RealTimeEventDispatcher.sendNewMessage(forwarded, receivers, "forward", meta); } else { response = new ResponseModel("error", "Failed to forward message."); } @@ -2336,9 +2367,28 @@ public class ClientHandler implements Runnable { String reaction = requestJson.getString("reaction"); boolean success = MessageReactionDatabase.saveOrUpdateReaction(messageId, currentUser.getInternal_uuid(), reaction); - response = success - ? new ResponseModel("success", "Reaction saved.") - : new ResponseModel("error", "Failed to save reaction."); + + if (success) { + Message msg = MessageDatabase.findById(messageId); + + JSONObject counts = MessageReactionDatabase.getCountsAsJson(messageId); + + List receivers = Receivers.resolveFor(msg.getReceiver_type(), msg.getReceiver_id(), null); + + RealTimeEventDispatcher.notifyReactionAdded( + msg.getReceiver_id(), // chatId + messageId, // messageId + reaction, // emoji + counts.optInt(reaction, 0), + counts, + receivers + ); + + response = new ResponseModel("success", "Reaction saved."); + } else { + response = new ResponseModel("error", "Failed to save reaction."); + } + break; } diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index c9de28d..5e65eb9 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -81,33 +81,94 @@ public class RealTimeEventDispatcher { - public static void notifyMessageEdited(UUID messageId, String newContent, List receivers) { - - String editTime = LocalDateTime.now().toString(); + public static void notifyMessageEdited(UUID chatId, UUID messageId, String newContent, LocalDateTime editedAt, List receivers) { JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); data.put("message_id", messageId.toString()); data.put("new_content", newContent); - data.put("edited_at", editTime); + data.put("edited_at", editedAt.toString()); JSONObject event = new JSONObject(); - event.put("action", "edit_message"); + event.put("action", "message_edited"); event.put("data", data); broadcastToUsers(receivers, event); } - public static void notifyMessageDeleted(UUID messageId, List receivers) { + + public static void sendNewMessage(Message message, List receivers, String kind, JSONObject meta) { + JSONObject payload = new JSONObject(); + payload.put("action", "new_message"); + JSONObject data = new JSONObject(); + data.put("id", message.getMessage_id().toString()); + data.put("chat_id", message.getReceiver_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("message_type", message.getMessage_type()); + 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()); + + data.put("kind", kind == null ? "plain" : kind); // plain|reply|forward + if (meta != null) data.put("meta", meta); // reply_to {...} | forwarded_from {...} + + payload.put("data", data); + for (UUID userId : receivers) sendToUser(userId, payload); + } + + public static void notifyMessageDeletedGlobal(UUID chatId, UUID messageId, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); data.put("message_id", messageId.toString()); JSONObject event = new JSONObject(); - event.put("action", "delete_message"); + event.put("action", "message_deleted_global"); event.put("data", data); broadcastToUsers(receivers, event); } + + public static void notifyReactionAdded(UUID chatId, UUID messageId, String emoji, + int totalForEmoji, JSONObject countsAll, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + data.put("message_id", messageId.toString()); + data.put("emoji", emoji); + data.put("counts", countsAll); // {"❤️":3,"👍":1,...} + data.put("count_for_emoji", totalForEmoji); + + JSONObject event = new JSONObject(); + event.put("action", "message_reacted"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + public static void notifyReactionRemoved(UUID chatId, UUID messageId, String emoji, + int totalForEmoji, JSONObject countsAll, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + data.put("message_id", messageId.toString()); + data.put("emoji", emoji); + data.put("counts", countsAll); + data.put("count_for_emoji", totalForEmoji); + + JSONObject event = new JSONObject(); + event.put("action", "message_unreacted"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + + + public static void notifyUserUpdated(UUID userId, String newProfileName, String newImageUrl, List contactIds) { JSONObject data = new JSONObject(); data.put("user_id", userId.toString()); diff --git a/src/main/java/org/to/telegramfinalproject/Server/Receivers.java b/src/main/java/org/to/telegramfinalproject/Server/Receivers.java new file mode 100644 index 0000000..578cde0 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/Receivers.java @@ -0,0 +1,21 @@ +package org.to.telegramfinalproject.Server; + +import org.to.telegramfinalproject.Database.ChannelDatabase; +import org.to.telegramfinalproject.Database.GroupDatabase; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; + +import java.util.List; +import java.util.UUID; + +public class Receivers { + public static List resolveFor(String type, UUID chatId, UUID exclude) { + List ids = switch (type) { + case "private" -> PrivateChatDatabase.getMembers(chatId); + case "group" -> GroupDatabase.getMemberUUIDs(chatId); + case "channel" -> ChannelDatabase.getSubscriberUUIDs(chatId); + default -> List.of(); + }; + if (exclude != null) ids.remove(exclude); + return ids; + } +}