diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 5085ed5..f248058 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -381,71 +381,180 @@ public class MessageDatabase { +// public static List getUnreadMessages(UUID userId) { +// List messages = new ArrayList<>(); +// +// String sql = """ +// SELECT m.* +// FROM messages m +// LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? +// LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? +// WHERE r.user_id IS NULL +// AND d.message_id IS NULL +// AND m.is_deleted_globally = FALSE +// AND ( +// (m.receiver_type = 'private' AND m.receiver_id = ?) +// OR +// (m.receiver_type = 'group' AND EXISTS ( +// SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? +// )) +// OR +// (m.receiver_type = 'channel' AND EXISTS ( +// SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? +// )) +// ) +// ORDER BY m.send_at DESC +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// +// stmt.setObject(1, userId); // for message_receipts +// stmt.setObject(2, userId); // for deleted_messages +// stmt.setObject(3, userId); // for private messages +// stmt.setObject(4, userId); // for group members +// stmt.setObject(5, userId); // for channel subscribers +// +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// Message message = new Message( +// UUID.fromString(rs.getString("message_id")), +// rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, +// rs.getString("receiver_type"), +// UUID.fromString(rs.getString("receiver_id")), +// rs.getString("content"), +// rs.getString("message_type"), +// rs.getTimestamp("send_at").toLocalDateTime(), +// rs.getString("status"), +// rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, +// rs.getBoolean("is_edited"), +// rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, +// rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, +// rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null, +// rs.getBoolean("is_deleted_globally"), +// rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null +// ); +// +// messages.add(message); +// } +// +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// return messages; +// } + + + public static List getUnreadMessages(UUID userId) { + return getUnreadMessages(userId, 200); // یک سقف معقول + } + + public static List getUnreadMessages(UUID userId, int limit) { List messages = new ArrayList<>(); + // توجه: نام جدول private_chats/gm/cs را با اسامی واقعی دیتابیس‌ت هماهنگ کن String sql = """ SELECT m.* FROM messages m - LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? - LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? - WHERE r.user_id IS NULL + LEFT JOIN message_receipts r + ON r.message_id = m.message_id + AND r.user_id = ? + LEFT JOIN deleted_messages d + ON d.message_id = m.message_id + AND d.user_id = ? + WHERE r.message_id IS NULL AND d.message_id IS NULL AND m.is_deleted_globally = FALSE + AND m.sender_id <> ? AND ( - (m.receiver_type = 'private' AND m.receiver_id = ?) - OR - (m.receiver_type = 'group' AND EXISTS ( - SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? - )) - OR - (m.receiver_type = 'channel' AND EXISTS ( - SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? - )) + (m.receiver_type = 'private' AND EXISTS ( + SELECT 1 + FROM private_chat pc + WHERE pc.chat_id = m.receiver_id + AND (pc.user1_id = ? OR pc.user2_id = ?) + ))) + OR (m.receiver_type = 'group' AND EXISTS ( + SELECT 1 + FROM group_members gm + WHERE gm.group_id = m.receiver_id + AND gm.user_id = ? + )) + OR (m.receiver_type = 'channel' AND EXISTS ( + SELECT 1 + FROM channel_subscribers cs + WHERE cs.channel_id = m.receiver_id + AND cs.user_id = ? + )) ) - ORDER BY m.send_at DESC - """; + ORDER BY m.send_at ASC + LIMIT ? + """; try (Connection conn = ConnectionDb.connect(); - PreparedStatement stmt = conn.prepareStatement(sql)) { + PreparedStatement ps = conn.prepareStatement(sql)) { - stmt.setObject(1, userId); // for message_receipts - stmt.setObject(2, userId); // for deleted_messages - stmt.setObject(3, userId); // for private messages - stmt.setObject(4, userId); // for group members - stmt.setObject(5, userId); // for channel subscribers + int i = 1; + ps.setObject(i++, userId); // r.user_id + ps.setObject(i++, userId); // d.user_id + ps.setObject(i++, userId); // m.sender_id <> ? + ps.setObject(i++, userId); // pc.user1_id=userId + ps.setObject(i++, userId); // pc.user2_id=userId + ps.setObject(i++, userId); // gm.user_id + ps.setObject(i++, userId); // cs.user_id + ps.setInt(i++, Math.max(1, limit)); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - Message message = new Message( - UUID.fromString(rs.getString("message_id")), - rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, - rs.getString("receiver_type"), - UUID.fromString(rs.getString("receiver_id")), - rs.getString("content"), - rs.getString("message_type"), - rs.getTimestamp("send_at").toLocalDateTime(), - rs.getString("status"), - rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, - rs.getBoolean("is_edited"), - rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, - rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, - rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null, - rs.getBoolean("is_deleted_globally"), - rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null - ); - - messages.add(message); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + messages.add(mapMessage(rs)); // همان mapMessage که گفتیم + } } - } catch (SQLException e) { e.printStackTrace(); } - return messages; } + private static java.util.UUID readUUID(ResultSet rs, String col) throws SQLException { + Object o = rs.getObject(col); + if (o == null) return null; + if (o instanceof java.util.UUID) return (java.util.UUID) o; + return java.util.UUID.fromString(o.toString()); + } + + private static java.time.LocalDateTime readLdt(ResultSet rs, String col) throws SQLException { + java.sql.Timestamp ts = rs.getTimestamp(col); + return (ts == null) ? null : ts.toLocalDateTime(); + } + + private static boolean readBool(ResultSet rs, String col) throws SQLException { + boolean v = rs.getBoolean(col); + return v; + } + + private static Message mapMessage(ResultSet rs) throws SQLException { + Message m = new Message(); + + m.setMessage_id( readUUID(rs, "message_id") ); + m.setSender_id( readUUID(rs, "sender_id") ); + m.setReceiver_type( rs.getString("receiver_type") ); + m.setReceiver_id( readUUID(rs, "receiver_id") ); + m.setContent( rs.getString("content") ); + m.setMessage_type( rs.getString("message_type") ); + m.setSend_at( readLdt(rs, "send_at") ); + m.setStatus( rs.getString("status") ); + m.setReply_to_id( readUUID(rs, "reply_to_id") ); + m.setIs_edited( readBool(rs, "is_edited") ); + m.setEdited_at( readLdt(rs, "edited_at") ); + m.setOriginal_message_id( readUUID(rs, "original_message_id") ); + m.setForwarded_by( readUUID(rs, "forwarded_by") ); + m.setForwarded_from( readUUID(rs, "forwarded_from") ); + m.setIs_deleted_globally( readBool(rs, "is_deleted_globally") ); + + return m; + } public static List getAttachments(UUID messageId) { List attachments = new ArrayList<>(); @@ -1079,30 +1188,30 @@ public class MessageDatabase { } } - public static int getUnreadCount(UUID me, UUID targetId, String type) { - final String sql = - "SELECT COUNT(*) FROM messages " + - "WHERE receiver_type = ? AND receiver_id = ? " + - "AND sender_id <> ? " + - "AND (status IS NULL OR status <> 'SEEN')"; - - try (Connection conn = ConnectionDb.connect(); - PreparedStatement ps = conn.prepareStatement(sql)) { - - ps.setString(1, type.toLowerCase()); - ps.setObject(2, targetId); - ps.setObject(3, me); - - try (ResultSet rs = ps.executeQuery()) { - return rs.next() ? rs.getInt(1) : 0; - } - } catch (SQLException e) { - e.printStackTrace(); - return 0; - } - - - } +// public static int getUnreadCount(UUID me, UUID targetId, String type) { +// final String sql = +// "SELECT COUNT(*) FROM messages " + +// "WHERE receiver_type = ? AND receiver_id = ? " + +// "AND sender_id <> ? " + +// "AND (status IS NULL OR status <> 'SEEN')"; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement ps = conn.prepareStatement(sql)) { +// +// ps.setString(1, type.toLowerCase()); +// ps.setObject(2, targetId); +// ps.setObject(3, me); +// +// try (ResultSet rs = ps.executeQuery()) { +// return rs.next() ? rs.getInt(1) : 0; +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// return 0; +// } +// +// +// } private static Message mapRow(ResultSet rs) throws SQLException { Message m = new Message(); @@ -1126,4 +1235,112 @@ public class MessageDatabase { return m; } + + + public static List getUnreadMessageIds(UUID me, UUID chatId, String chatType, int limit) { + String sql = """ + SELECT m.message_id + FROM messages m + WHERE m.receiver_id = ? AND m.receiver_type = ? + AND m.sender_id <> ? + AND NOT EXISTS ( + SELECT 1 FROM message_receipts r + WHERE r.message_id = m.message_id + AND r.user_id = ? + ) + ORDER BY m.send_at ASC + LIMIT ? + """; + List ids = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.setString(2, chatType); + ps.setObject(3, me); + ps.setObject(4, me); + ps.setInt(5, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + ids.add((UUID) rs.getObject(1)); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return ids; + } + + // تعداد ناخوانده‌ها (برای چت‌لیست) + public static int getUnreadCount(UUID me, UUID chatId, String chatType) { + String sql = """ + SELECT COUNT(1) + FROM messages m + WHERE m.receiver_id = ? + AND m.receiver_type = ? + AND m.sender_id <> ? + AND NOT EXISTS ( + SELECT 1 FROM message_receipts r + WHERE r.message_id = m.message_id + AND r.user_id = ? + ) + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.setString(2, chatType); + ps.setObject(3, me); + ps.setObject(4, me); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + + public static int insertReceiptIfAbsent(UUID messageId, UUID userId) { + String sql = """ + INSERT INTO message_receipts(message_id, user_id, read_at) + VALUES (?, ?, now()) + ON CONFLICT (message_id, user_id) DO NOTHING + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + return ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + //Read status if it's first reader + public static int setMessageReadIfNeeded(UUID messageId, UUID viewerId) { + String sql = """ + UPDATE messages m + SET status = 'READ' + WHERE m.message_id = ? + AND m.sender_id <> ? + AND m.status <> 'READ' + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, viewerId); + return ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + return 0; + } + } + + + + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 8005356..31fc039 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -11,6 +11,7 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; +import java.sql.Connection; import java.time.LocalDateTime; import java.util.*; @@ -2734,6 +2735,55 @@ public class ClientHandler implements Runnable { break; } + case "mark_as_read": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + try { + UUID chatId = UUID.fromString(requestJson.getString("receiver_id")); + String chatType = requestJson.getString("receiver_type").toLowerCase(); + int limit = requestJson.optInt("limit", 500); + + List targetMessageIds = new ArrayList<>(); + if (requestJson.has("message_ids")) { + JSONArray arr = requestJson.getJSONArray("message_ids"); + for (int i = 0; i < arr.length(); i++) { + targetMessageIds.add(UUID.fromString(arr.getString(i))); + } + } else { + targetMessageIds = MessageDatabase.getUnreadMessageIds( + currentUser.getInternal_uuid(), chatId, chatType, limit + ); + } + + int updatedStatus = 0; + int insertedReceipts = 0; + + try (Connection c = ConnectionDb.connect()) { + c.setAutoCommit(false); + for (UUID mid : targetMessageIds) { + updatedStatus += MessageDatabase.setMessageReadIfNeeded(mid, currentUser.getInternal_uuid()); + insertedReceipts+= MessageDatabase.insertReceiptIfAbsent(mid, currentUser.getInternal_uuid()); + } + c.commit(); + } catch (Exception tx) { + tx.printStackTrace(); + } + + JSONObject data = new JSONObject(); + data.put("marked_count", targetMessageIds.size()); + data.put("status_updates", updatedStatus); + data.put("receipts_inserted", insertedReceipts); + + response = new ResponseModel("success", "Marked as read.", data); + } catch (Exception e) { + response = new ResponseModel("error", "Error in mark_as_read: " + e.getMessage()); + } + break; + } + + default: response = new ResponseModel("error", "Unknown action: " + action); } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 3a51a9d..aee726f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -109,9 +109,9 @@ public class ChatPageController { //Time formatter for messages - private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); - private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); - private static final String YESTERDAY_LABEL = "Yesterday"; +// private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); +// private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); +// private static final String YESTERDAY_LABEL = "Yesterday"; private static String str(org.json.JSONObject j, String k) { try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; } @@ -261,43 +261,43 @@ public class ChatPageController { themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme()); } - private void initCurrentUserId() { - try { - String meStr = org.to.telegramfinalproject.Client.Session - .currentUser.getString("internal_uuid"); - me = UUID.fromString(meStr); - } catch (Exception ignore) { - me = null; - } - } - - private String formatWhen(LocalDateTime ts) { - if (ts == null) return ""; - LocalDate today = LocalDate.now(); - LocalDate d = ts.toLocalDate(); - - if (d.isEqual(today)) { - return FMT_HHMM.format(ts); - } else if (d.isEqual(today.minusDays(1))) { - return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); - } else { - return FMT_DATE_TIME.format(ts); - } - } - - /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ - private LocalDateTime parseWhen(String iso) { - if (iso == null || iso.isEmpty()) return null; - try { - return OffsetDateTime.parse(iso).toLocalDateTime(); - } catch (Exception ignore) { - try { - return LocalDateTime.parse(iso); - } catch (Exception e) { - return null; - } - } - } +// private void initCurrentUserId() { +// try { +// String meStr = org.to.telegramfinalproject.Client.Session +// .currentUser.getString("internal_uuid"); +// me = UUID.fromString(meStr); +// } catch (Exception ignore) { +// me = null; +// } +// } +// +// private String formatWhen(LocalDateTime ts) { +// if (ts == null) return ""; +// LocalDate today = LocalDate.now(); +// LocalDate d = ts.toLocalDate(); +// +// if (d.isEqual(today)) { +// return FMT_HHMM.format(ts); +// } else if (d.isEqual(today.minusDays(1))) { +// return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); +// } else { +// return FMT_DATE_TIME.format(ts); +// } +// } +// +// /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ +// private LocalDateTime parseWhen(String iso) { +// if (iso == null || iso.isEmpty()) return null; +// try { +// return OffsetDateTime.parse(iso).toLocalDateTime(); +// } catch (Exception ignore) { +// try { +// return LocalDateTime.parse(iso); +// } catch (Exception e) { +// return null; +// } +// } +// } @FXML private void openSearchPanel() {