diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 06b5dd0..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; @@ -4233,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() @@ -4282,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", 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); + .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(); @@ -4320,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]; @@ -4342,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 { @@ -4360,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")); } } @@ -4631,4 +4724,7 @@ public class ActionHandler { + + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 9f2af55..38ed2dd 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -124,6 +124,7 @@ public class ChatPageController { private final Map messageNodes = new HashMap<>(); private final Deque pendingBubbles = new ArrayDeque<>(); + private final Map pendingById = new HashMap<>(); // جایی عمومی (مثلا بالای کلاس) @@ -169,6 +170,12 @@ public class ChatPageController { } + + + public static ChatPageController get() { + return instance; + } + private void initCurrentUserId() { try { String meStr = org.to.telegramfinalproject.Client.Session @@ -707,52 +714,39 @@ public class ChatPageController { }).start(); } - private void openFileChooser() { FileChooser fc = new FileChooser(); 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") + 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) return; - // نوع (IMAGE/AUDIO) را از اسم/محتوا حدس بزنیم - String type = guessType(file); - if (type == null) { - toast("Only Image and Aduio"); - return; - } - currentChatId = String.valueOf(currentChat.getId()); - Session.currentChatType = currentChat.getType(); + String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO" + if (type == null) { toast("Only image or audio"); return; } - // مقصد چت از 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" + 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) حباب Pending فوری در UI - HBox pending = addPendingMediaBubble(file, type, caption); + // 1) message_id را همین‌جا بساز تا Pending به همین ID وصل شود + UUID messageId = UUID.randomUUID(); - // 2) ارسال واقعی در بک‌گراند با ActionHandler + // 2) حباب Pending (نسخه‌ای که messageId می‌گیرد) + addPendingMediaBubble(messageId.toString(), file, type, caption); + + // 3) ارسال واقعی با همین messageId new Thread(() -> { ActionHandler ah = ActionHandler.getInstance(); - ah.sendMediaMessage(receiverId, receiverType, type, file, caption); - - // نکته: پیام واقعی بعد از ذخیره روی سرور، از طریق Listener برمی‌گرده. - // وقتی رسید، در Listener این متد را صدا بزنید تا یک حباب Pending حذف شود: - // Platform.runLater(() -> removeOnePendingBubble()); + ah.sendMediaMessage(messageId, receiverId, receiverType, type, file, caption); + // اگر ACK success برگشت، خود ActionHandler می‌تونه removePendingBubble(messageId) صدا بزنه + // وگرنه در onRealTimeNewMessage که پیام واقعی آمد، پاک می‌کنیم (کد آن را قبلاً دادم). }, "Media-Uploader").start(); } @@ -765,62 +759,120 @@ public class ChatPageController { return null; } - /** ساخت یک حباب «درحال ارسال…» */ - private HBox addPendingMediaBubble(File file, String type, String caption) { - HBox root = new HBox(8); - root.getStyleClass().add("bubble-outgoing"); // استایل دلخواهت - root.setFillHeight(true); +// /** ساخت یک حباب «درحال ارسال…» */ +// 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); +// } +// } - ImageView iv = null; + + 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)) { - iv = new ImageView(new Image(file.toURI().toString(), 360, 360, true, true, true)); + ImageView iv = new ImageView(new Image(file.toURI().toString(), 240, 240, true, true, true)); iv.setPreserveRatio(true); - iv.setFitWidth(240); // سایز معقول برای Pending + iv.setFitWidth(240); 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); + 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.getStyleClass().add("msg-caption"); cap.setWrapText(true); right.getChildren().add(cap); } HBox statusRow = new HBox(6); ProgressIndicator spinner = new ProgressIndicator(); - spinner.setPrefSize(16, 16); + spinner.setPrefSize(14, 14); 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); - + 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; } - /** وقتی پیام واقعی (از خودِ کاربر) برای همین چت رسید، یکی از Pendingها را حذف کن. */ - public void removeOnePendingBubble() { - HBox node = pendingBubbles.pollFirst(); + + 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); @@ -2091,32 +2143,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) { @@ -2224,67 +2270,150 @@ 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 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(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"); + // 4) نوع پیام (سرور ممکنه lowercase بده) + String tRaw = nz(m.optString("message_type","TEXT")); + String type = tRaw.trim().toUpperCase(java.util.Locale.ROOT); - // 👇 جدید: URL ها برای عکس/صدا - String fileUrl = str(m,"file_url"); - String thumbUrl = str(m,"thumb_url"); + // 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(); - - // اندیس پیام برای ریپلای/ادیت - msgIndex.put(msgId, m); - - // تشخیص خروجی/ورودی + // 9) outgoing 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()) { + // 10) ایندکس برای ریپلای/ادیت‌های بعدی + msgIndex.put(msgId, m); + + // 11) آپدیت پریویو لیست چت‌ها + String previewText = switch (type) { case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo"); case "AUDIO" -> "🎵 Audio"; - default -> content; + default -> content; }; updateChatListPreview(chatId, chatType, !outgoing, previewText, type); + // 12) اگر چت جاری است، حباب بساز if (!isCurrent) return; - // 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl) addBubble(outgoing, senderName, type, content, ts, msgId, - fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl); + fwdFrom, fwdBy, replyTo, edited, reacts, + fileUrl, thumbUrl); if (currentChat != null) markAsRead(currentChat); @@ -2294,6 +2423,7 @@ public class ChatPageController { } + private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) { var mc = MainController.getInstance(); if (mc == null) return; @@ -3523,4 +3653,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/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/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