Merge remote-tracking branch 'origin/Main-UI' into Main-UI

# Conflicts:
#	src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css
#	src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css
This commit is contained in:
Asal Lotfi
2025-09-07 22:51:40 +03:30
21 changed files with 1420 additions and 163 deletions
@@ -1,6 +1,9 @@
package org.to.telegramfinalproject.Client; package org.to.telegramfinalproject.Client;
import javafx.application.Platform; 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.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Database.PrivateChatDatabase; 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.ContactEntry;
import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchRequestModel;
import org.to.telegramfinalproject.Models.SearchResultModel; import org.to.telegramfinalproject.Models.SearchResultModel;
import org.to.telegramfinalproject.UI.ChatPageController;
import java.io.*; import java.io.*;
import java.nio.file.Files; 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) { public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
this.out = out; this.out = out;
this.in = in; this.in = in;
@@ -4229,38 +4237,38 @@ public class ActionHandler {
} }
} }
//
public void sendMessageInteractive(UUID receiverId, String receiverType) { // public void sendMessageInteractive(UUID receiverId, String receiverType) {
Scanner sc = new Scanner(System.in); // Scanner sc = new Scanner(System.in);
//
System.out.print("Type (TEXT / IMAGE / AUDIO): "); // System.out.print("Type (TEXT / IMAGE / AUDIO): ");
String type = sc.nextLine().trim().toUpperCase(); // String type = sc.nextLine().trim().toUpperCase();
while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { // while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) {
System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); // System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): ");
type = sc.nextLine().trim().toUpperCase(); // type = sc.nextLine().trim().toUpperCase();
} // }
//
System.out.print("Text (optional for media; empty = no caption): "); // System.out.print("Text (optional for media; empty = no caption): ");
String text = sc.nextLine(); // String text = sc.nextLine();
//
if ("TEXT".equals(type)) { // if ("TEXT".equals(type)) {
sendTextMessage(receiverId, receiverType, text); // sendTextMessage(receiverId, receiverType, text);
} else { // } else {
System.out.print("File path: "); // System.out.print("File path: ");
String path = sc.nextLine().trim(); // String path = sc.nextLine().trim();
File f = new File(path); // File f = new File(path);
if (!f.isFile()) { // if (!f.isFile()) {
System.out.println("❌ File not found"); // System.out.println("❌ File not found");
return; // return;
} // }
try { // try {
sendMediaMessage(receiverId, receiverType, type, f, text); // sendMediaMessage(receiverId, receiverType, type, f, text);
} catch (Exception e) { // } catch (Exception e) {
e.printStackTrace(); // e.printStackTrace();
System.out.println("❌ Media send failed: " + e.getMessage()); // System.out.println("❌ Media send failed: " + e.getMessage());
} // }
} // }
} // }
private void sendTextMessage(UUID receiverId, String receiverType, String content) { private void sendTextMessage(UUID receiverId, String receiverType, String content) {
JSONObject req = new JSONObject() 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<JSONObject> 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 { try {
String mime = detectMime(file, type.toUpperCase()); String mime = detectMime(file, type.toUpperCase());
if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*";
JSONObject header = new JSONObject() JSONObject header = new JSONObject()
.put("message_id", messageId.toString()) .put("message_id", messageId.toString())
.put("sender_id", TelegramClient.loggedInUserId.toString()) .put("sender_id", Session.getUserUUID()) // همون UUID کاربر
.put("receiver_type", receiverType) // private|group|channel .put("receiver_type",receiverType)
.put("receiver_id", receiverId.toString()) .put("receiver_id", receiverId.toString())
.put("message_type", type.toUpperCase()) // IMAGE | AUDIO .put("message_type", type.toUpperCase())
.put("file_name", file.getName()) .put("file_name", file.getName())
.put("mime_type", mime) .put("mime_type", mime)
.put("text", caption == null ? "" : caption); .put("text", caption == null ? "" : caption);
byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
long contentLen = file.length(); long contentLen = file.length();
@@ -4316,15 +4405,13 @@ public class ActionHandler {
TelegramClient.pendingResponses.put(messageId.toString(), q); TelegramClient.pendingResponses.put(messageId.toString(), q);
try { try {
outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
outBin.flush(); outBin.flush();
// 2) binary frame: magic + headerLen + header + contentLen + content outBin.writeInt(0x4D444D31); // "MDM1"
outBin.writeInt(0x4D444D31); // "MDM1" outBin.writeInt(headerBytes.length);
outBin.writeInt(headerBytes.length); // headerLen (int) outBin.write(headerBytes);
outBin.write(headerBytes); // header outBin.writeLong(contentLen);
outBin.writeLong(contentLen); // contentLen (long)
try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) {
byte[] buf = new byte[8192]; byte[] buf = new byte[8192];
@@ -4338,15 +4425,23 @@ public class ActionHandler {
JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS);
if (ack == null) { if (ack == null) {
System.out.println("❌ Media ACK timeout for " + messageId); System.out.println("❌ Media ACK timeout for " + messageId);
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
messageId.toString(), "Failed (timeout)"));
return; return;
} }
String status = ack.optString("status", "error"); String status = ack.optString("status", "error");
if ("success".equalsIgnoreCase(status)) { if ("success".equalsIgnoreCase(status)) {
System.out.println("✅ Media sent. id=" + ack.optString("message_id") + // سرور شما فیلد نمایش را با نام display_path می‌فرستد (نه file_url)
" url=" + ack.optString("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 { } else {
System.out.println("❌ Media failed: " + ack.optString("message")); System.out.println("❌ Media failed: " + ack.optString("message"));
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
messageId.toString(), "Failed to send"));
} }
} finally { } finally {
@@ -4356,6 +4451,8 @@ public class ActionHandler {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
System.out.println("❌ sendMediaMessage error: " + e.getMessage()); System.out.println("❌ sendMediaMessage error: " + e.getMessage());
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
messageId.toString(), "Error"));
} }
} }
@@ -4627,4 +4724,7 @@ public class ActionHandler {
} }
@@ -1537,6 +1537,35 @@ public class MessageDatabase {
public static String getFirstAttachmentUrlByType(UUID messageId, String messageType) {
final String sql = """
SELECT file_url
FROM message_attachments
WHERE message_id = ?
AND (file_type = ? OR ? IS NULL)
ORDER BY attachment_id
LIMIT 1
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
// اگر messageType خالی بود، با NULL بفرست که شرط OR ? IS NULL برقرار شه
String mt = (messageType == null || messageType.isBlank()) ? null : messageType.toUpperCase();
ps.setString(2, mt);
if (mt == null) ps.setNull(3, Types.VARCHAR); else ps.setString(3, mt);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("file_url") : null;
}
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
@@ -1605,6 +1605,14 @@ public class ClientHandler implements Runnable {
obj.put("forwarded_by", JSONObject.NULL); obj.put("forwarded_by", JSONObject.NULL);
} }
String mt = m.getMessage_type();
if (mt != null && !mt.equalsIgnoreCase("TEXT")) {
String fileUrl = MessageDatabase.getFirstAttachmentUrlByType(m.getMessage_id(), mt);
if (fileUrl != null && !fileUrl.isBlank()) {
obj.put("file_url", fileUrl);
}
}
JSONArray reactionsArr = new JSONArray(); JSONArray reactionsArr = new JSONArray();
try { try {
List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id()); List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id());
@@ -7,10 +7,15 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
@@ -117,6 +122,15 @@ public class ChatPageController {
private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده
private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده
private final Map<String, Node> messageNodes = new HashMap<>(); private final Map<String, Node> messageNodes = new HashMap<>();
private final Deque<HBox> pendingBubbles = new ArrayDeque<>();
private final Map<String, HBox> 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; private JSONObject lastHeaderData = null;
@@ -156,6 +170,12 @@ public class ChatPageController {
} }
public static ChatPageController get() {
return instance;
}
private void initCurrentUserId() { private void initCurrentUserId() {
try { try {
String meStr = org.to.telegramfinalproject.Client.Session String meStr = org.to.telegramfinalproject.Client.Session
@@ -668,7 +688,10 @@ public class ChatPageController {
fMessageId, // message_id fMessageId, // message_id
"", "", "", // forwarded_from, forwarded_by, reply_to_id "", "", "", // forwarded_from, forwarded_by, reply_to_id
false, // edited false, // edited
null // reactions null, // reactions
null,
null
); );
// آپدیت پیش‌نمایش لیست چت‌ها // آپدیت پیش‌نمایش لیست چت‌ها
@@ -691,15 +714,170 @@ public class ChatPageController {
}).start(); }).start();
} }
private void openFileChooser() { private void openFileChooser() {
FileChooser fc = new FileChooser(); FileChooser fc = new FileChooser();
fc.setTitle("Select a file to send"); fc.setTitle("Select image or audio");
fc.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Images", "*.png","*.jpg","*.jpeg","*.gif","*.bmp","*.webp"),
new FileChooser.ExtensionFilter("Audio", "*.mp3","*.wav","*.m4a","*.ogg","*.aac")
);
File file = fc.showOpenDialog(attachmentButton.getScene().getWindow()); File file = fc.showOpenDialog(attachmentButton.getScene().getWindow());
if (file != null) { if (file == null) return;
System.out.println("Selected file: " + file.getAbsolutePath());
// TODO: actually send file String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO"
addSystemMessage("Attached file: " + file.getName()); 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<String, org.json.JSONObject> msgIndex = new java.util.HashMap<>(); private final java.util.Map<String, org.json.JSONObject> msgIndex = new java.util.HashMap<>();
// private void renderMessages(org.json.JSONArray list) {
// messageContainer.getChildren().clear();
//
// // برای reply-preview: ایندکس کردن پیام‌ها با message_id
// msgIndex.clear();
// for (int i = 0; i < list.length(); i++) {
// org.json.JSONObject m = list.getJSONObject(i);
// String mid = str(m, "message_id");
// if (!mid.isEmpty()) msgIndex.put(mid, m);
// }
//
// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
// ? Session.currentUser.getString("internal_uuid") : "";
//
// for (int i = 0; i < list.length(); i++) {
// org.json.JSONObject m = list.getJSONObject(i);
//
// String senderId = str(m, "sender_id");
// String senderName = str(m, "sender_name");
// String type = str(m, "message_type");
// String content = str(m, "content");
// String whenStr = str(m, "send_at");
// String msgId = str(m, "message_id");
//
// // فوروارد / ریپلای / ادیت / ری‌اکشن
// String fwdFrom = nz(str(m, "forwarded_from"));
// String fwdBy = nz(str(m, "forwarded_by"));
// String replyToId = nz(str(m, "reply_to_id"));
// boolean edited = bool(m, "is_edited");
// org.json.JSONArray reactions = arr(m, "reactions");
//
// boolean outgoing = senderId.equalsIgnoreCase(myId);
// if (senderName == null || senderName.isBlank()) {
// senderName = outgoing ? "You"
// : (senderId == null || senderId.isBlank()
// ? "Unknown"
// : senderId.substring(0, Math.min(8, senderId.length())));
// }
//
// java.time.LocalDateTime ts = parseWhen(whenStr);
//
// // نمایش
// addBubble(outgoing, senderName, type, content, ts, msgId,
// fwdFrom, fwdBy, replyToId, edited, reactions);
// }
//
// // کمی فاصله بین پیام‌ها
// messageContainer.setSpacing(8);
// messageScrollPane.layout();
// messageScrollPane.setVvalue(1.0);
// }
private void renderMessages(org.json.JSONArray list) { private void renderMessages(org.json.JSONArray list) {
messageContainer.getChildren().clear(); messageContainer.getChildren().clear();
@@ -1331,10 +1561,14 @@ public class ChatPageController {
String senderId = str(m, "sender_id"); String senderId = str(m, "sender_id");
String senderName = str(m, "sender_name"); String senderName = str(m, "sender_name");
String type = str(m, "message_type"); String type = str(m, "message_type");
String content = str(m, "content"); String content = str(m, "content"); // این همون کپشنه برای IMAGE
String whenStr = str(m, "send_at"); String whenStr = str(m, "send_at");
String msgId = str(m, "message_id"); String msgId = str(m, "message_id");
// 👇 جدید: URL ها
String fileUrl = str(m, "file_url");
String thumbUrl = str(m, "thumb_url");
// فوروارد / ریپلای / ادیت / ری‌اکشن // فوروارد / ریپلای / ادیت / ری‌اکشن
String fwdFrom = nz(str(m, "forwarded_from")); String fwdFrom = nz(str(m, "forwarded_from"));
String fwdBy = nz(str(m, "forwarded_by")); String fwdBy = nz(str(m, "forwarded_by"));
@@ -1352,17 +1586,17 @@ public class ChatPageController {
java.time.LocalDateTime ts = parseWhen(whenStr); java.time.LocalDateTime ts = parseWhen(whenStr);
// نمایش // 👇 امضای جدید addBubble با fileUrl/thumbUrl
addBubble(outgoing, senderName, type, content, ts, msgId, addBubble(outgoing, senderName, type, content, ts, msgId,
fwdFrom, fwdBy, replyToId, edited, reactions); fwdFrom, fwdBy, replyToId, edited, reactions, fileUrl, thumbUrl);
} }
// کمی فاصله بین پیام‌ها
messageContainer.setSpacing(8); messageContainer.setSpacing(8);
messageScrollPane.layout(); messageScrollPane.layout();
messageScrollPane.setVvalue(1.0); messageScrollPane.setVvalue(1.0);
} }
private String shortId(String id){ private String shortId(String id){
return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length()));
} }
@@ -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( private void addBubble(
boolean outgoing, boolean outgoing,
String displayName, String displayName,
String type, String type,
String content, String content, // برای IMAGE = کپشن
java.time.LocalDateTime sentAt, java.time.LocalDateTime sentAt,
String messageId, String messageId,
String forwardedFrom, String forwardedFrom,
String forwardedBy, String forwardedBy,
String replyToId, String replyToId,
boolean edited, boolean edited,
org.json.JSONArray reactions org.json.JSONArray reactions,
String fileUrl, // 👈 جدید
String thumbUrl // 👈 جدید
) { ) {
// === Meta (نام + زمان) ===
String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt); String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt);
if (edited) metaText += " (edited)"; if (edited) metaText += " (edited)";
Label meta = new Label(metaText); Label meta = new Label(metaText);
meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
meta.setWrapText(true); meta.setWrapText(true);
// برچسب برای آپدیت‌های بعدی (edit)
meta.getProperties().put("role", "metaLabel"); meta.getProperties().put("role", "metaLabel");
// === متن/نوع پیام ===
String t = type == null ? "" : type.trim().toUpperCase(); String t = type == null ? "" : type.trim().toUpperCase();
boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
Label msg = new Label(bodyText); // رنگ پس‌زمینه برای متن (برای عکس پس‌زمینه نمی‌ذاریم تا تمیز باشه)
msg.setWrapText(true);
msg.setMinHeight(Region.USE_PREF_SIZE);
// برچسب برای آپدیت‌های بعدی (edit)
msg.getProperties().put("role", "msgLabel");
// === رنگ بابل‌ها
boolean dark = themeManager.isDarkMode(); boolean dark = themeManager.isDarkMode();
String mine = dark ? "#2b7cff" : "#d8ecff"; // outgoing (من) String mine = dark ? "#2b7cff" : "#d8ecff";
String theirs = dark ? "#2c333a" : "#f2f4f7"; // incoming (خیلی روشن به‌جای سفید) String theirs = dark ? "#2c333a" : "#f2f4f7";
String bg = outgoing ? mine : theirs;
msg.setStyle(
"-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;"
);
// === بدنه‌ی بابل ===
VBox bubble = new VBox(4); VBox bubble = new VBox(4);
bubble.getChildren().add(meta); bubble.getChildren().add(meta);
// برچسب‌گذاری بابل برای پیدا کردنش در آپدیت‌های realtime
if (messageId != null && !messageId.isBlank()) { if (messageId != null && !messageId.isBlank()) {
bubble.getProperties().put("messageId", messageId); bubble.getProperties().put("messageId", messageId);
} }
// Forward header (اختیاری)
if (hasVal(forwardedFrom) || hasVal(forwardedBy)) { if (hasVal(forwardedFrom) || hasVal(forwardedBy)) {
bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy));
} }
// Reply preview (اختیاری)
if (hasVal(replyToId)) { if (hasVal(replyToId)) {
bubble.getChildren().add(buildReplyBoxFromIndex(replyToId)); bubble.getChildren().add(buildReplyBoxFromIndex(replyToId));
} }
// متن اصلی if (isText) {
bubble.getChildren().add(msg); // متن
String bodyText = content == null ? "" : content;
Label msg = new Label(bodyText);
msg.setWrapText(true);
msg.setMinHeight(Region.USE_PREF_SIZE);
msg.getProperties().put("role", "msgLabel");
String bg = outgoing ? mine : theirs;
msg.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(msg);
} else if ("IMAGE".equals(t)) {
// 👇 نمایش تصویر از روی URL سرور + کپشن اختیاری
Node imageNode = buildImageNode(fileUrl, thumbUrl, content);
bubble.getChildren().add(imageNode);
} else if ("AUDIO".equals(t)) {
// می‌تونی بعداً کاملش کنی
Label ph = new Label("🎵 Audio");
ph.setWrapText(true);
String bg = outgoing ? mine : theirs;
ph.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(ph);
} else {
// ناشناخته
Label ph = new Label("[" + t + "]");
ph.setWrapText(true);
String bg = outgoing ? mine : theirs;
ph.setStyle("-fx-background-color:" + bg + ";" +
"-fx-padding:8 12;" +
"-fx-background-radius:12;" +
"-fx-max-width: 520;");
bubble.getChildren().add(ph);
}
// Reactions (اختیاری) + برچسب برای تعویض سریع در ریِل‌تایم
if (reactions != null && reactions.length() > 0) { if (reactions != null && reactions.length() > 0) {
Node rxBar = buildReactionsBarFromJson(reactions, dark); Node rxBar = buildReactionsBarFromJson(reactions, dark);
rxBar.getProperties().put("role", "reactionsBar"); rxBar.getProperties().put("role", "reactionsBar");
bubble.getChildren().add(rxBar); bubble.getChildren().add(rxBar);
} }
// === ردیف چیدمان راست/چپ ===
HBox row = new HBox(bubble); HBox row = new HBox(bubble);
row.setFillHeight(true); row.setFillHeight(true);
row.setSpacing(4); row.setSpacing(4);
row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT); row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);
row.setPadding(new Insets(2, 6, 2, 6)); row.setPadding(new Insets(2, 6, 2, 6));
// اضافه به کانتینر
messageContainer.getChildren().add(row); messageContainer.getChildren().add(row);
// ایندکس نود برای آپدیت/حذف realtime
if (messageId != null && !messageId.isBlank()) { if (messageId != null && !messageId.isBlank()) {
messageNodes.put(messageId, row); messageNodes.put(messageId, row);
} }
boolean isMine = outgoing; boolean isMine = outgoing;
// منوی راست‌کلیک/کلیک (بدون تغییر در ساختار کدت)
ContextMenu menu = buildMessageMenu(isMine, messageId, type, content); ContextMenu menu = buildMessageMenu(isMine, messageId, type, content);
row.setOnContextMenuRequested(ev -> { row.setOnContextMenuRequested(ev -> { menu.show(row, ev.getScreenX(), ev.getScreenY()); ev.consume(); });
menu.show(row, ev.getScreenX(), ev.getScreenY());
ev.consume();
});
row.setOnMouseClicked(ev -> { row.setOnMouseClicked(ev -> {
if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) { if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) {
menu.show(row, ev.getScreenX(), ev.getScreenY()); menu.show(row, ev.getScreenX(), ev.getScreenY());
@@ -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) { private ContextMenu buildMessageMenu(boolean isMine, String messageId, String type, String content) {
ContextMenu menu = new ContextMenu(); ContextMenu menu = new ContextMenu();
// --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) --- // --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) ---
HBox reactions = new HBox(8); HBox reactions = new HBox(8);
String[] emojis = {"👍","👎","😂","😭","🕊️",""}; String[] emojis = {"👍","👎","😂","😭",""};
for (String e : emojis) { for (String e : emojis) {
Button b = new Button(e); Button b = new Button(e);
b.getStyleClass().add("reaction-btn"); b.getStyleClass().add("reaction-btn");
@@ -1724,32 +2154,26 @@ public class ChatPageController {
} }
private javafx.scene.Node buildReactionsBarFromJson(org.json.JSONArray arr, boolean dark) { private Node buildReactionsBarFromJson(org.json.JSONArray reactions, boolean dark) {
javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(6); HBox bar = new HBox(6);
bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); for (int i = 0; i < reactions.length(); i++) {
for (int i = 0; i < arr.length(); i++) { var r = reactions.getJSONObject(i);
org.json.JSONObject ro = arr.optJSONObject(i); String emo = r.optString("emoji", "👍"); // 👈 باید کاراکتر واقعی باشه
if (ro == null) continue; int cnt = r.optInt("count", 1);
String emoji = ro.optString("emoji", "👍");
int count = ro.optInt("count", 1); Label chip = new Label(emo + " " + cnt);
boolean byMe = ro.optBoolean("by_me", false); 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); bar.getChildren().add(chip);
} }
return bar; return bar;
} }
private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "" : s; } private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "" : s; }
public boolean isSameChat(UUID chatId, String type) { 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 { try {
String chatIdStr = str(m,"receiver_id"); // 1) chat id/type با fallback
String chatType = str(m,"receiver_type"); 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; if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
UUID chatId = UUID.fromString(chatIdStr); UUID chatId = UUID.fromString(chatIdStr);
boolean isCurrent = isSameChat(chatId, chatType); boolean isCurrent = isSameChat(chatId, chatType);
// id message_id fallback // 2) message_id با fallback از id
if (!m.has("message_id") && m.has("id")) { if (!m.has("message_id") && m.has("id")) m.put("message_id", m.getString("id"));
m.put("message_id", m.getString("id"));
}
String msgId = str(m,"message_id"); String msgId = str(m,"message_id");
if (!hasVal(msgId)) return; if (!hasVal(msgId)) return;
// اگر قبلاً همین پیام داخل UI اضافه شده، دیگه دوباره نساز // تکراری نساز
if (messageNodes.containsKey(msgId)) return; 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") 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"; // 4) نوع پیام (سرور ممکنه lowercase بده)
String content = str(m,"content"); String tRaw = nz(m.optString("message_type","TEXT"));
String whenIso = str(m,"send_at"); 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 fwdFrom = str(m,"forwarded_from");
String fwdBy = str(m,"forwarded_by"); String fwdBy = str(m,"forwarded_by");
String replyTo = str(m,"reply_to_id"); String replyTo = str(m,"reply_to_id");
boolean edited = bool(m,"is_edited"); boolean edited = bool(m,"is_edited");
JSONArray reacts = arr(m,"reactions"); org.json.JSONArray reacts = arr(m,"reactions");
LocalDateTime ts = parseWhen(whenIso); // 9) outgoing
if (ts == null) ts = LocalDateTime.now(); 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); msgIndex.put(msgId, m);
// آپدیت لیست چت‌ها (پریویو) // 11) آپدیت پریویو لیست چت‌ها
boolean incoming = true; // از سرور آمده ورودی String previewText = switch (type) {
updateChatListPreview(chatId, chatType, incoming, content, type); case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
case "AUDIO" -> "🎵 Audio";
default -> content;
};
updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
// اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد // 12) اگر چت جاری است، حباب بساز
if (!isCurrent) return; if (!isCurrent) return;
// اضافه کردن حباب بدون رفرش addBubble(outgoing, senderName, type, content, ts, msgId,
addBubble(false, senderName, type, content, ts, msgId, fwdFrom, fwdBy, replyTo, edited, reacts,
fwdFrom, fwdBy, replyTo, edited, reacts); fileUrl, thumbUrl);
// خوانده شد (در صورت نیاز)
if (currentChat != null) markAsRead(currentChat); if (currentChat != null) markAsRead(currentChat);
} catch (Exception e) { } catch (Exception e) {
@@ -1856,6 +2433,8 @@ public class ChatPageController {
} }
} }
private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) { private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) {
var mc = MainController.getInstance(); var mc = MainController.getInstance();
if (mc == null) return; if (mc == null) return;
@@ -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;
}
}
}
}
}
}
} }
@@ -256,7 +256,6 @@ public class EditProfileController {
} }
if (!anyError) { if (!anyError) {
// 1) اگر صفحه‌ی MyProfile باز است
var mp = MyProfileController.getInstance(); var mp = MyProfileController.getInstance();
if (mp != null) { if (mp != null) {
mp.setProfileData( mp.setProfileData(
@@ -77,4 +77,20 @@
/* نسخه‌ی قرمز برای UNBLOCK */ /* نسخه‌ی قرمز برای UNBLOCK */
.button.footer-link-btn.danger { .button.footer-link-btn.danger {
-fx-text-fill: #D32F2F; -fx-text-fill: #D32F2F;
} }
/* فونت‌های ایموجی روی همهٔ سیستم‌ها */
.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";
}
@@ -1283,4 +1283,18 @@
.link-btn:hover { .link-btn:hover {
-fx-underline: true; -fx-underline: true;
} }
/* ===== 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;
}
@@ -1261,4 +1261,39 @@
.link-btn:hover { .link-btn:hover {
-fx-underline: true; -fx-underline: true;
} }
/* ===== 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;
}
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml"
styleClass="tf-root">
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram FAQ" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Label text="(frequently asked questions)" styleClass="tf-caption"/>
</children>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<Accordion>
<panes>
<TitledPane text="1) How do I create a new account?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Download the Telegram app, enter your phone number, and confirm it with the SMS code. Then you can set up your name and profile picture."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="2) How can I add a new contact?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap the menu, choose 'Contacts', then 'Add Contact'. Enter the phone number and name. Telegram will link it to their account automatically."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="3) How do I start a private chat?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open the contacts profile and tap 'Message'. This will create a private chat window with them."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="4) How can I create a group?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Go to the menu, select 'New Group', choose members, and set a group name and picture."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="5) How do channels work?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Channels let you broadcast messages to large audiences. Only admins can post, but everyone who subscribes receives the updates."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="6) How can I search for messages?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Use the search bar at the top of the chat list for global search, or use the search option inside a chat to find specific messages."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="7) How do I delete or edit a message?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Long-press (or right-click) on your message. Choose 'Edit' to change it or 'Delete' to remove it. You can delete for yourself or for everyone if allowed."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="8) How can I use reactions?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Tap and hold on a message, then choose an emoji reaction to express your feedback instantly."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="9) How do I archive chats?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Swipe left on a chat (mobile) or right-click on it (desktop) and choose 'Archive'. Archived chats are hidden in a separate folder until a new message arrives."/>
</children>
</VBox>
</content>
</TitledPane>
<TitledPane text="10) How can I enable Dark Mode?">
<content>
<VBox spacing="6" styleClass="tf-section">
<children>
<Label wrapText="true" styleClass="tf-item-desc"
text="Open 'Settings' → 'Appearance' and switch between Light and Dark themes. You can also schedule it automatically."/>
</children>
</VBox>
</content>
</TitledPane>
</panes>
</Accordion>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
@@ -0,0 +1,314 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml"
styleClass="tf-root">
<top>
<HBox spacing="10" styleClass="tf-toolbar">
<children>
<Label text="Telegram Features" styleClass="tf-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Label text="(static showcase)" styleClass="tf-caption"/>
</children>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED">
<content>
<Accordion>
<panes>
<!-- 1) Account & Authentication -->
<TitledPane text="1) Account &amp; Authentication">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Register, Login &amp; Logout" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create account, sign in/out, and manage sessions (devices, multi-login prevention)." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
<Label text="SECURITY" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Profile &amp; Presence" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Bio, photo, display name, user id, online / last seen." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 2) Contacts -->
<TitledPane text="2) Contacts">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Add / Remove Contacts" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Add, remove, and browse contacts with full profile view." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CONTACT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Block / Unblock" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Manage private chat access and visibility." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CONTACT" styleClass="tf-chip"/>
<Label text="SECURITY" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 3) Chats -->
<TitledPane text="3) Chats">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Private Chats" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create or open private chats (get_or_create_private_chat)." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Groups" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create groups, add members, roles &amp; permissions, ownership transfer." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
<Label text="ADMIN" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Channels" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Create channels, manage subscribers, admins, and ownership." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
<Label text="ADMIN" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Saved Messages" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Personal cloud chat to keep your own notes and files." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="GLOBAL" styleClass="tf-chip"/>
<Label text="MESSAGE" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Archive Chats" styleClass="tf-item-title"/>
<Label text="In Progress" styleClass="tf-status tf-status-progress"/>
</children>
</HBox>
<Label text="Move chats in/out of Archive for better organization." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children>
<Label text="CHAT" styleClass="tf-chip"/>
</children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
</children>
</VBox>
</content>
</TitledPane>
<!-- 4) Messaging -->
<TitledPane text="4) Messaging">
<content>
<VBox spacing="8" styleClass="tf-section">
<children>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Reply" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Quote and reply to a specific message." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children><Label text="MESSAGE" styleClass="tf-chip"/></children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<HBox spacing="10" styleClass="tf-item">
<children>
<VBox spacing="4" HBox.hgrow="ALWAYS">
<children>
<HBox spacing="8" alignment="CENTER_LEFT">
<children>
<Label text="Forward" styleClass="tf-item-title"/>
<Label text="Available" styleClass="tf-status tf-status-available"/>
</children>
</HBox>
<Label text="Send a message to another chat with source attribution." styleClass="tf-item-desc" wrapText="true"/>
<FlowPane hgap="6" vgap="6">
<children><Label text="MESSAGE" styleClass="tf-chip"/></children>
</FlowPane>
</children>
</VBox>
</children>
</HBox>
<!-- Add more items similarly: Edit, Delete, Reactions, Attachments, Read Receipts -->
</children>
</VBox>
</content>
</TitledPane>
<!-- You can add more TitledPane sections similarly (Search, UI/UX, Real-Time Events, Security). -->
</panes>
</Accordion>
</content>
</ScrollPane>
</center>
<padding>
<Insets top="8" right="8" bottom="8" left="8"/>
</padding>
</BorderPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB