diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 04b128b..7a84301 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1,6 +1,9 @@ package org.to.telegramfinalproject.Client; import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Database.PrivateChatDatabase; @@ -9,6 +12,7 @@ import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchResultModel; +import org.to.telegramfinalproject.UI.ChatPageController; import java.io.*; import java.nio.file.Files; @@ -64,6 +68,10 @@ public class ActionHandler { } + public static ActionHandler getInstance() { + return instance; + } + public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { this.out = out; this.in = in; @@ -4229,38 +4237,38 @@ public class ActionHandler { } } - - public void sendMessageInteractive(UUID receiverId, String receiverType) { - Scanner sc = new Scanner(System.in); - - System.out.print("Type (TEXT / IMAGE / AUDIO): "); - String type = sc.nextLine().trim().toUpperCase(); - while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { - System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); - type = sc.nextLine().trim().toUpperCase(); - } - - System.out.print("Text (optional for media; empty = no caption): "); - String text = sc.nextLine(); - - if ("TEXT".equals(type)) { - sendTextMessage(receiverId, receiverType, text); - } else { - System.out.print("File path: "); - String path = sc.nextLine().trim(); - File f = new File(path); - if (!f.isFile()) { - System.out.println("❌ File not found"); - return; - } - try { - sendMediaMessage(receiverId, receiverType, type, f, text); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("❌ Media send failed: " + e.getMessage()); - } - } - } +// +// public void sendMessageInteractive(UUID receiverId, String receiverType) { +// Scanner sc = new Scanner(System.in); +// +// System.out.print("Type (TEXT / IMAGE / AUDIO): "); +// String type = sc.nextLine().trim().toUpperCase(); +// while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { +// System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); +// type = sc.nextLine().trim().toUpperCase(); +// } +// +// System.out.print("Text (optional for media; empty = no caption): "); +// String text = sc.nextLine(); +// +// if ("TEXT".equals(type)) { +// sendTextMessage(receiverId, receiverType, text); +// } else { +// System.out.print("File path: "); +// String path = sc.nextLine().trim(); +// File f = new File(path); +// if (!f.isFile()) { +// System.out.println("❌ File not found"); +// return; +// } +// try { +// sendMediaMessage(receiverId, receiverType, type, f, text); +// } catch (Exception e) { +// e.printStackTrace(); +// System.out.println("❌ Media send failed: " + e.getMessage()); +// } +// } +// } private void sendTextMessage(UUID receiverId, String receiverType, String content) { JSONObject req = new JSONObject() @@ -4278,36 +4286,117 @@ public class ActionHandler { } } +// +// public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { +// if (file == null) { +// System.out.println("❌ File is null"); +// return; +// } +// if (!file.exists()) { +// System.out.println("❌ File not found: " + file.getAbsolutePath()); +// return; +// } +// if (file.isDirectory()) { +// System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath()); +// return; +// } +// +// final UUID messageId = UUID.randomUUID(); +// +// try { +// String mime = detectMime(file, type.toUpperCase()); +// if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; +// +// JSONObject header = new JSONObject() +// .put("message_id", messageId.toString()) +// .put("sender_id", Session.getUserUUID()) +// .put("receiver_type", receiverType) // private|group|channel +// .put("receiver_id", receiverId.toString()) +// .put("message_type", type.toUpperCase()) // IMAGE | AUDIO +// .put("file_name", file.getName()) +// .put("mime_type", mime) +// .put("text", caption == null ? "" : caption); +// +// byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); +// long contentLen = file.length(); +// +// BlockingQueue q = new LinkedBlockingQueue<>(1); +// TelegramClient.pendingResponses.put(messageId.toString(), q); +// +// try { +// +// outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); +// outBin.flush(); +// +// // 2) binary frame: magic + headerLen + header + contentLen + content +// outBin.writeInt(0x4D444D31); // "MDM1" +// outBin.writeInt(headerBytes.length); // headerLen (int) +// outBin.write(headerBytes); // header +// outBin.writeLong(contentLen); // contentLen (long) +// +// try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { +// byte[] buf = new byte[8192]; +// int n; +// while ((n = fis.read(buf)) != -1) { +// outBin.write(buf, 0, n); +// } +// } +// outBin.flush(); +// +// JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); +// if (ack == null) { +// System.out.println("❌ Media ACK timeout for " + messageId); +// return; +// } +// +// String status = ack.optString("status", "error"); +// if ("success".equalsIgnoreCase(status)) { +// System.out.println("✅ Media sent. id=" + ack.optString("message_id") + +// " url=" + ack.optString("file_url")); +// } else { +// System.out.println("❌ Media failed: " + ack.optString("message")); +// } +// +// } finally { +// TelegramClient.pendingResponses.remove(messageId.toString()); +// } +// +// } catch (Exception e) { +// e.printStackTrace(); +// System.out.println("❌ sendMediaMessage error: " + e.getMessage()); +// } +// } - public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { - if (file == null) { - System.out.println("❌ File is null"); - return; - } - if (!file.exists()) { - System.out.println("❌ File not found: " + file.getAbsolutePath()); - return; - } - if (file.isDirectory()) { - System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath()); - return; - } - final UUID messageId = UUID.randomUUID(); + + // اورلود پیشنهادی: messageId از بیرون دریافت می‌شود + public void sendMediaMessage( + UUID messageId, // 👈 از بیرون می‌آید (برای Pending) + UUID receiverId, + String receiverType, // "private" | "group" | "channel" + String type, // "IMAGE" | "AUDIO" + File file, + String caption + ) { + if (file == null || !file.exists() || file.isDirectory()) { + Platform.runLater(() -> ChatPageController.get().updatePendingStatus( + messageId.toString(), "Failed (file)")); + return; + } try { String mime = detectMime(file, type.toUpperCase()); if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; JSONObject header = new JSONObject() - .put("message_id", messageId.toString()) - .put("sender_id", TelegramClient.loggedInUserId.toString()) - .put("receiver_type", receiverType) // private|group|channel - .put("receiver_id", receiverId.toString()) - .put("message_type", type.toUpperCase()) // IMAGE | AUDIO - .put("file_name", file.getName()) - .put("mime_type", mime) - .put("text", caption == null ? "" : caption); + .put("message_id", messageId.toString()) + .put("sender_id", Session.getUserUUID()) // همون UUID کاربر + .put("receiver_type",receiverType) + .put("receiver_id", receiverId.toString()) + .put("message_type", type.toUpperCase()) + .put("file_name", file.getName()) + .put("mime_type", mime) + .put("text", caption == null ? "" : caption); byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); long contentLen = file.length(); @@ -4316,15 +4405,13 @@ public class ActionHandler { TelegramClient.pendingResponses.put(messageId.toString(), q); try { - outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); outBin.flush(); - // 2) binary frame: magic + headerLen + header + contentLen + content - outBin.writeInt(0x4D444D31); // "MDM1" - outBin.writeInt(headerBytes.length); // headerLen (int) - outBin.write(headerBytes); // header - outBin.writeLong(contentLen); // contentLen (long) + outBin.writeInt(0x4D444D31); // "MDM1" + outBin.writeInt(headerBytes.length); + outBin.write(headerBytes); + outBin.writeLong(contentLen); try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { byte[] buf = new byte[8192]; @@ -4338,15 +4425,23 @@ public class ActionHandler { JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); if (ack == null) { System.out.println("❌ Media ACK timeout for " + messageId); + Platform.runLater(() -> ChatPageController.get().updatePendingStatus( + messageId.toString(), "Failed (timeout)")); return; } String status = ack.optString("status", "error"); if ("success".equalsIgnoreCase(status)) { - System.out.println("✅ Media sent. id=" + ack.optString("message_id") + - " url=" + ack.optString("file_url")); + // سرور شما فیلد نمایش را با نام display_path می‌فرستد (نه file_url) + String url = ack.optString("display_path", ack.optString("file_url", "")); + System.out.println("✅ Media sent. id=" + ack.optString("message_id") + " url=" + url); + + // بابل Pending را بردار (بلافاصله؛ پیام واقعی هم بعداً با real-time می‌آید) + Platform.runLater(() -> ChatPageController.get().removePendingBubble(messageId.toString())); } else { System.out.println("❌ Media failed: " + ack.optString("message")); + Platform.runLater(() -> ChatPageController.get().updatePendingStatus( + messageId.toString(), "Failed to send")); } } finally { @@ -4356,6 +4451,8 @@ public class ActionHandler { } catch (Exception e) { e.printStackTrace(); System.out.println("❌ sendMediaMessage error: " + e.getMessage()); + Platform.runLater(() -> ChatPageController.get().updatePendingStatus( + messageId.toString(), "Error")); } } @@ -4627,4 +4724,7 @@ public class ActionHandler { + + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 5ec8ae4..cca3521 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -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; + } + } + + + + diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index a85e9c6..a3c63fe 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1605,6 +1605,14 @@ public class ClientHandler implements Runnable { 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(); try { List reactions = MessageReactionDatabase.getReactions(m.getMessage_id()); diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 872d47e..a5546e3 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -7,10 +7,15 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; @@ -117,6 +122,15 @@ public class ChatPageController { private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده private final Map messageNodes = new HashMap<>(); + private final Deque pendingBubbles = new ArrayDeque<>(); + + private final Map pendingById = new HashMap<>(); + + + // جایی عمومی (مثلا بالای کلاس) + 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; @@ -156,6 +170,12 @@ public class ChatPageController { } + + + public static ChatPageController get() { + return instance; + } + private void initCurrentUserId() { try { String meStr = org.to.telegramfinalproject.Client.Session @@ -668,7 +688,10 @@ public class ChatPageController { fMessageId, // message_id "", "", "", // forwarded_from, forwarded_by, reply_to_id false, // edited - null // reactions + null, // reactions + null, + null + ); // آپدیت پیش‌نمایش لیست چت‌ها @@ -691,15 +714,170 @@ public class ChatPageController { }).start(); } - private void openFileChooser() { 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()); - if (file != null) { - System.out.println("Selected file: " + file.getAbsolutePath()); - // TODO: actually send file - addSystemMessage("Attached file: " + file.getName()); + if (file == null) return; + + String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO" + if (type == null) { toast("Only image or audio"); return; } + + if (currentChat == null) { toast("Not available chat"); return; } + UUID receiverId = currentChat.getId(); // همون chat_id + String receiverType = currentChat.getType(); // "private" | "group" | "channel" + + String caption = (messageInput != null) ? messageInput.getText().trim() : ""; + if (messageInput != null) messageInput.clear(); + + // 1) message_id را همین‌جا بساز تا Pending به همین ID وصل شود + UUID messageId = UUID.randomUUID(); + + // 2) حباب Pending (نسخه‌ای که messageId می‌گیرد) + addPendingMediaBubble(messageId.toString(), file, type, caption); + + // 3) ارسال واقعی با همین messageId + new Thread(() -> { + ActionHandler ah = ActionHandler.getInstance(); + ah.sendMediaMessage(messageId, receiverId, receiverType, type, file, caption); + // اگر ACK success برگشت، خود ActionHandler می‌تونه removePendingBubble(messageId) صدا بزنه + // وگرنه در onRealTimeNewMessage که پیام واقعی آمد، پاک می‌کنیم (کد آن را قبلاً دادم). + }, "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 HBox addPendingMediaBubble(String messageId, File file, String type, String caption) { + HBox root = new HBox(8); + root.setFillHeight(true); + root.setAlignment(Pos.CENTER_RIGHT); // چون outgoing است + + // preview + if ("IMAGE".equalsIgnoreCase(type)) { + ImageView iv = new ImageView(new Image(file.toURI().toString(), 240, 240, true, true, true)); + iv.setPreserveRatio(true); + iv.setFitWidth(240); + iv.setFitHeight(240); + root.getChildren().add(iv); + } else if ("AUDIO".equalsIgnoreCase(type)) { + Label name = new Label(file.getName()); + root.getChildren().add(new HBox(6, new Label("🎵"), name)); + } + + VBox right = new VBox(4); + if (caption != null && !caption.isBlank()) { + Label cap = new Label(caption); + cap.setWrapText(true); + right.getChildren().add(cap); + } + + HBox statusRow = new HBox(6); + ProgressIndicator spinner = new ProgressIndicator(); + spinner.setPrefSize(14, 14); + Label status = new Label("Sending..."); + status.getProperties().put("role", "statusLabel"); // برای آپدیت بعدی + statusRow.getChildren().addAll(spinner, status); + right.getChildren().add(statusRow); + + root.getChildren().add(right); + + // برچسب messageId روی نود + if (messageId != null) root.getProperties().put("messageId", messageId); + + messageContainer.getChildren().add(root); + + // ثبت در مپ/صف + if (messageId != null) pendingById.put(messageId, root); + pendingBubbles.addLast(root); + + return root; + } + + + public void removePendingBubble(String messageId) { + if (messageId == null) return; + HBox node = pendingById.remove(messageId); + if (node != null) { + pendingBubbles.remove(node); + messageContainer.getChildren().remove(node); + } + } + + + private void toast(String msg) { + // هر جور که خودت نوتیف/Toast داری + System.out.println("ℹ️ " + msg); + if (messageInput != null) { + messageInput.setTooltip(new Tooltip(msg)); } } @@ -1311,6 +1489,58 @@ public class ChatPageController { private final java.util.Map 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) { messageContainer.getChildren().clear(); @@ -1331,10 +1561,14 @@ public class ChatPageController { String senderId = str(m, "sender_id"); String senderName = str(m, "sender_name"); String type = str(m, "message_type"); - String content = str(m, "content"); + String content = str(m, "content"); // این همون کپشنه برای IMAGE String whenStr = str(m, "send_at"); 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 fwdBy = nz(str(m, "forwarded_by")); @@ -1352,17 +1586,17 @@ public class ChatPageController { java.time.LocalDateTime ts = parseWhen(whenStr); - // نمایش + // 👇 امضای جدید addBubble با fileUrl/thumbUrl addBubble(outgoing, senderName, type, content, ts, msgId, - fwdFrom, fwdBy, replyToId, edited, reactions); + fwdFrom, fwdBy, replyToId, edited, reactions, fileUrl, thumbUrl); } - // کمی فاصله بین پیام‌ها messageContainer.setSpacing(8); messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } + private String shortId(String id){ return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); } @@ -1501,104 +1735,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( boolean outgoing, String displayName, String type, - String content, + String content, // برای IMAGE = کپشن java.time.LocalDateTime sentAt, String messageId, String forwardedFrom, String forwardedBy, String replyToId, boolean edited, - org.json.JSONArray reactions + org.json.JSONArray reactions, + String fileUrl, // 👈 جدید + String thumbUrl // 👈 جدید ) { - // === 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; + String mine = dark ? "#2b7cff" : "#d8ecff"; + String theirs = dark ? "#2c333a" : "#f2f4f7"; - 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); + if (isText) { + // متن + 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) { 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.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()); @@ -1606,12 +1952,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) { ContextMenu menu = new ContextMenu(); // --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) --- HBox reactions = new HBox(8); - String[] emojis = {"👍","👎","😂","😭","🕊️","⚡"}; + String[] emojis = {"👍","👎","😂","😭","⚡"}; for (String e : emojis) { Button b = new Button(e); b.getStyleClass().add("reaction-btn"); @@ -1724,32 +2154,26 @@ public class ChatPageController { } - private javafx.scene.Node buildReactionsBarFromJson(org.json.JSONArray arr, boolean dark) { - javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(6); - bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - for (int i = 0; i < arr.length(); i++) { - org.json.JSONObject ro = arr.optJSONObject(i); - if (ro == null) continue; - String emoji = ro.optString("emoji", "👍"); - int count = ro.optInt("count", 1); - boolean byMe = ro.optBoolean("by_me", false); + private Node buildReactionsBarFromJson(org.json.JSONArray reactions, boolean dark) { + HBox bar = new HBox(6); + for (int i = 0; i < reactions.length(); i++) { + var r = reactions.getJSONObject(i); + String emo = r.optString("emoji", "👍"); // 👈 باید کاراکتر واقعی باشه + int cnt = r.optInt("count", 1); + + Label chip = new Label(emo + " " + cnt); + chip.getStyleClass().add("emoji-label"); // 👈 کلاس CSS برای ایموجی + chip.setStyle("-fx-background-color:" + (dark ? "#39424a" : "#e9eef3") + + "; -fx-padding:3 8; -fx-background-radius:12;"); - String chipBg = byMe ? (dark ? "#215a9f" : "#d1e8ff") - : (dark ? "#2f3942" : "#eef3f7"); - String text = emoji + (count > 0 ? (" " + count) : ""); - Label chip = new Label(text); - chip.setStyle( - "-fx-font-size: 12;" + - "-fx-background-radius: 12;" + - "-fx-padding: 2 8;" + - "-fx-background-color: " + chipBg + ";" - ); bar.getChildren().add(chip); } return bar; } + + private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "…" : s; } public boolean isSameChat(UUID chatId, String type) { @@ -1799,56 +2223,209 @@ public class ChatPageController { // } - public void onRealTimeNewMessage(JSONObject m) { +// 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) { +// 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 senderId = str(m,"sender_id"); +// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") +// : (hasVal(senderId) ? shortId(senderId) : "Unknown"); +// +// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT"; +// String content = str(m,"content"); // برای IMAGE = کپشن +// 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 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); +// +// // تشخیص خروجی/ورودی +// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid")) +// ? 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; +// +// // 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl) +// addBubble(outgoing, senderName, type, content, ts, msgId, +// fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl); +// +// if (currentChat != null) markAsRead(currentChat); +// +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + + public void onRealTimeNewMessage(org.json.JSONObject m) { try { - String chatIdStr = str(m,"receiver_id"); - String chatType = str(m,"receiver_type"); + // 1) chat id/type با fallback + String chatIdStr = nz(m.optString("receiver_id", m.optString("chat_id",""))); + String chatType = nz(m.optString("receiver_type", m.optString("chat_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")); - } + // 2) message_id با fallback از id + 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; + // 3) sender/name + String senderId = str(m,"sender_id"); 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 content = str(m,"content"); - String whenIso = str(m,"send_at"); + // 4) نوع پیام (سرور ممکنه lowercase بده) + String tRaw = nz(m.optString("message_type","TEXT")); + String type = tRaw.trim().toUpperCase(java.util.Locale.ROOT); + // 5) متن/کپشن (content یا text) + String content = nz(m.optString("content", m.optString("text",""))); + + // 6) زمان + String whenIso = nz(m.optString("send_at", m.optString("created_at",""))); + java.time.LocalDateTime ts = parseWhen(whenIso); + if (ts == null) ts = java.time.LocalDateTime.now(); + + // 7) فایل: هم فرمت قدیم (file_url/thumb_url) هم جدید (media.url/thumbnail_url) + String fileUrl = nz(m.optString("file_url","")); + String thumbUrl = nz(m.optString("thumb_url","")); + org.json.JSONObject media = m.optJSONObject("media"); + if (media != null) { + if (!hasVal(fileUrl)) fileUrl = nz(media.optString("url","")); + if (!hasVal(thumbUrl)) thumbUrl = nz(media.optString("thumbnail_url","")); + // اگر width/height لازم شد، از media.optInt("width"), media.optInt("height") بخوان + } + + // 8) فوروارد/ریپلای/ادیت/ری‌اکشن 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"); + org.json.JSONArray reacts = arr(m,"reactions"); - LocalDateTime ts = parseWhen(whenIso); - if (ts == null) ts = LocalDateTime.now(); + // 9) outgoing + String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid")) + ? Session.currentUser.getString("internal_uuid") : ""; + boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId); - // ایندکس برای ریپلای/ادیت/ری‌اکشن‌های بعدی + // 10) ایندکس برای ریپلای/ادیت‌های بعدی msgIndex.put(msgId, m); - // آپدیت لیست چت‌ها (پریویو) - boolean incoming = true; // از سرور آمده → ورودی - updateChatListPreview(chatId, chatType, incoming, content, type); + // 11) آپدیت پریویو لیست چت‌ها + String previewText = switch (type) { + case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo"); + case "AUDIO" -> "🎵 Audio"; + default -> content; + }; + updateChatListPreview(chatId, chatType, !outgoing, previewText, type); - // اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد + // 12) اگر چت جاری است، حباب بساز if (!isCurrent) return; - // اضافه کردن حباب بدون رفرش - addBubble(false, senderName, type, content, ts, msgId, - fwdFrom, fwdBy, replyTo, edited, reacts); + addBubble(outgoing, senderName, type, content, ts, msgId, + fwdFrom, fwdBy, replyTo, edited, reacts, + fileUrl, thumbUrl); - // خوانده شد (در صورت نیاز) if (currentChat != null) markAsRead(currentChat); } catch (Exception e) { @@ -1856,6 +2433,8 @@ public class ChatPageController { } } + + private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) { var mc = MainController.getInstance(); if (mc == null) return; @@ -3093,4 +3672,23 @@ public class ChatPageController { } } + + public void updatePendingStatus(String messageId, String text) { + HBox node = pendingById.get(messageId); + if (node == null) return; + // پیدا کردن لیبل وضعیت + if (node.getChildren().size() >= 2 && node.getChildren().get(1) instanceof VBox v) { + for (Node n : v.getChildren()) { + if (n instanceof HBox row) { + for (Node c : row.getChildren()) { + if (c instanceof Label l && "statusLabel".equals(l.getProperties().get("role"))) { + l.setText(text); return; + } + } + } + } + } + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java b/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java index 93da353..4b41213 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java @@ -256,7 +256,6 @@ public class EditProfileController { } if (!anyError) { - // 1) اگر صفحه‌ی MyProfile باز است var mp = MyProfileController.getInstance(); if (mp != null) { mp.setProfileData( diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/chat.css b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css index bc3a46b..3a71f2b 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/chat.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css @@ -77,4 +77,20 @@ /* نسخه‌ی قرمز برای UNBLOCK */ .button.footer-link-btn.danger { -fx-text-fill: #D32F2F; -} \ No newline at end of file +} + + +/* فونت‌های ایموجی روی همهٔ سیستم‌ها */ +.emoji-label { + -fx-font-family: "Segoe UI Emoji", "Segoe UI Symbol", + "Apple Color Emoji", "Noto Color Emoji", + "Arial Unicode MS"; + -fx-font-size: 13px; +} + +/* اگر بخواهی فقط داخل صفحهٔ چت اعمال شود */ +.chat-root .emoji-label { + -fx-font-family: "Segoe UI Emoji", "Segoe UI Symbol", + "Apple Color Emoji", "Noto Color Emoji", + "Arial Unicode MS"; +} diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css index f4a8338..f22e7e8 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css @@ -1283,4 +1283,18 @@ .link-btn:hover { -fx-underline: true; -} \ No newline at end of file +} +/* ===== Dark theme overrides ===== */ +.root.dark .tf-title { -fx-text-fill: #f3f4f6; } +.root.dark .tf-caption { -fx-text-fill: #b8bdc7; } +.root.dark .tf-item-desc { -fx-text-fill: #9ca3af; } + +/* TitledPane header/content */ +.root.dark .titled-pane > .title { + -fx-background-color: #2b2f33; + -fx-text-fill: #f3f4f6; +} +.root.dark .titled-pane > *.content { + -fx-background-color: #1f2327; +} + diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css index 8238057..c6944cb 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css @@ -1261,4 +1261,39 @@ .link-btn:hover { -fx-underline: true; -} \ No newline at end of file +} +/* ===== Base (shared) ===== */ +.tf-root { -fx-background-color: transparent; } + +.tf-toolbar { -fx-padding: 10 12; -fx-alignment: CENTER_LEFT; } +.tf-title { -fx-font-size: 18px; -fx-font-weight: 700; } +.tf-caption { -fx-opacity: .75; } + +.tf-section { -fx-padding: 10 12 14 12; } +.tf-item-desc { -fx-wrap-text: true; -fx-font-size: 13px; -fx-line-spacing: 1; } + +/* TitledPane (Accordion items) */ +.titled-pane > .title { + -fx-padding: 10 12; + -fx-background-radius: 10; +} +.titled-pane > *.content { + -fx-background-radius: 10; + -fx-background-insets: 0; + -fx-padding: 8 4 12 4; +} + +/* ===== Light theme overrides ===== */ +.root.light .tf-title { -fx-text-fill: #111111; } +.root.light .tf-caption { -fx-text-fill: #555555; } +.root.light .tf-item-desc { -fx-text-fill: #444444; } + +/* TitledPane header/content */ +.root.light .titled-pane > .title { + -fx-background-color: #f1f1f1; + -fx-text-fill: #111111; +} +.root.light .titled-pane > *.content { + -fx-background-color: #ffffff; +} + diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/feature_FAQ.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/feature_FAQ.fxml new file mode 100644 index 0000000..23509b3 --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/feature_FAQ.fxml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/feature_board.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/feature_board.fxml new file mode 100644 index 0000000..96671be --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/feature_board.fxml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
diff --git a/uploads/audios/2025-09-06/f2e8ed9f-7bfa-4064-aba8-6b1d723ba3d7.mp3 b/uploads/audios/2025-09-06/f2e8ed9f-7bfa-4064-aba8-6b1d723ba3d7.mp3 new file mode 100644 index 0000000..7c414d9 Binary files /dev/null and b/uploads/audios/2025-09-06/f2e8ed9f-7bfa-4064-aba8-6b1d723ba3d7.mp3 differ diff --git a/uploads/images/2025-09-06/aa3264e4-685d-4ab1-b6fb-e7fa1e44f1c5.jpg b/uploads/images/2025-09-06/aa3264e4-685d-4ab1-b6fb-e7fa1e44f1c5.jpg new file mode 100644 index 0000000..4ba8aae Binary files /dev/null and b/uploads/images/2025-09-06/aa3264e4-685d-4ab1-b6fb-e7fa1e44f1c5.jpg differ diff --git a/uploads/images/2025-09-06/d82b5664-7682-4b37-ac62-4bc291933618.jpg b/uploads/images/2025-09-06/d82b5664-7682-4b37-ac62-4bc291933618.jpg new file mode 100644 index 0000000..3a96141 Binary files /dev/null and b/uploads/images/2025-09-06/d82b5664-7682-4b37-ac62-4bc291933618.jpg differ diff --git a/uploads/images/2025-09-06/f8c06ffd-c35b-454b-844b-a97c2352a07e.jpg b/uploads/images/2025-09-06/f8c06ffd-c35b-454b-844b-a97c2352a07e.jpg new file mode 100644 index 0000000..05084e6 Binary files /dev/null and b/uploads/images/2025-09-06/f8c06ffd-c35b-454b-844b-a97c2352a07e.jpg differ diff --git a/uploads/images/2025-09-07/242a76f9-f540-4d3a-bd0a-6507099b164e.jpeg b/uploads/images/2025-09-07/242a76f9-f540-4d3a-bd0a-6507099b164e.jpeg new file mode 100644 index 0000000..9515e2c Binary files /dev/null and b/uploads/images/2025-09-07/242a76f9-f540-4d3a-bd0a-6507099b164e.jpeg differ diff --git a/uploads/images/2025-09-07/7b6d3072-1c72-40d8-bee7-d6dbfda54dd2.jpg b/uploads/images/2025-09-07/7b6d3072-1c72-40d8-bee7-d6dbfda54dd2.jpg new file mode 100644 index 0000000..4ba8aae Binary files /dev/null and b/uploads/images/2025-09-07/7b6d3072-1c72-40d8-bee7-d6dbfda54dd2.jpg differ diff --git a/uploads/images/2025-09-07/a93c7324-22fa-4268-9f1e-34a9448f5f6e.jpg b/uploads/images/2025-09-07/a93c7324-22fa-4268-9f1e-34a9448f5f6e.jpg new file mode 100644 index 0000000..f6d2c4e Binary files /dev/null and b/uploads/images/2025-09-07/a93c7324-22fa-4268-9f1e-34a9448f5f6e.jpg differ diff --git a/uploads/images/2025-09-07/d9208502-a2a5-41d2-b912-29d622b582bf.jpg b/uploads/images/2025-09-07/d9208502-a2a5-41d2-b912-29d622b582bf.jpg new file mode 100644 index 0000000..103ad9a Binary files /dev/null and b/uploads/images/2025-09-07/d9208502-a2a5-41d2-b912-29d622b582bf.jpg differ diff --git a/uploads/images/2025-09-07/dcb9b568-abea-43f4-bc39-acd71dc2438c.jpg b/uploads/images/2025-09-07/dcb9b568-abea-43f4-bc39-acd71dc2438c.jpg new file mode 100644 index 0000000..b0ee46f Binary files /dev/null and b/uploads/images/2025-09-07/dcb9b568-abea-43f4-bc39-acd71dc2438c.jpg differ diff --git a/uploads/images/2025-09-07/e85885e6-4502-45fe-a2fe-71dd1f8595ba.jpg b/uploads/images/2025-09-07/e85885e6-4502-45fe-a2fe-71dd1f8595ba.jpg new file mode 100644 index 0000000..05084e6 Binary files /dev/null and b/uploads/images/2025-09-07/e85885e6-4502-45fe-a2fe-71dd1f8595ba.jpg differ diff --git a/uploads/images/2025-09-07/f87ee3b0-0a90-4c54-be85-a91f04d517df.jpg b/uploads/images/2025-09-07/f87ee3b0-0a90-4c54-be85-a91f04d517df.jpg new file mode 100644 index 0000000..6436ef0 Binary files /dev/null and b/uploads/images/2025-09-07/f87ee3b0-0a90-4c54-be85-a91f04d517df.jpg differ