fix chat header

This commit is contained in:
2025-08-31 17:00:35 +03:30
parent bdd9d03128
commit 626a1335e6
6 changed files with 336 additions and 54 deletions
@@ -172,11 +172,25 @@ public class IncomingMessageListener implements Runnable {
bumpChatListFromUpdate(data); 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); displayRealTimeMessage(action, msg);
} }
@@ -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;
}
}
}
}
@@ -448,22 +448,19 @@ public class MessageDatabase {
public static List<Message> getUnreadMessages(UUID userId) { public static List<Message> getUnreadMessages(UUID userId) {
return getUnreadMessages(userId, 200); // یک سقف معقول return getUnreadMessages(userId, 200);
} }
public static List<Message> getUnreadMessages(UUID userId, int limit) { public static List<Message> getUnreadMessages(UUID userId, int limit) {
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
// توجه: نام جدول private_chats/gm/cs را با اسامی واقعی دیتابیس‌ت هماهنگ کن
String sql = """ String sql = """
SELECT m.* SELECT m.*
FROM messages m FROM messages m
LEFT JOIN message_receipts r LEFT JOIN message_receipts r
ON r.message_id = m.message_id ON r.message_id = m.message_id AND r.user_id = ?
AND r.user_id = ?
LEFT JOIN deleted_messages d LEFT JOIN deleted_messages d
ON d.message_id = m.message_id ON d.message_id = m.message_id AND d.user_id = ?
AND d.user_id = ?
WHERE r.message_id IS NULL WHERE r.message_id IS NULL
AND d.message_id IS NULL AND d.message_id IS NULL
AND m.is_deleted_globally = FALSE AND m.is_deleted_globally = FALSE
@@ -474,14 +471,14 @@ public class MessageDatabase {
FROM private_chat pc FROM private_chat pc
WHERE pc.chat_id = m.receiver_id WHERE pc.chat_id = m.receiver_id
AND (pc.user1_id = ? OR pc.user2_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 SELECT 1
FROM group_members gm FROM group_members gm
WHERE gm.group_id = m.receiver_id WHERE gm.group_id = m.receiver_id
AND gm.user_id = ? AND gm.user_id = ?
)) ))
OR (m.receiver_type = 'channel' AND EXISTS ( OR (m.receiver_type = 'channel' AND EXISTS (
SELECT 1 SELECT 1
FROM channel_subscribers cs FROM channel_subscribers cs
WHERE cs.channel_id = m.receiver_id 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); // r.user_id
ps.setObject(i++, userId); // d.user_id ps.setObject(i++, userId); // d.user_id
ps.setObject(i++, userId); // m.sender_id <> ? ps.setObject(i++, userId); // m.sender_id <> ?
ps.setObject(i++, userId); // pc.user1_id=userId ps.setObject(i++, userId); // pc.user1_id
ps.setObject(i++, userId); // pc.user2_id=userId ps.setObject(i++, userId); // pc.user2_id
ps.setObject(i++, userId); // gm.user_id ps.setObject(i++, userId); // gm.user_id
ps.setObject(i++, userId); // cs.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()) { try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { while (rs.next()) messages.add(mapMessage(rs));
messages.add(mapMessage(rs)); // همان mapMessage که گفتیم
}
} }
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
@@ -517,6 +512,7 @@ public class MessageDatabase {
} }
private static java.util.UUID readUUID(ResultSet rs, String col) throws SQLException { private static java.util.UUID readUUID(ResultSet rs, String col) throws SQLException {
Object o = rs.getObject(col); Object o = rs.getObject(col);
if (o == null) return null; if (o == null) return null;
@@ -2808,6 +2808,37 @@ public class ClientHandler implements Runnable {
break; 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: default:
response = new ResponseModel("error", "Unknown action: " + action); response = new ResponseModel("error", "Unknown action: " + action);
} }
@@ -23,6 +23,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -536,7 +537,7 @@ public class ChatPageController {
chatTitle.setText(entry.getName()); chatTitle.setText(entry.getName());
// Default avatar // آواتار پیش‌فرض بر اساس نوع
if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) {
userAvatar.setImage(new Image(entry.getImageUrl())); userAvatar.setImage(new Image(entry.getImageUrl()));
} else if ("channel".equals(entry.getType())) { } else if ("channel".equals(entry.getType())) {
@@ -557,43 +558,9 @@ public class ChatPageController {
} }
userAvatar.setClip(new Circle(20, 20, 20)); userAvatar.setClip(new Circle(20, 20, 20));
// === Private chat? Fetch other user status === fetchAndRenderHeader(entry);
if ("private".equalsIgnoreCase(entry.getType()) && entry.getOtherUserId() != null) {
UUID otherId = entry.getOtherUserId();
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(); messageContainer.getChildren().clear();
loadMessages(entry); loadMessages(entry);
markAsRead(entry); markAsRead(entry);
@@ -601,6 +568,7 @@ public class ChatPageController {
Platform.runLater(() -> messageInput.requestFocus()); Platform.runLater(() -> messageInput.requestFocus());
} }
private void loadMessages(ChatEntry entry) { private void loadMessages(ChatEntry entry) {
JSONObject req = new JSONObject(); JSONObject req = new JSONObject();
req.put("action", "get_messages_UI"); req.put("action", "get_messages_UI");
@@ -993,4 +961,140 @@ public class ChatPageController {
e.printStackTrace(); 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";
}
} }
@@ -963,4 +963,11 @@ public class MainController {
return false; return false;
} }
} }