diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index cd8c6e0..afded31 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -172,11 +172,25 @@ public class IncomingMessageListener implements Runnable { bumpChatListFromUpdate(data); } + case "user_status_changed" -> { + + displayRealTimeMessage(action, msg); + Platform.runLater(() -> { + var mc = org.to.telegramfinalproject.UI.MainController.getInstance(); + var cp = (mc != null) ? mc.getChatPageController() : null; + if (cp != null) cp.onUserStatusChanged( + msg.optString("user_id",""), + msg.optString("status",""), + msg.optString("last_seen","") + ); + }); + } - case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted", - "user_status_changed", "blocked_by_user", "unblocked_by_user", "message_seen" -> { + + case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted" + , "blocked_by_user", "unblocked_by_user", "message_seen" -> { displayRealTimeMessage(action, msg); } diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java new file mode 100644 index 0000000..63a3eec --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ChatInfoDatabase.java @@ -0,0 +1,130 @@ +package org.to.telegramfinalproject.Database; + +import org.json.JSONObject; + +import java.sql.*; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class ChatInfoDatabase { + + /** + * هدر چت را بر اساس نوع آن برمی‌گرداند. + * private: name,image_url, online,last_seen + * group : name,image_url, member_count + * channel: name,image_url, member_count + */ + public static JSONObject getHeaderInfo(String type, UUID receiverId, UUID viewerId) throws SQLException { + try (Connection conn = ConnectionDb.connect()) { + switch (type.toLowerCase()) { + case "private": + return getPrivateHeader(conn, receiverId, viewerId); + case "group": + return getGroupHeader(conn, receiverId); + case "channel": + return getChannelHeader(conn, receiverId); + default: + throw new IllegalArgumentException("Unsupported receiver_type: " + type); + } + } + } + + /** هدر چت خصوصی: other-user نسبت به viewerId را پیدا می‌کنیم */ + private static JSONObject getPrivateHeader(Connection conn, UUID chatId, UUID viewerId) throws SQLException { + if (viewerId == null) throw new IllegalArgumentException("viewerId is required for private header."); + + final String sql = """ + SELECT + u.profile_name AS name, + COALESCE(u.image_url, '') AS image_url, + (u.status = 'online') AS online, + u.status AS status, + u.last_seen AS last_seen + FROM private_chat pc + JOIN users u + ON u.internal_uuid = CASE WHEN pc.user1_id = ? THEN pc.user2_id ELSE pc.user1_id END + WHERE pc.chat_id = ? + AND (pc.user1_id = ? OR pc.user2_id = ?) + LIMIT 1 + """; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + int i = 1; + ps.setObject(i++, viewerId); + ps.setObject(i++, chatId); + ps.setObject(i++, viewerId); + ps.setObject(i++, viewerId); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Private chat not found or viewer is not a participant."); + + JSONObject j = new JSONObject(); + j.put("type", "private"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + + // خواندن بولینِ online با سازگاری DB + Object onlineObj = rs.getObject("online"); + boolean online = (onlineObj instanceof Boolean) ? (Boolean) onlineObj + : rs.getInt("online") == 1; + j.put("online", online); + j.put("status", rs.getString("status")); // رشته‌ی وضعیت هم می‌آید + + Timestamp ts = rs.getTimestamp("last_seen"); + j.put("last_seen", ts == null ? JSONObject.NULL : ts.toInstant().toString()); + return j; + } + } + } + + + /** هدر گروه: نام، عکس و تعداد اعضا */ + private static JSONObject getGroupHeader(Connection conn, UUID groupInternalId) throws SQLException { + String sql = """ + SELECT + g.group_name AS name, + COALESCE(g.image_url, '') AS image_url, + (SELECT COUNT(*) FROM group_members gm WHERE gm.group_id = g.internal_uuid) AS member_count + FROM groups g + WHERE g.internal_uuid = ? + LIMIT 1 + """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, groupInternalId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Group not found."); + JSONObject j = new JSONObject(); + j.put("type", "group"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + j.put("member_count", rs.getInt("member_count")); + return j; + } + } + } + + /** هدر کانال: نام، عکس و تعداد سابسکرایبر */ + private static JSONObject getChannelHeader(Connection conn, UUID channelInternalId) throws SQLException { + String sql = """ + SELECT + c.channel_name AS name, + COALESCE(c.image_url, '') AS image_url, + (SELECT COUNT(*) FROM channel_subscribers cs WHERE cs.channel_id = c.internal_uuid) AS member_count + FROM channels c + WHERE c.internal_uuid = ? + LIMIT 1 + """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, channelInternalId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) throw new SQLException("Channel not found."); + JSONObject j = new JSONObject(); + j.put("type", "channel"); + j.put("name", rs.getString("name")); + j.put("image_url", rs.getString("image_url")); + j.put("member_count", rs.getInt("member_count")); + return j; + } + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index f248058..af0033d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -448,22 +448,19 @@ public class MessageDatabase { public static List getUnreadMessages(UUID userId) { - return getUnreadMessages(userId, 200); // یک سقف معقول + 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 r.message_id = m.message_id - AND r.user_id = ? + 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 = ? + 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 @@ -474,14 +471,14 @@ public class MessageDatabase { 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 ( + )) + 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 ( + OR (m.receiver_type = 'channel' AND EXISTS ( SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id @@ -499,16 +496,14 @@ public class MessageDatabase { 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); // pc.user1_id + ps.setObject(i++, userId); // pc.user2_id ps.setObject(i++, userId); // gm.user_id ps.setObject(i++, userId); // cs.user_id - ps.setInt(i++, Math.max(1, limit)); + ps.setInt(i, Math.max(1, limit)); try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - messages.add(mapMessage(rs)); // همان mapMessage که گفتیم - } + while (rs.next()) messages.add(mapMessage(rs)); } } catch (SQLException e) { e.printStackTrace(); @@ -517,6 +512,7 @@ public class MessageDatabase { } + private static java.util.UUID readUUID(ResultSet rs, String col) throws SQLException { Object o = rs.getObject(col); if (o == null) 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 f2f56a9..cccdc4c 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -2808,6 +2808,37 @@ public class ClientHandler implements Runnable { break; } + + + case "get_header_info": { + try { + String type = requestJson.getString("receiver_type"); // "private" | "group" | "channel" + UUID receiverId = UUID.fromString(requestJson.getString("receiver_id")); + + UUID viewerId = null; + if ("private".equalsIgnoreCase(type)) { + String v = requestJson.optString("viewer_id", + requestJson.optString("my_id", null)); + if (v == null) { + response = new ResponseModel("error", "viewer_id (or my_id) is required for private chats."); + break; + } + viewerId = UUID.fromString(v); + } + + org.json.JSONObject data = org.to.telegramfinalproject.Database.ChatInfoDatabase + .getHeaderInfo(type, receiverId, viewerId); + + response = new ResponseModel("success", "Header info fetched.", data); + + } catch (Exception e) { + e.printStackTrace(); + response = new ResponseModel("error", "Failed to fetch header info."); + } + 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 5b92471..750c776 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -23,6 +23,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.UUID; @@ -536,7 +537,7 @@ public class ChatPageController { chatTitle.setText(entry.getName()); - // Default avatar + // آواتار پیش‌فرض بر اساس نوع if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { userAvatar.setImage(new Image(entry.getImageUrl())); } else if ("channel".equals(entry.getType())) { @@ -557,43 +558,9 @@ public class ChatPageController { } userAvatar.setClip(new Circle(20, 20, 20)); - // === Private chat? Fetch other user status === - if ("private".equalsIgnoreCase(entry.getType()) && entry.getOtherUserId() != null) { - UUID otherId = entry.getOtherUserId(); + fetchAndRenderHeader(entry); - new Thread(() -> { - JSONObject req = new JSONObject(); - req.put("action", "get_other_user_status"); - req.put("user_id", otherId.toString()); - - JSONObject resp = ActionHandler.sendWithResponse(req); - if (resp != null && "success".equals(resp.optString("status"))) { - JSONObject data = resp.optJSONObject("data"); - boolean online = data.optBoolean("online", false); - String lastSeenIso = data.optString("last_seen", null); - - String statusText; - if (online) { - statusText = "online"; - } else { - // fallback if no timestamp - statusText = (lastSeenIso == null || lastSeenIso.isEmpty()) - ? "last seen recently" - : "last seen " + formatWhen(parseWhen(lastSeenIso)); - } - - Platform.runLater(() -> chatStatus.setText(statusText)); - } - }).start(); - } else if ("group".equalsIgnoreCase(entry.getType())) { - chatStatus.setText("Group"); - } else if ("channel".equalsIgnoreCase(entry.getType())) { - chatStatus.setText("Channel"); - } else { - chatStatus.setText(""); - } - - // Load chat messages + // پیام‌ها messageContainer.getChildren().clear(); loadMessages(entry); markAsRead(entry); @@ -601,6 +568,7 @@ public class ChatPageController { Platform.runLater(() -> messageInput.requestFocus()); } + private void loadMessages(ChatEntry entry) { JSONObject req = new JSONObject(); req.put("action", "get_messages_UI"); @@ -993,4 +961,140 @@ public class ChatPageController { e.printStackTrace(); } } + + + private void fetchAndRenderHeader(ChatEntry entry) { + JSONObject req = new JSONObject(); + req.put("action", "get_header_info"); + req.put("receiver_id", entry.getId().toString()); + req.put("receiver_type", entry.getType()); // باید "private" باشه + + // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) + UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری + if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { + req.put("viewer_id", viewerId.toString()); + } + + new Thread(() -> { + JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { ex.printStackTrace(); return; } + + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + System.err.println("get_header_info failed: " + (resp != null ? resp.optString("message") : "null resp")); + return; + } + JSONObject data = resp.optJSONObject("data"); + if (data == null) return; + + Platform.runLater(() -> renderHeaderFromData(entry, data)); + }).start(); + } + + private void renderHeaderFromData(ChatEntry entry, JSONObject data) { + String t = entry.getType() == null ? "" : entry.getType().toLowerCase(); + switch (t) { + case "private" -> updatePrivateHeader(entry, data); + case "group" -> updateGroupHeader(entry, data); + case "channel" -> updateChannelHeader(entry, data); + default -> chatStatus.setText(""); + } + } + + private void updatePrivateHeader(ChatEntry entry, JSONObject data) { + String name = nz(data.optString("profile_name", entry.getName())); + chatTitle.setText(name); + + // other_user_id برای ریل‌تایم status + String other = data.optString("other_user_id", ""); + if (!other.isBlank()) { + try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} + } + + // تصویر + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + userAvatar.setImage(new Image(img, true)); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + chatStatus.setText(userStatusText( + data.optBoolean("online", false), + data.optString("last_seen", null) + )); + + } + + private void updateGroupHeader(ChatEntry entry, JSONObject data) { + chatTitle.setText(nz(data.optString("group_name", entry.getName()))); + + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + userAvatar.setImage(new Image(img, true)); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + + int members = data.optInt("member_count", 0); + int online = data.optInt("online_count", -1); + chatStatus.setText(online >= 0 ? (members + " members, " + online + " online") + : (members + " members")); + } + + private void updateChannelHeader(ChatEntry entry, JSONObject data) { + chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); + + String img = data.optString("image_url", ""); + if (hasVal(img)) { + try { + userAvatar.setImage(new Image(img, true)); + userAvatar.setClip(new Circle(20, 20, 20)); + } catch (Exception ignore) {} + } + + int subs = data.optInt("member_count", 0); + chatStatus.setText(subs + " subscribers"); + } + + public void onUserStatusChanged(String userUuid, String status, String lastSeenIso) { + if (currentChat == null || !"private".equalsIgnoreCase(currentChat.getType())) return; + var other = currentChat.getOtherUserId(); + if (other == null || !other.toString().equalsIgnoreCase(userUuid)) return; + + if ("online".equalsIgnoreCase(status)) { + chatStatus.setText("online"); + } else { + if (hasVal(lastSeenIso)) { + var ts = parseWhen(lastSeenIso); + chatStatus.setText(ts != null ? ("last seen " + formatWhen(ts)) : "last seen recently"); + } else { + chatStatus.setText("last seen recently"); + } + } + } + + + + private String userStatusText(boolean online, String lastSeenIso) { + if (online) return "online"; + + LocalDateTime ts = parseWhen(lastSeenIso); + if (ts == null) return "Last seen recently"; + + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + return FMT_HHMM.format(ts); + } + + long days = ChronoUnit.DAYS.between(d, today); + if (days > 30) { + return "Last seen long time ago"; + } + return "recently"; + } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 2da29ff..95064a4 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -963,4 +963,11 @@ public class MainController { return false; } + + + + + + + } \ No newline at end of file