Work on send picture in chat

This commit is contained in:
2025-09-06 10:42:51 +03:30
parent f79eeb2086
commit 30e9929b72
5 changed files with 556 additions and 67 deletions
@@ -64,6 +64,10 @@ public class ActionHandler {
} }
public static ActionHandler getInstance() {
return instance;
}
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
this.out = out; this.out = out;
this.in = in; this.in = in;
@@ -4301,7 +4305,7 @@ public class ActionHandler {
JSONObject header = new JSONObject() JSONObject header = new JSONObject()
.put("message_id", messageId.toString()) .put("message_id", messageId.toString())
.put("sender_id", TelegramClient.loggedInUserId.toString()) .put("sender_id", Session.getUserUUID())
.put("receiver_type", receiverType) // private|group|channel .put("receiver_type", receiverType) // private|group|channel
.put("receiver_id", receiverId.toString()) .put("receiver_id", receiverId.toString())
.put("message_type", type.toUpperCase()) // IMAGE | AUDIO .put("message_type", type.toUpperCase()) // IMAGE | AUDIO
@@ -1537,6 +1537,35 @@ public class MessageDatabase {
public static String getFirstAttachmentUrlByType(UUID messageId, String messageType) {
final String sql = """
SELECT file_url
FROM message_attachments
WHERE message_id = ?
AND (file_type = ? OR ? IS NULL)
ORDER BY attachment_id
LIMIT 1
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
// اگر messageType خالی بود، با NULL بفرست که شرط OR ? IS NULL برقرار شه
String mt = (messageType == null || messageType.isBlank()) ? null : messageType.toUpperCase();
ps.setString(2, mt);
if (mt == null) ps.setNull(3, Types.VARCHAR); else ps.setString(3, mt);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("file_url") : null;
}
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
@@ -1605,6 +1605,14 @@ public class ClientHandler implements Runnable {
obj.put("forwarded_by", JSONObject.NULL); obj.put("forwarded_by", JSONObject.NULL);
} }
String mt = m.getMessage_type();
if (mt != null && !mt.equalsIgnoreCase("TEXT")) {
String fileUrl = MessageDatabase.getFirstAttachmentUrlByType(m.getMessage_id(), mt);
if (fileUrl != null && !fileUrl.isBlank()) {
obj.put("file_url", fileUrl);
}
}
JSONArray reactionsArr = new JSONArray(); JSONArray reactionsArr = new JSONArray();
try { try {
List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id()); List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id());
@@ -7,10 +7,15 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
@@ -117,6 +122,14 @@ public class ChatPageController {
private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده
private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده
private final Map<String, Node> messageNodes = new HashMap<>(); private final Map<String, Node> messageNodes = new HashMap<>();
private final Deque<HBox> pendingBubbles = new ArrayDeque<>();
// جایی عمومی (مثلا بالای کلاس)
private static final String UPLOADS_DIR = "C:/Users/User/Desktop/Project/uploads"; // با مسیر خودت یکی کن
private static final String HTTP_BASE = "http://localhost:8080"; // اگر بعدا HTTP رو درست کردی
private JSONObject lastHeaderData = null; private JSONObject lastHeaderData = null;
@@ -668,7 +681,10 @@ public class ChatPageController {
fMessageId, // message_id fMessageId, // message_id
"", "", "", // forwarded_from, forwarded_by, reply_to_id "", "", "", // forwarded_from, forwarded_by, reply_to_id
false, // edited false, // edited
null // reactions null, // reactions
null,
null
); );
// آپدیت پیش‌نمایش لیست چت‌ها // آپدیت پیش‌نمایش لیست چت‌ها
@@ -694,12 +710,122 @@ public class ChatPageController {
private void openFileChooser() { private void openFileChooser() {
FileChooser fc = new FileChooser(); FileChooser fc = new FileChooser();
fc.setTitle("Select a file to send"); fc.setTitle("Select image or audio");
// فقط عکس/صدا
fc.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp"),
new FileChooser.ExtensionFilter("Audio", "*.mp3", "*.wav", "*.m4a", "*.ogg", "*.aac")
);
File file = fc.showOpenDialog(attachmentButton.getScene().getWindow()); File file = fc.showOpenDialog(attachmentButton.getScene().getWindow());
if (file != null) { if (file == null) return;
System.out.println("Selected file: " + file.getAbsolutePath());
// TODO: actually send file // نوع (IMAGE/AUDIO) را از اسم/محتوا حدس بزنیم
addSystemMessage("Attached file: " + file.getName()); String type = guessType(file);
if (type == null) {
toast("Only Image and Aduio");
return;
}
currentChatId = String.valueOf(currentChat.getId());
Session.currentChatType = currentChat.getType();
// مقصد چت از Session
if (Session.currentChatId == null || Session.currentChatType == null) {
toast("Not available chat");
return;
}
UUID receiverId = UUID.fromString(Session.currentChatId);
String receiverType = Session.currentChatType; // "private" | "group" | "channel"
String caption = (messageInput != null) ? messageInput.getText().trim() : "";
if (messageInput != null) messageInput.clear();
// 1) حباب Pending فوری در UI
HBox pending = addPendingMediaBubble(file, type, caption);
// 2) ارسال واقعی در بک‌گراند با ActionHandler
new Thread(() -> {
ActionHandler ah = ActionHandler.getInstance();
ah.sendMediaMessage(receiverId, receiverType, type, file, caption);
// نکته: پیام واقعی بعد از ذخیره روی سرور، از طریق Listener برمی‌گرده.
// وقتی رسید، در Listener این متد را صدا بزنید تا یک حباب Pending حذف شود:
// Platform.runLater(() -> removeOnePendingBubble());
}, "Media-Uploader").start();
}
/** حدس نوع فایل: IMAGE یا AUDIO */
private String guessType(File f) {
String name = f.getName().toLowerCase();
if (name.matches(".*\\.(png|jpg|jpeg|gif|bmp|webp)$")) return "IMAGE";
if (name.matches(".*\\.(mp3|wav|m4a|ogg|aac)$")) return "AUDIO";
// اگر خواستی دقیق‌تر: با Files.probeContentType هم تست کن
return null;
}
/** ساخت یک حباب «درحال ارسال…» */
private HBox addPendingMediaBubble(File file, String type, String caption) {
HBox root = new HBox(8);
root.getStyleClass().add("bubble-outgoing"); // استایل دلخواهت
root.setFillHeight(true);
ImageView iv = null;
if ("IMAGE".equalsIgnoreCase(type)) {
iv = new ImageView(new Image(file.toURI().toString(), 360, 360, true, true, true));
iv.setPreserveRatio(true);
iv.setFitWidth(240); // سایز معقول برای Pending
iv.setFitHeight(240);
root.getChildren().add(iv);
} else if ("AUDIO".equalsIgnoreCase(type)) {
// برای صدا یک آیکون ساده و نام فایل
ImageView icon = new ImageView(); // اگر آیکون داری اینجا بگذار
icon.setFitWidth(24); icon.setFitHeight(24);
Label name = new Label(file.getName());
HBox audioBox = new HBox(6, icon, name);
root.getChildren().add(audioBox);
}
VBox right = new VBox(4);
if (caption != null && !caption.isBlank()) {
Label cap = new Label(caption);
cap.getStyleClass().add("msg-caption");
cap.setWrapText(true);
right.getChildren().add(cap);
}
HBox statusRow = new HBox(6);
ProgressIndicator spinner = new ProgressIndicator();
spinner.setPrefSize(16, 16);
Label status = new Label("Sending...");
status.getStyleClass().add("msg-status");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
statusRow.getChildren().addAll(spinner, status, spacer);
right.getChildren().add(statusRow);
root.getChildren().add(right);
messageContainer.getChildren().add(root);
pendingBubbles.addLast(root);
return root;
}
/** وقتی پیام واقعی (از خودِ کاربر) برای همین چت رسید، یکی از Pendingها را حذف کن. */
public void removeOnePendingBubble() {
HBox node = pendingBubbles.pollFirst();
if (node != null) {
messageContainer.getChildren().remove(node);
}
}
private void toast(String msg) {
// هر جور که خودت نوتیف/Toast داری
System.out.println("" + msg);
if (messageInput != null) {
messageInput.setTooltip(new Tooltip(msg));
} }
} }
@@ -1300,6 +1426,58 @@ public class ChatPageController {
private final java.util.Map<String, org.json.JSONObject> msgIndex = new java.util.HashMap<>(); private final java.util.Map<String, org.json.JSONObject> msgIndex = new java.util.HashMap<>();
// private void renderMessages(org.json.JSONArray list) {
// messageContainer.getChildren().clear();
//
// // برای reply-preview: ایندکس کردن پیام‌ها با message_id
// msgIndex.clear();
// for (int i = 0; i < list.length(); i++) {
// org.json.JSONObject m = list.getJSONObject(i);
// String mid = str(m, "message_id");
// if (!mid.isEmpty()) msgIndex.put(mid, m);
// }
//
// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
// ? Session.currentUser.getString("internal_uuid") : "";
//
// for (int i = 0; i < list.length(); i++) {
// org.json.JSONObject m = list.getJSONObject(i);
//
// String senderId = str(m, "sender_id");
// String senderName = str(m, "sender_name");
// String type = str(m, "message_type");
// String content = str(m, "content");
// String whenStr = str(m, "send_at");
// String msgId = str(m, "message_id");
//
// // فوروارد / ریپلای / ادیت / ری‌اکشن
// String fwdFrom = nz(str(m, "forwarded_from"));
// String fwdBy = nz(str(m, "forwarded_by"));
// String replyToId = nz(str(m, "reply_to_id"));
// boolean edited = bool(m, "is_edited");
// org.json.JSONArray reactions = arr(m, "reactions");
//
// boolean outgoing = senderId.equalsIgnoreCase(myId);
// if (senderName == null || senderName.isBlank()) {
// senderName = outgoing ? "You"
// : (senderId == null || senderId.isBlank()
// ? "Unknown"
// : senderId.substring(0, Math.min(8, senderId.length())));
// }
//
// java.time.LocalDateTime ts = parseWhen(whenStr);
//
// // نمایش
// addBubble(outgoing, senderName, type, content, ts, msgId,
// fwdFrom, fwdBy, replyToId, edited, reactions);
// }
//
// // کمی فاصله بین پیام‌ها
// messageContainer.setSpacing(8);
// messageScrollPane.layout();
// messageScrollPane.setVvalue(1.0);
// }
private void renderMessages(org.json.JSONArray list) { private void renderMessages(org.json.JSONArray list) {
messageContainer.getChildren().clear(); messageContainer.getChildren().clear();
@@ -1320,10 +1498,14 @@ public class ChatPageController {
String senderId = str(m, "sender_id"); String senderId = str(m, "sender_id");
String senderName = str(m, "sender_name"); String senderName = str(m, "sender_name");
String type = str(m, "message_type"); String type = str(m, "message_type");
String content = str(m, "content"); String content = str(m, "content"); // این همون کپشنه برای IMAGE
String whenStr = str(m, "send_at"); String whenStr = str(m, "send_at");
String msgId = str(m, "message_id"); String msgId = str(m, "message_id");
// 👇 جدید: URL ها
String fileUrl = str(m, "file_url");
String thumbUrl = str(m, "thumb_url");
// فوروارد / ریپلای / ادیت / ری‌اکشن // فوروارد / ریپلای / ادیت / ری‌اکشن
String fwdFrom = nz(str(m, "forwarded_from")); String fwdFrom = nz(str(m, "forwarded_from"));
String fwdBy = nz(str(m, "forwarded_by")); String fwdBy = nz(str(m, "forwarded_by"));
@@ -1341,17 +1523,17 @@ public class ChatPageController {
java.time.LocalDateTime ts = parseWhen(whenStr); java.time.LocalDateTime ts = parseWhen(whenStr);
// نمایش // 👇 امضای جدید addBubble با fileUrl/thumbUrl
addBubble(outgoing, senderName, type, content, ts, msgId, addBubble(outgoing, senderName, type, content, ts, msgId,
fwdFrom, fwdBy, replyToId, edited, reactions); fwdFrom, fwdBy, replyToId, edited, reactions, fileUrl, thumbUrl);
} }
// کمی فاصله بین پیام‌ها
messageContainer.setSpacing(8); messageContainer.setSpacing(8);
messageScrollPane.layout(); messageScrollPane.layout();
messageScrollPane.setVvalue(1.0); messageScrollPane.setVvalue(1.0);
} }
private String shortId(String id){ private String shortId(String id){
return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length()));
} }
@@ -1490,104 +1672,216 @@ public class ChatPageController {
// private void addBubble(
// boolean outgoing,
// String displayName,
// String type,
// String content,
// java.time.LocalDateTime sentAt,
// String messageId,
// String forwardedFrom,
// String forwardedBy,
// String replyToId,
// boolean edited,
// org.json.JSONArray reactions
// ) {
// // === Meta (نام + زمان) ===
// String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt);
// if (edited) metaText += " (edited)";
// Label meta = new Label(metaText);
// meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
// meta.setWrapText(true);
// // برچسب برای آپدیت‌های بعدی (edit)
// meta.getProperties().put("role", "metaLabel");
//
// // === متن/نوع پیام ===
// String t = type == null ? "" : type.trim().toUpperCase();
// boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
// String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
//
// Label msg = new Label(bodyText);
// msg.setWrapText(true);
// msg.setMinHeight(Region.USE_PREF_SIZE);
// // برچسب برای آپدیت‌های بعدی (edit)
// msg.getProperties().put("role", "msgLabel");
//
// // === رنگ بابل‌ها
// boolean dark = themeManager.isDarkMode();
// String mine = dark ? "#2b7cff" : "#d8ecff"; // outgoing (من)
// String theirs = dark ? "#2c333a" : "#f2f4f7"; // incoming (خیلی روشن به‌جای سفید)
// String bg = outgoing ? mine : theirs;
//
// msg.setStyle(
// "-fx-background-color:" + bg + ";" +
// "-fx-padding:8 12;" +
// "-fx-background-radius:12;" +
// "-fx-max-width: 520;"
// );
//
// // === بدنه‌ی بابل ===
// VBox bubble = new VBox(4);
// bubble.getChildren().add(meta);
//
// // برچسب‌گذاری بابل برای پیدا کردنش در آپدیت‌های realtime
// if (messageId != null && !messageId.isBlank()) {
// bubble.getProperties().put("messageId", messageId);
// }
//
// // Forward header (اختیاری)
// if (hasVal(forwardedFrom) || hasVal(forwardedBy)) {
// bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy));
// }
//
// // Reply preview (اختیاری)
// if (hasVal(replyToId)) {
// bubble.getChildren().add(buildReplyBoxFromIndex(replyToId));
// }
//
// // متن اصلی
// bubble.getChildren().add(msg);
//
// // Reactions (اختیاری) + برچسب برای تعویض سریع در ریِل‌تایم
// if (reactions != null && reactions.length() > 0) {
// Node rxBar = buildReactionsBarFromJson(reactions, dark);
// rxBar.getProperties().put("role", "reactionsBar");
// bubble.getChildren().add(rxBar);
// }
//
// // === ردیف چیدمان راست/چپ ===
// HBox row = new HBox(bubble);
// row.setFillHeight(true);
// row.setSpacing(4);
// row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);
// row.setPadding(new Insets(2, 6, 2, 6));
//
// // اضافه به کانتینر
// messageContainer.getChildren().add(row);
//
// // ایندکس نود برای آپدیت/حذف realtime
// if (messageId != null && !messageId.isBlank()) {
// messageNodes.put(messageId, row);
// }
//
// boolean isMine = outgoing;
//
// // منوی راست‌کلیک/کلیک (بدون تغییر در ساختار کدت)
// ContextMenu menu = buildMessageMenu(isMine, messageId, type, content);
// row.setOnContextMenuRequested(ev -> {
// menu.show(row, ev.getScreenX(), ev.getScreenY());
// ev.consume();
// });
// row.setOnMouseClicked(ev -> {
// if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) {
// menu.show(row, ev.getScreenX(), ev.getScreenY());
// }
// });
// }
private void addBubble( private void addBubble(
boolean outgoing, boolean outgoing,
String displayName, String displayName,
String type, String type,
String content, String content, // برای IMAGE = کپشن
java.time.LocalDateTime sentAt, java.time.LocalDateTime sentAt,
String messageId, String messageId,
String forwardedFrom, String forwardedFrom,
String forwardedBy, String forwardedBy,
String replyToId, String replyToId,
boolean edited, boolean edited,
org.json.JSONArray reactions org.json.JSONArray reactions,
String fileUrl, // 👈 جدید
String thumbUrl // 👈 جدید
) { ) {
// === Meta (نام + زمان) ===
String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt); String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt);
if (edited) metaText += " (edited)"; if (edited) metaText += " (edited)";
Label meta = new Label(metaText); Label meta = new Label(metaText);
meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
meta.setWrapText(true); meta.setWrapText(true);
// برچسب برای آپدیت‌های بعدی (edit)
meta.getProperties().put("role", "metaLabel"); meta.getProperties().put("role", "metaLabel");
// === متن/نوع پیام ===
String t = type == null ? "" : type.trim().toUpperCase(); String t = type == null ? "" : type.trim().toUpperCase();
boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
Label msg = new Label(bodyText); // رنگ پس‌زمینه برای متن (برای عکس پس‌زمینه نمی‌ذاریم تا تمیز باشه)
msg.setWrapText(true);
msg.setMinHeight(Region.USE_PREF_SIZE);
// برچسب برای آپدیت‌های بعدی (edit)
msg.getProperties().put("role", "msgLabel");
// === رنگ بابل‌ها
boolean dark = themeManager.isDarkMode(); boolean dark = themeManager.isDarkMode();
String mine = dark ? "#2b7cff" : "#d8ecff"; // outgoing (من) String mine = dark ? "#2b7cff" : "#d8ecff";
String theirs = dark ? "#2c333a" : "#f2f4f7"; // incoming (خیلی روشن به‌جای سفید) String theirs = dark ? "#2c333a" : "#f2f4f7";
String bg = outgoing ? mine : theirs;
msg.setStyle(
"-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;"
);
// === بدنه‌ی بابل ===
VBox bubble = new VBox(4); VBox bubble = new VBox(4);
bubble.getChildren().add(meta); bubble.getChildren().add(meta);
// برچسب‌گذاری بابل برای پیدا کردنش در آپدیت‌های realtime
if (messageId != null && !messageId.isBlank()) { if (messageId != null && !messageId.isBlank()) {
bubble.getProperties().put("messageId", messageId); bubble.getProperties().put("messageId", messageId);
} }
// Forward header (اختیاری)
if (hasVal(forwardedFrom) || hasVal(forwardedBy)) { if (hasVal(forwardedFrom) || hasVal(forwardedBy)) {
bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy));
} }
// Reply preview (اختیاری)
if (hasVal(replyToId)) { if (hasVal(replyToId)) {
bubble.getChildren().add(buildReplyBoxFromIndex(replyToId)); bubble.getChildren().add(buildReplyBoxFromIndex(replyToId));
} }
// متن اصلی if (isText) {
bubble.getChildren().add(msg); // متن
String bodyText = content == null ? "" : content;
Label msg = new Label(bodyText);
msg.setWrapText(true);
msg.setMinHeight(Region.USE_PREF_SIZE);
msg.getProperties().put("role", "msgLabel");
String bg = outgoing ? mine : theirs;
msg.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(msg);
} else if ("IMAGE".equals(t)) {
// 👇 نمایش تصویر از روی URL سرور + کپشن اختیاری
Node imageNode = buildImageNode(fileUrl, thumbUrl, content);
bubble.getChildren().add(imageNode);
} else if ("AUDIO".equals(t)) {
// می‌تونی بعداً کاملش کنی
Label ph = new Label("🎵 Audio");
ph.setWrapText(true);
String bg = outgoing ? mine : theirs;
ph.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(ph);
} else {
// ناشناخته
Label ph = new Label("[" + t + "]");
ph.setWrapText(true);
String bg = outgoing ? mine : theirs;
ph.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(ph);
}
// Reactions (اختیاری) + برچسب برای تعویض سریع در ریِل‌تایم
if (reactions != null && reactions.length() > 0) { if (reactions != null && reactions.length() > 0) {
Node rxBar = buildReactionsBarFromJson(reactions, dark); Node rxBar = buildReactionsBarFromJson(reactions, dark);
rxBar.getProperties().put("role", "reactionsBar"); rxBar.getProperties().put("role", "reactionsBar");
bubble.getChildren().add(rxBar); bubble.getChildren().add(rxBar);
} }
// === ردیف چیدمان راست/چپ ===
HBox row = new HBox(bubble); HBox row = new HBox(bubble);
row.setFillHeight(true); row.setFillHeight(true);
row.setSpacing(4); row.setSpacing(4);
row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT); row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);
row.setPadding(new Insets(2, 6, 2, 6)); row.setPadding(new Insets(2, 6, 2, 6));
// اضافه به کانتینر
messageContainer.getChildren().add(row); messageContainer.getChildren().add(row);
// ایندکس نود برای آپدیت/حذف realtime
if (messageId != null && !messageId.isBlank()) { if (messageId != null && !messageId.isBlank()) {
messageNodes.put(messageId, row); messageNodes.put(messageId, row);
} }
boolean isMine = outgoing; boolean isMine = outgoing;
// منوی راست‌کلیک/کلیک (بدون تغییر در ساختار کدت)
ContextMenu menu = buildMessageMenu(isMine, messageId, type, content); ContextMenu menu = buildMessageMenu(isMine, messageId, type, content);
row.setOnContextMenuRequested(ev -> { row.setOnContextMenuRequested(ev -> { menu.show(row, ev.getScreenX(), ev.getScreenY()); ev.consume(); });
menu.show(row, ev.getScreenX(), ev.getScreenY());
ev.consume();
});
row.setOnMouseClicked(ev -> { row.setOnMouseClicked(ev -> {
if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) { if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) {
menu.show(row, ev.getScreenX(), ev.getScreenY()); menu.show(row, ev.getScreenX(), ev.getScreenY());
@@ -1595,12 +1889,96 @@ public class ChatPageController {
}); });
} }
private Node buildImageNode(String fileUrl, String thumbUrl, String caption) {
String url = hasVal(fileUrl) ? absolute(fileUrl) : null;
if (!hasVal(url)) return new Label("❌ Image not available");
ImageView iv = new ImageView();
iv.setPreserveRatio(true);
iv.setSmooth(true);
iv.setFitWidth(360);
iv.setFitHeight(360);
Image img = new Image(url, 360, 360, true, true, true);
iv.setImage(img);
img.errorProperty().addListener((obs, wasErr, isErr) -> {
if (isErr) {
System.err.println("⚠️ Image load failed: " + url + " | ex=" + img.getException());
Label err = new Label("❌ Failed to load image");
VBox fallback = new VBox(4, err);
Platform.runLater(() -> {
if (iv.getParent() instanceof VBox v) {
int idx = v.getChildren().indexOf(iv);
if (idx >= 0) v.getChildren().set(idx, fallback);
}
});
}
});
iv.setOnMouseClicked(e -> openImagePreviewDialog(url));
VBox box = new VBox(6, iv);
if (hasVal(caption)) {
Label cap = new Label(caption);
cap.setWrapText(true);
cap.setStyle("-fx-padding:6 8; -fx-background-radius:10; -fx-background-color: transparent; -fx-max-width: 520;");
box.getChildren().add(cap);
}
return box;
}
private void openImagePreviewDialog(String fullUrl) {
if (fullUrl == null || fullUrl.isBlank()) return;
// IMPORTANT: fullUrl همین حالا absolute است؛ دوباره absolute(...) نزن
String url = fullUrl;
ImageView iv = new ImageView(new Image(url, true));
iv.setPreserveRatio(true);
ScrollPane sp = new ScrollPane(iv);
sp.setPannable(true);
sp.setFitToWidth(true);
sp.setFitToHeight(true);
Stage st = new Stage();
st.setTitle("Preview");
st.initOwner(attachmentButton.getScene().getWindow());
st.setScene(new Scene(sp, 900, 700));
st.addEventHandler(KeyEvent.KEY_PRESSED, e -> { if (e.getCode() == KeyCode.ESCAPE) st.close(); });
st.show();
}
private String absolute(String pathOrUrl) {
if (pathOrUrl == null || pathOrUrl.isBlank()) return null;
if (pathOrUrl.startsWith("http")) return pathOrUrl;
// اگر نسبی است مثل /images/2025-09-06/xxx.jpg یا /audios/...
String rel = pathOrUrl.startsWith("/") ? pathOrUrl.substring(1) : pathOrUrl;
// حالت لوکال: file://
java.nio.file.Path p = java.nio.file.Paths.get(UPLOADS_DIR, rel.replace("/", java.io.File.separator));
java.net.URI uri = p.toUri(); // می‌شود file:///C:/Users/.../uploads/images/...
return uri.toString();
// اگر خواستی از HTTP بخوانی، به‌جای return بالا این را برگردان:
// return HTTP_BASE + (pathOrUrl.startsWith("/") ? pathOrUrl : "/" + pathOrUrl);
}
private ContextMenu buildMessageMenu(boolean isMine, String messageId, String type, String content) { private ContextMenu buildMessageMenu(boolean isMine, String messageId, String type, String content) {
ContextMenu menu = new ContextMenu(); ContextMenu menu = new ContextMenu();
// --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) --- // --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) ---
HBox reactions = new HBox(8); HBox reactions = new HBox(8);
String[] emojis = {"👍","👎","😂","😭","🕊️",""}; String[] emojis = {"👍","👎","😂","😭",""};
for (String e : emojis) { for (String e : emojis) {
Button b = new Button(e); Button b = new Button(e);
b.getStyleClass().add("reaction-btn"); b.getStyleClass().add("reaction-btn");
@@ -1788,6 +2166,64 @@ public class ChatPageController {
// } // }
// public void onRealTimeNewMessage(JSONObject m) {
// try {
// String chatIdStr = str(m,"receiver_id");
// String chatType = str(m,"receiver_type");
// if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
//
// UUID chatId = UUID.fromString(chatIdStr);
// boolean isCurrent = isSameChat(chatId, chatType);
//
// // id message_id fallback
// if (!m.has("message_id") && m.has("id")) {
// m.put("message_id", m.getString("id"));
// }
// String msgId = str(m,"message_id");
// if (!hasVal(msgId)) return;
//
// // اگر قبلاً همین پیام داخل UI اضافه شده، دیگه دوباره نساز
// if (messageNodes.containsKey(msgId)) return;
//
// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
// : (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown");
//
// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
// String content = str(m,"content");
// String whenIso = str(m,"send_at");
//
// String fwdFrom = str(m,"forwarded_from");
// String fwdBy = str(m,"forwarded_by");
// String replyTo = str(m,"reply_to_id");
// boolean edited = bool(m,"is_edited");
// JSONArray reacts = arr(m,"reactions");
//
// LocalDateTime ts = parseWhen(whenIso);
// if (ts == null) ts = LocalDateTime.now();
//
// // ایندکس برای ریپلای/ادیت/ری‌اکشن‌های بعدی
// msgIndex.put(msgId, m);
//
// // آپدیت لیست چت‌ها (پریویو)
// boolean incoming = true; // از سرور آمده ورودی
// updateChatListPreview(chatId, chatType, incoming, content, type);
//
// // اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد
// if (!isCurrent) return;
//
// // اضافه کردن حباب بدون رفرش
// addBubble(false, senderName, type, content, ts, msgId,
// fwdFrom, fwdBy, replyTo, edited, reacts);
//
// // خوانده شد (در صورت نیاز)
// if (currentChat != null) markAsRead(currentChat);
//
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
public void onRealTimeNewMessage(JSONObject m) { public void onRealTimeNewMessage(JSONObject m) {
try { try {
String chatIdStr = str(m,"receiver_id"); String chatIdStr = str(m,"receiver_id");
@@ -1804,16 +2240,21 @@ public class ChatPageController {
String msgId = str(m,"message_id"); String msgId = str(m,"message_id");
if (!hasVal(msgId)) return; if (!hasVal(msgId)) return;
// اگر قبلاً همین پیام داخل UI اضافه شده، دیگه دوباره نساز // اگر قبلاً تو UI هست، دوباره نساز
if (messageNodes.containsKey(msgId)) return; if (messageNodes.containsKey(msgId)) return;
String senderId = str(m,"sender_id");
String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
: (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown"); : (hasVal(senderId) ? shortId(senderId) : "Unknown");
String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT"; String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
String content = str(m,"content"); String content = str(m,"content"); // برای IMAGE = کپشن
String whenIso = str(m,"send_at"); String whenIso = str(m,"send_at");
// 👇 جدید: URL ها برای عکس/صدا
String fileUrl = str(m,"file_url");
String thumbUrl = str(m,"thumb_url");
String fwdFrom = str(m,"forwarded_from"); String fwdFrom = str(m,"forwarded_from");
String fwdBy = str(m,"forwarded_by"); String fwdBy = str(m,"forwarded_by");
String replyTo = str(m,"reply_to_id"); String replyTo = str(m,"reply_to_id");
@@ -1823,21 +2264,28 @@ public class ChatPageController {
LocalDateTime ts = parseWhen(whenIso); LocalDateTime ts = parseWhen(whenIso);
if (ts == null) ts = LocalDateTime.now(); if (ts == null) ts = LocalDateTime.now();
// ایندکس برای ریپلای/ادیت/ری‌اکشن‌های بعدی // اندیس پیام برای ریپلای/ادیت
msgIndex.put(msgId, m); msgIndex.put(msgId, m);
// آپدیت لیست چت‌ها (پریویو) // تشخیص خروجی/ورودی
boolean incoming = true; // از سرور آمده ورودی String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
updateChatListPreview(chatId, chatType, incoming, content, type); ? Session.currentUser.getString("internal_uuid") : "";
boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
// آپدیت لیست چت‌ها (پریویوِ کوتاه)
String previewText = switch (type.toUpperCase()) {
case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
case "AUDIO" -> "🎵 Audio";
default -> content;
};
updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
// اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد
if (!isCurrent) return; if (!isCurrent) return;
// اضافه کردن حباب بدون رفرش // 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl)
addBubble(false, senderName, type, content, ts, msgId, addBubble(outgoing, senderName, type, content, ts, msgId,
fwdFrom, fwdBy, replyTo, edited, reacts); fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl);
// خوانده شد (در صورت نیاز)
if (currentChat != null) markAsRead(currentChat); if (currentChat != null) markAsRead(currentChat);
} catch (Exception e) { } catch (Exception e) {
@@ -1845,6 +2293,7 @@ public class ChatPageController {
} }
} }
private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) { private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) {
var mc = MainController.getInstance(); var mc = MainController.getInstance();
if (mc == null) return; if (mc == null) return;
@@ -256,7 +256,6 @@ public class EditProfileController {
} }
if (!anyError) { if (!anyError) {
// 1) اگر صفحه‌ی MyProfile باز است
var mp = MyProfileController.getInstance(); var mp = MyProfileController.getInstance();
if (mp != null) { if (mp != null) {
mp.setProfileData( mp.setProfileData(