package org.to.telegramfinalproject.UI; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.geometry.Side; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; import javafx.stage.FileChooser; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.AvatarLocalResolver; import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Models.ChatEntry; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.UUID; public class ChatPageController { // ===== messages area ===== @FXML private VBox messageContainer; @FXML private ScrollPane messageScrollPane; // ===== input area ===== @FXML private TextArea messageInput; @FXML private Button sendButton; @FXML private Button attachmentButton; @FXML private ImageView attachmentIcon; // inside the attachment button // ===== header ===== @FXML private ImageView userAvatar; // 36x36 in the FXML @FXML private Label chatTitle; // contact/group title @FXML private Label chatStatus; // last seen / online @FXML private Button searchInChatButton; // magnifier button @FXML private ImageView searchIcon; @FXML private Button moreButton; // 3-dots button @FXML private ImageView moreIcon; @FXML private ContextMenu moreMenu; @FXML private MenuItem viewProfileItem; @FXML private MenuItem deleteChatItem; // ===== send icon ===== @FXML private ImageView sendIcon; // ===== Time formatter for messages ===== private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); private static final String YESTERDAY_LABEL = "Yesterday"; // ===== state ===== private String chatName; private final ThemeManager themeManager = ThemeManager.getInstance(); private final java.util.Set pendingClientMsgIds = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); // Where your icons live private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; private ChatEntry currentChat; private UUID me; // ----- helpers for safe strings ----- private static String nz(String s) { return s == null ? "" : s.trim(); } private static boolean hasVal(String s) { if (s == null) return false; String t = s.trim(); return !t.isEmpty() && !"null".equalsIgnoreCase(t); } private void initCurrentUserId() { try { String meStr = org.to.telegramfinalproject.Client.Session .currentUser.getString("internal_uuid"); me = UUID.fromString(meStr); } catch (Exception ignore) { me = null; } } private static String str(org.json.JSONObject j, String k) { try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; } } private static boolean bool(org.json.JSONObject j, String k) { try { return (j.has(k) && !j.isNull(k)) && j.getBoolean(k); } catch (Exception e) { return false; } } private static org.json.JSONArray arr(org.json.JSONObject j, String k) { try { return (j.has(k) && !j.isNull(k)) ? j.getJSONArray(k) : null; } catch (Exception e) { return null; } } private String formatWhen(LocalDateTime ts) { if (ts == null) return ""; LocalDate today = LocalDate.now(); LocalDate d = ts.toLocalDate(); if (d.isEqual(today)) { return FMT_HHMM.format(ts); } else if (d.isEqual(today.minusDays(1))) { return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); } else { return FMT_DATE_TIME.format(ts); } } /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ private LocalDateTime parseWhen(String iso) { if (iso == null || iso.isEmpty()) return null; try { return OffsetDateTime.parse(iso).toLocalDateTime(); } catch (Exception ignore) { try { return LocalDateTime.parse(iso); } catch (Exception e) { return null; } } } @FXML public void initialize() { initCurrentUserId(); // Send button if (sendButton != null) { sendButton.setOnAction(e -> sendMessage()); } // ENTER = send, SHIFT+ENTER = newline messageInput.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { if (e.getCode() == javafx.scene.input.KeyCode.ENTER) { if (e.isShiftDown()) { // let newline happen } else { e.consume(); // block newline sendMessage(); } } }); // Enable/disable send button by content (ignore spaces/newlines) messageInput.textProperty().addListener((obs, oldV, newV) -> { boolean hasRealText = newV != null && !newV.trim().isEmpty(); sendButton.setDisable(!hasRealText); // toggle style class for color state var sc = sendButton.getStyleClass(); sc.removeAll("send-empty", "send-ready"); sc.add(hasRealText ? "send-ready" : "send-empty"); }); // Auto-resize input box like Telegram messageInput.textProperty().addListener((obs, oldText, newText) -> { Platform.runLater(() -> { var textNode = messageInput.lookup(".text"); if (textNode != null) { double textHeight = textNode.getBoundsInLocal().getHeight(); double padding = 20; // top + bottom padding double newHeight = textHeight + padding; if (newHeight < 40) newHeight = 40; // min (1 row) if (newHeight > 120) newHeight = 120; // max (~5 rows) messageInput.setPrefHeight(newHeight); } }); }); // Start in "empty" state sendButton.getStyleClass().add("send-empty"); // attach file if (attachmentButton != null) { attachmentButton.setOnAction(e -> openFileChooser()); } // Hook "More" button → show menu under it if (moreButton != null) { moreButton.setOnAction(e -> { if (!moreMenu.isShowing()) { moreMenu.show(moreButton, Side.BOTTOM, 0, 0); } else { moreMenu.hide(); } }); } // Menu item actions viewProfileItem.setOnAction(e -> { System.out.println("Viewing profile of " + chatName); // TODO: open profile UI }); deleteChatItem.setOnAction(e -> { System.out.println("Deleting chat with " + chatName); // TODO: delete logic }); // Initial icon sync once the Scene is ready (stylesheet applied) Platform.runLater(this::syncIconsWithTheme); // Auto-Scroll messageContainer.heightProperty().addListener((obs, oldVal, newVal) -> { Platform.runLater(() -> { var timeline = new javafx.animation.Timeline(); var kv = new javafx.animation.KeyValue( messageScrollPane.vvalueProperty(), 1.0, javafx.animation.Interpolator.EASE_BOTH ); var kf = new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), kv); timeline.getKeyFrames().add(kf); timeline.play(); }); }); // Smooth scroll feel messageScrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); messageScrollPane.setPannable(true); messageScrollPane.setFitToWidth(true); messageScrollPane.setFitToHeight(false); messageScrollPane.getContent().setOnScroll(event -> { double deltaY = event.getDeltaY() * 0.00003; // smaller = smoother messageScrollPane.setVvalue(messageScrollPane.getVvalue() - deltaY); }); // React to theme changes everywhere themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme()); } // private void initCurrentUserId() { // try { // String meStr = org.to.telegramfinalproject.Client.Session // .currentUser.getString("internal_uuid"); // me = UUID.fromString(meStr); // } catch (Exception ignore) { // me = null; // } // } // // private String formatWhen(LocalDateTime ts) { // if (ts == null) return ""; // LocalDate today = LocalDate.now(); // LocalDate d = ts.toLocalDate(); // // if (d.isEqual(today)) { // return FMT_HHMM.format(ts); // } else if (d.isEqual(today.minusDays(1))) { // return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); // } else { // return FMT_DATE_TIME.format(ts); // } // } // // /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ // private LocalDateTime parseWhen(String iso) { // if (iso == null || iso.isEmpty()) return null; // try { // return OffsetDateTime.parse(iso).toLocalDateTime(); // } catch (Exception ignore) { // try { // return LocalDateTime.parse(iso); // } catch (Exception e) { // return null; // } // } // } @FXML private void openSearchPanel() { MainController.getInstance().showSearchPanel(); } // /** // * Called by main controller when opening a chat. // */ // public void setChat(String chatName, String avatarPath) { // this.chatName = chatName; // // // Header text (if present) // if (chatTitle != null) chatTitle.setText(chatName); // if (chatStatus != null) chatStatus.setText("last seen recently"); // or live status // // // Load avatar if provided // if (userAvatar != null && avatarPath != null) { // try { // Image avatarImg = new Image(getClass().getResourceAsStream(avatarPath)); // userAvatar.setImage(avatarImg); // Circle clip = new Circle(18, 18, 18); // x, y, radius // userAvatar.setClip(clip); // } catch (Exception e) { // System.err.println("Could not load avatar: " + avatarPath); // } // } // // addSystemMessage("Chat with " + chatName + " opened."); // Platform.runLater(() -> messageInput.requestFocus()); // } // ----- actions ----- // private void sendMessage() { // String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); // if (!text.isEmpty()) { // addMessage("You", text); // messageInput.clear(); // // TODO: send to server // } // } private void sendMessage() { // 0) Read and validate input String raw = messageInput.getText(); String text = (raw == null) ? "" : raw.trim(); if (text.isEmpty()) return; if (currentChat == null) { addSystemMessage("No chat is selected."); return; } // 1) Clear input immediately for good UX messageInput.clear(); // 2) Snapshot chat info (must be final for lambdas) final UUID targetChatId = currentChat.getId(); final String targetType = currentChat.getType(); // "private" | "group" | "channel" final String contentToSend = text; // effectively final // 3) Build the SAME JSON as your console method (for TEXT only) org.json.JSONObject req = new org.json.JSONObject(); req.put("action", "send_message"); req.put("receiver_type", targetType); req.put("receiver_id", targetChatId.toString()); req.put("content", contentToSend); req.put("message_type", "TEXT"); // 4) Send on a background thread new Thread(() -> { org.json.JSONObject resp; try { resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); } catch (Exception ex) { ex.printStackTrace(); Platform.runLater(() -> addSystemMessage("Send failed: " + ex.getMessage())); return; } // 5) Check status like your console method if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { String err = (resp != null) ? resp.optString("message", "No response") : "No response"; Platform.runLater(() -> addSystemMessage("Send failed: " + err)); return; } // 6) Extract fields (your console reads data.message_id; handle both shapes) org.json.JSONObject data = resp.optJSONObject("data"); String messageId = null; String sendAtIso = null; if (data != null) { // If server returns { data: { message_id, send_at, ... } } messageId = data.optString("message_id", null); // Some servers nest: { data: { message: {...} } } if (messageId == null) { org.json.JSONObject msgObj = data.optJSONObject("message"); if (msgObj != null) { messageId = msgObj.optString("message_id", null); sendAtIso = msgObj.optString("send_at", null); } } else { sendAtIso = data.optString("send_at", null); } } if (messageId == null) messageId = java.util.UUID.randomUUID().toString(); final java.time.LocalDateTime ts = (sendAtIso != null && !sendAtIso.isBlank()) ? parseWhen(sendAtIso) : java.time.LocalDateTime.now(); final String fMessageId = messageId; final java.time.LocalDateTime fTs = ts; // 7) Update UI on FX thread (render outgoing bubble + index for reply previews) Platform.runLater(() -> { // If user switched chats while sending, don’t render here if (currentChat == null || !currentChat.getId().equals(targetChatId)) return; addBubble( true, // outgoing "You", // display name "TEXT", // message type contentToSend, // content fTs, // timestamp fMessageId, // message_id "", "", "", // forwarded_from, forwarded_by, reply_to_id false, // edited null // reactions ); //Real time var mc = MainController.getInstance(); if (mc != null) { String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend); mc.onChatUpdated(targetChatId, targetType, fTs, /*isIncoming*/ false, preview); } // Keep it in msgIndex for reply previews org.json.JSONObject idx = new org.json.JSONObject(); idx.put("message_id", fMessageId); idx.put("message_type", "TEXT"); idx.put("content", contentToSend); idx.put("sender_name", "You"); idx.put("sender_id", (me != null) ? me.toString() : ""); idx.put("send_at", fTs.toString()); msgIndex.put(fMessageId, idx); }); }).start(); } private void openFileChooser() { FileChooser fc = new FileChooser(); fc.setTitle("Select a file to send"); 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()); } } // ----- UI helpers ----- // /** // * Add a normal message bubble (very simple for now). // */ // public void addMessage(String sender, String content) { // Label msg = new Label(sender + ": " + content); // msg.setWrapText(true); // // boolean dark = themeManager.isDarkMode(); // String bubbleColor = dark ? "#20405a" : "#4fa8f0"; // String textColor = dark ? "#e8f1f8" : "#0f141a"; // msg.setStyle( // "-fx-background-color: " + bubbleColor + ";" + // "-fx-text-fill: " + textColor + ";" + // "-fx-padding: 6 10; -fx-background-radius: 10;" // ); // // messageContainer.getChildren().add(msg); // } private void addSystemMessage(String content) { Label sys = new Label(content); sys.setStyle("-fx-text-fill: gray; -fx-font-size: 11;"); messageContainer.getChildren().add(sys); messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } /** * Update all header/footer icons according to current theme. */ private void syncIconsWithTheme() { boolean dark = themeManager.isDarkMode(); // We use “_light” icons on dark backgrounds, and “_dark” on light backgrounds. String suffix = dark ? "_light.png" : "_dark.png"; // attachment if (attachmentIcon != null) { attachmentIcon.setImage(loadIcon("attachment" + suffix)); } // send if (sendIcon != null) { sendIcon.setImage(loadIcon("send_cyan2.png")); } // header icons if (searchIcon != null) { searchIcon.setImage(loadIcon("search" + suffix)); } if (moreIcon != null) { moreIcon.setImage(loadIcon("more" + suffix)); } // header text tint (if you’re not fully relying on CSS) if (chatTitle != null) chatTitle.setStyle(dark ? "-fx-text-fill:#e8f1f8;" : "-fx-text-fill:#0f141a;"); if (chatStatus != null) chatStatus.setStyle(dark ? "-fx-text-fill:#8ea1b2;" : "-fx-text-fill:#7e8a97;"); // View profile icon in more button ((ImageView) viewProfileItem.getGraphic()) .setImage(loadIcon("view_profile" + suffix)); } private Image loadIcon(String filename) { var url = getClass().getResource(ICON_BASE + filename); if (url == null) { System.err.println("Icon not found: " + ICON_BASE + filename); return null; } return new Image(url.toExternalForm()); } public void showChat(ChatEntry entry) { this.currentChat = entry; chatTitle.setText(entry.getName()); // آواتار پیش‌فرض بر اساس نوع // ChatPageController.showChat(...) if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { Image img = AvatarLocalResolver.load(entry.getImageUrl()); if (img != null) { userAvatar.setImage(img); } else { // ⬇️ فال‌بک بر اساس نوع setDefaultHeaderAvatarByType(entry.getType()); } } else { setDefaultHeaderAvatarByType(entry.getType()); } // userAvatar.setClip(new Circle(20, 20, 20)); AvatarFX.circleClip(userAvatar, 36); fetchAndRenderHeader(entry); // پیام‌ها messageContainer.getChildren().clear(); loadMessages(entry); markAsRead(entry); Platform.runLater(() -> messageInput.requestFocus()); } private void loadMessages(ChatEntry entry) { JSONObject req = new JSONObject(); req.put("action", "get_messages_UI"); req.put("receiver_id", String.valueOf(entry.getId())); req.put("receiver_type", entry.getType()); req.put("limit", 50); new Thread(() -> { JSONObject resp; try { resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); } catch (Exception ex) { ex.printStackTrace(); return; } if (resp == null) return; String status = ""; try { status = resp.getString("status"); } catch (Exception ignore) { } if (!"success".equals(status)) return; JSONObject data = null; try { data = resp.getJSONObject("data"); } catch (Exception ignore) { } if (data == null) return; JSONArray arr = null; try { arr = data.getJSONArray("messages"); } catch (Exception ignore) { } if (arr == null) return; JSONArray finalArr = arr; Platform.runLater(() -> renderMessages(finalArr)); }).start(); } // private void renderMessages(org.json.JSONArray arr) { // messageContainer.getChildren().clear(); // // String myId = Session.currentUser != null // ? Session.currentUser.optString("internal_uuid", "") // : ""; // // for (int i = 0; i < arr.length(); i++) { // org.json.JSONObject m = arr.getJSONObject(i); // // String senderId = m.has("sender_id") && !m.isNull("sender_id") ? m.getString("sender_id") : ""; // String senderName = m.has("sender_name") && !m.isNull("sender_name") ? m.getString("sender_name") : ""; // String type = m.has("message_type") && !m.isNull("message_type") ? m.getString("message_type") : "TEXT"; // String content = m.has("content") && !m.isNull("content") ? m.getString("content") : ""; // String whenStr = m.has("send_at") && !m.isNull("send_at") ? m.getString("send_at") : null; // // boolean outgoing = senderId.equalsIgnoreCase(myId); // // String display = outgoing ? "You" // : (!senderName.isEmpty() ? senderName // : (senderId.isEmpty() ? "Unknown" // : senderId.substring(0, Math.min(8, senderId.length())))); // // String text; // switch (type.toUpperCase()) { // case "IMAGE": text = "[Image]"; break; // case "AUDIO": text = "[Audio]"; break; // case "VIDEO": text = "[Video]"; break; // case "FILE": text = "[File]"; break; // default: text = content; break; // } // // LocalDateTime ts = parseWhen(whenStr); // addBubble(outgoing, display, text, ts); // } // // messageScrollPane.layout(); // messageScrollPane.setVvalue(1.0); // } 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 String shortId(String id){ return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); } private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); readReq.put("receiver_id", entry.getId().toString()); // internal_id readReq.put("receiver_type", entry.getType()); ActionHandler.sendWithResponse(readReq); } // private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { // // Label meta = new Label(displayName + " • " + formatWhen(sentAt)); // meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); // meta.setWrapText(true); // // Label msg = new Label(content); // msg.setWrapText(true); // // boolean dark = themeManager.isDarkMode(); // String mine = dark ? "#2b7cff" : "#d8ecff"; // String theirs = dark ? "#2c333a" : "#ffffff"; // String bg = outgoing ? mine : theirs; // // msg.setStyle( // "-fx-background-color:" + bg + ";" + // "-fx-padding:8 12;" + // "-fx-background-radius:12;" + // "-fx-max-width: 520;" // ); // msg.setMinHeight(Region.USE_PREF_SIZE); // // VBox bubble = new VBox(4, meta, msg); // // javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); // row.setFillHeight(true); // row.setSpacing(6); // row.setAlignment(outgoing // ? javafx.geometry.Pos.CENTER_RIGHT // : javafx.geometry.Pos.CENTER_LEFT); // // messageContainer.getChildren().add(row); // } 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 ) { 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); 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); boolean dark = themeManager.isDarkMode(); String mine = dark ? "#2b7cff" : "#d8ecff"; String theirs = dark ? "#2c333a" : "#ffffff"; String bg = outgoing ? mine : theirs; msg.setStyle( "-fx-background-color:" + bg + ";" + "-fx-padding:8 12;" + "-fx-background-radius:12;" + "-fx-max-width: 520;" ); msg.setMinHeight(Region.USE_PREF_SIZE); // بدنه حباب VBox bubble = new VBox(4); // spacing عمودی داخل حباب bubble.getChildren().add(meta); // 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) { bubble.getChildren().add(buildReactionsBarFromJson(reactions, dark)); } // چیدمان راست/چپ javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); row.setFillHeight(true); row.setSpacing(4); row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT : javafx.geometry.Pos.CENTER_LEFT); row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6)); messageContainer.getChildren().add(row); } private String bracketLabel(String t) { String tt = (t == null) ? "" : t.trim().toUpperCase(); switch (tt) { case "IMAGE": return "[Image]"; case "AUDIO": return "[Audio]"; case "VIDEO": return "[Video]"; case "FILE": return "[File]"; default: return "[Message]"; } } private javafx.scene.Node buildForwardHeader(String forwardedFrom, String forwardedBy) { String from = hasVal(forwardedFrom) ? forwardedFrom.trim() : null; String by = hasVal(forwardedBy) ? forwardedBy.trim() : null; String txt = (from != null && by != null) ? ("Forwarded from " + from + " by " + by) : (from != null) ? ("Forwarded from " + from) : (by != null) ? ("Forwarded by " + by) : "Forwarded"; Label l = new Label(txt); l.setStyle("-fx-font-size: 11; -fx-text-fill: #5b8bb1;"); l.setWrapText(true); return l; } private javafx.scene.Node buildReplyBoxFromIndex(String replyToId) { org.json.JSONObject r = msgIndex.get(replyToId); String from = "Unknown"; String preview; if (r != null) { String rType = r.optString("message_type", "TEXT"); String rContent = r.optString("content", ""); String rSender = r.optString("sender_name", ""); if (hasVal(rSender)) from = rSender; preview = "TEXT".equalsIgnoreCase(rType) ? rContent : bracketLabel(rType); } else { preview = "[Quoted message]"; } Label t = new Label("Reply to " + from); t.setStyle("-fx-font-size: 11; -fx-text-fill: #5b8bb1; -fx-font-weight: bold;"); Label p = new Label(ellipsize(preview == null ? "" : preview, 140)); p.setWrapText(true); p.setStyle("-fx-font-size: 12; -fx-text-fill: #6b7c8a;"); VBox box = new VBox(2, t, p); box.setStyle( "-fx-background-color: rgba(125,150,175,0.10);" + "-fx-padding:6 8; -fx-background-radius:8;" + "-fx-border-color:#7da0b8; -fx-border-width:0 0 0 2;" ); return box; } 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); 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) { return currentChat != null && currentChat.getId().equals(chatId) && currentChat.getType().equalsIgnoreCase(type); } 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); if (!isSameChat(chatId, chatType)) { System.out.println("[UI] RT msg for another chat: " + chatId); return; } // id → message_id fallback if (!m.has("message_id") && m.has("id")) { m.put("message_id", m.getString("id")); } 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 msgId = str(m,"message_id"); LocalDateTime ts = parseWhen(whenIso); if (ts == null) ts = LocalDateTime.now(); addBubble(false, senderName, type, content, ts, msgId, str(m,"forwarded_from"), str(m,"forwarded_by"), str(m,"reply_to_id"), bool(m,"is_edited"), arr(m,"reactions")); if (hasVal(msgId)) msgIndex.put(msgId, m); if (currentChat != null) markAsRead(currentChat); } catch (Exception e) { e.printStackTrace(); } } private void fetchAndRenderHeader(ChatEntry entry) { JSONObject req = new JSONObject(); req.put("action", "get_header_info"); req.put("receiver_id", entry.getId().toString()); req.put("receiver_type", entry.getType()); // باید "private" باشه // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { req.put("viewer_id", viewerId.toString()); } new Thread(() -> { JSONObject resp; try { resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); } catch (Exception ex) { ex.printStackTrace(); return; } if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { System.err.println("get_header_info failed: " + (resp != null ? resp.optString("message") : "null resp")); return; } JSONObject data = resp.optJSONObject("data"); if (data == null) return; Platform.runLater(() -> renderHeaderFromData(entry, data)); }).start(); } private void renderHeaderFromData(ChatEntry entry, JSONObject data) { String t = entry.getType() == null ? "" : entry.getType().toLowerCase(); switch (t) { case "private" -> updatePrivateHeader(entry, data); case "group" -> updateGroupHeader(entry, data); case "channel" -> updateChannelHeader(entry, data); default -> chatStatus.setText(""); } } private void updatePrivateHeader(ChatEntry entry, JSONObject data) { String name = nz(data.optString("profile_name", entry.getName())); chatTitle.setText(name); // other_user_id برای ریل‌تایم status String other = data.optString("other_user_id", ""); if (!other.isBlank()) { try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} } // تصویر String img = data.optString("image_url", ""); if (hasVal(img)) { try { Image im = AvatarLocalResolver.load(img); // ⬅️ if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} } chatStatus.setText(userStatusText( data.optBoolean("online", false), data.optString("last_seen", null) )); } private void updateGroupHeader(ChatEntry entry, JSONObject data) { chatTitle.setText(nz(data.optString("group_name", entry.getName()))); String img = data.optString("image_url", ""); if (hasVal(img)) { try { Image im = AvatarLocalResolver.load(img); // ⬅️ if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} } int members = data.optInt("member_count", 0); int online = data.optInt("online_count", -1); chatStatus.setText(online >= 0 ? (members + " members, " + online + " online") : (members + " members")); } private void updateChannelHeader(ChatEntry entry, JSONObject data) { chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); String img = data.optString("image_url", ""); if (hasVal(img)) { try { Image im = AvatarLocalResolver.load(img); // ⬅️ if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} } int subs = data.optInt("member_count", 0); chatStatus.setText(subs + " subscribers"); } public void onUserStatusChanged(String userUuid, String status, String lastSeenIso) { if (currentChat == null || !"private".equalsIgnoreCase(currentChat.getType())) return; var other = currentChat.getOtherUserId(); if (other == null || !other.toString().equalsIgnoreCase(userUuid)) return; if ("online".equalsIgnoreCase(status)) { chatStatus.setText("online"); } else { if (hasVal(lastSeenIso)) { var ts = parseWhen(lastSeenIso); chatStatus.setText(ts != null ? ("last seen " + formatWhen(ts)) : "last seen recently"); } else { chatStatus.setText("last seen recently"); } } } private String userStatusText(boolean online, String lastSeenIso) { if (online) return "online"; LocalDateTime ts = parseWhen(lastSeenIso); if (ts == null) return "Last seen recently"; LocalDate today = LocalDate.now(); LocalDate d = ts.toLocalDate(); if (d.isEqual(today)) { return FMT_HHMM.format(ts); } long days = ChronoUnit.DAYS.between(d, today); if (days > 30) { return "Last seen long time ago"; } return "recently"; } private void setDefaultHeaderAvatarByType(String type){ String path = switch (type == null ? "" : type.toLowerCase()) { case "channel" -> "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; }; userAvatar.setImage(new Image( java.util.Objects.requireNonNull(getClass().getResourceAsStream(path)) )); } }