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:
@@ -1,6 +1,9 @@
|
||||
package org.to.telegramfinalproject.Client;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Database.PrivateChatDatabase;
|
||||
@@ -9,6 +12,7 @@ import org.to.telegramfinalproject.Models.ChatEntry;
|
||||
import org.to.telegramfinalproject.Models.ContactEntry;
|
||||
import org.to.telegramfinalproject.Models.SearchRequestModel;
|
||||
import org.to.telegramfinalproject.Models.SearchResultModel;
|
||||
import org.to.telegramfinalproject.UI.ChatPageController;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
@@ -64,6 +68,10 @@ public class ActionHandler {
|
||||
}
|
||||
|
||||
|
||||
public static ActionHandler getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
|
||||
this.out = out;
|
||||
this.in = in;
|
||||
@@ -4229,38 +4237,38 @@ public class ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void sendMessageInteractive(UUID receiverId, String receiverType) {
|
||||
Scanner sc = new Scanner(System.in);
|
||||
|
||||
System.out.print("Type (TEXT / IMAGE / AUDIO): ");
|
||||
String type = sc.nextLine().trim().toUpperCase();
|
||||
while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) {
|
||||
System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): ");
|
||||
type = sc.nextLine().trim().toUpperCase();
|
||||
}
|
||||
|
||||
System.out.print("Text (optional for media; empty = no caption): ");
|
||||
String text = sc.nextLine();
|
||||
|
||||
if ("TEXT".equals(type)) {
|
||||
sendTextMessage(receiverId, receiverType, text);
|
||||
} else {
|
||||
System.out.print("File path: ");
|
||||
String path = sc.nextLine().trim();
|
||||
File f = new File(path);
|
||||
if (!f.isFile()) {
|
||||
System.out.println("❌ File not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sendMediaMessage(receiverId, receiverType, type, f, text);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("❌ Media send failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// public void sendMessageInteractive(UUID receiverId, String receiverType) {
|
||||
// Scanner sc = new Scanner(System.in);
|
||||
//
|
||||
// System.out.print("Type (TEXT / IMAGE / AUDIO): ");
|
||||
// String type = sc.nextLine().trim().toUpperCase();
|
||||
// while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) {
|
||||
// System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): ");
|
||||
// type = sc.nextLine().trim().toUpperCase();
|
||||
// }
|
||||
//
|
||||
// System.out.print("Text (optional for media; empty = no caption): ");
|
||||
// String text = sc.nextLine();
|
||||
//
|
||||
// if ("TEXT".equals(type)) {
|
||||
// sendTextMessage(receiverId, receiverType, text);
|
||||
// } else {
|
||||
// System.out.print("File path: ");
|
||||
// String path = sc.nextLine().trim();
|
||||
// File f = new File(path);
|
||||
// if (!f.isFile()) {
|
||||
// System.out.println("❌ File not found");
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// sendMediaMessage(receiverId, receiverType, type, f, text);
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// System.out.println("❌ Media send failed: " + e.getMessage());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private void sendTextMessage(UUID receiverId, String receiverType, String content) {
|
||||
JSONObject req = new JSONObject()
|
||||
@@ -4278,36 +4286,117 @@ public class ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) {
|
||||
// if (file == null) {
|
||||
// System.out.println("❌ File is null");
|
||||
// return;
|
||||
// }
|
||||
// if (!file.exists()) {
|
||||
// System.out.println("❌ File not found: " + file.getAbsolutePath());
|
||||
// return;
|
||||
// }
|
||||
// if (file.isDirectory()) {
|
||||
// System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath());
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// final UUID messageId = UUID.randomUUID();
|
||||
//
|
||||
// try {
|
||||
// String mime = detectMime(file, type.toUpperCase());
|
||||
// if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*";
|
||||
//
|
||||
// JSONObject header = new JSONObject()
|
||||
// .put("message_id", messageId.toString())
|
||||
// .put("sender_id", Session.getUserUUID())
|
||||
// .put("receiver_type", receiverType) // private|group|channel
|
||||
// .put("receiver_id", receiverId.toString())
|
||||
// .put("message_type", type.toUpperCase()) // IMAGE | AUDIO
|
||||
// .put("file_name", file.getName())
|
||||
// .put("mime_type", mime)
|
||||
// .put("text", caption == null ? "" : caption);
|
||||
//
|
||||
// byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
// long contentLen = file.length();
|
||||
//
|
||||
// BlockingQueue<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 {
|
||||
String mime = detectMime(file, type.toUpperCase());
|
||||
if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*";
|
||||
|
||||
JSONObject header = new JSONObject()
|
||||
.put("message_id", messageId.toString())
|
||||
.put("sender_id", TelegramClient.loggedInUserId.toString())
|
||||
.put("receiver_type", receiverType) // private|group|channel
|
||||
.put("receiver_id", receiverId.toString())
|
||||
.put("message_type", type.toUpperCase()) // IMAGE | AUDIO
|
||||
.put("file_name", file.getName())
|
||||
.put("mime_type", mime)
|
||||
.put("text", caption == null ? "" : caption);
|
||||
.put("message_id", messageId.toString())
|
||||
.put("sender_id", Session.getUserUUID()) // همون UUID کاربر
|
||||
.put("receiver_type",receiverType)
|
||||
.put("receiver_id", receiverId.toString())
|
||||
.put("message_type", type.toUpperCase())
|
||||
.put("file_name", file.getName())
|
||||
.put("mime_type", mime)
|
||||
.put("text", caption == null ? "" : caption);
|
||||
|
||||
byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
long contentLen = file.length();
|
||||
@@ -4316,15 +4405,13 @@ public class ActionHandler {
|
||||
TelegramClient.pendingResponses.put(messageId.toString(), q);
|
||||
|
||||
try {
|
||||
|
||||
outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
outBin.flush();
|
||||
|
||||
// 2) binary frame: magic + headerLen + header + contentLen + content
|
||||
outBin.writeInt(0x4D444D31); // "MDM1"
|
||||
outBin.writeInt(headerBytes.length); // headerLen (int)
|
||||
outBin.write(headerBytes); // header
|
||||
outBin.writeLong(contentLen); // contentLen (long)
|
||||
outBin.writeInt(0x4D444D31); // "MDM1"
|
||||
outBin.writeInt(headerBytes.length);
|
||||
outBin.write(headerBytes);
|
||||
outBin.writeLong(contentLen);
|
||||
|
||||
try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) {
|
||||
byte[] buf = new byte[8192];
|
||||
@@ -4338,15 +4425,23 @@ public class ActionHandler {
|
||||
JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (ack == null) {
|
||||
System.out.println("❌ Media ACK timeout for " + messageId);
|
||||
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
|
||||
messageId.toString(), "Failed (timeout)"));
|
||||
return;
|
||||
}
|
||||
|
||||
String status = ack.optString("status", "error");
|
||||
if ("success".equalsIgnoreCase(status)) {
|
||||
System.out.println("✅ Media sent. id=" + ack.optString("message_id") +
|
||||
" url=" + ack.optString("file_url"));
|
||||
// سرور شما فیلد نمایش را با نام display_path میفرستد (نه file_url)
|
||||
String url = ack.optString("display_path", ack.optString("file_url", ""));
|
||||
System.out.println("✅ Media sent. id=" + ack.optString("message_id") + " url=" + url);
|
||||
|
||||
// بابل Pending را بردار (بلافاصله؛ پیام واقعی هم بعداً با real-time میآید)
|
||||
Platform.runLater(() -> ChatPageController.get().removePendingBubble(messageId.toString()));
|
||||
} else {
|
||||
System.out.println("❌ Media failed: " + ack.optString("message"));
|
||||
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
|
||||
messageId.toString(), "Failed to send"));
|
||||
}
|
||||
|
||||
} finally {
|
||||
@@ -4356,6 +4451,8 @@ public class ActionHandler {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("❌ sendMediaMessage error: " + e.getMessage());
|
||||
Platform.runLater(() -> ChatPageController.get().updatePendingStatus(
|
||||
messageId.toString(), "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4627,4 +4724,7 @@ public class ActionHandler {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
String mt = m.getMessage_type();
|
||||
if (mt != null && !mt.equalsIgnoreCase("TEXT")) {
|
||||
String fileUrl = MessageDatabase.getFirstAttachmentUrlByType(m.getMessage_id(), mt);
|
||||
if (fileUrl != null && !fileUrl.isBlank()) {
|
||||
obj.put("file_url", fileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray reactionsArr = new JSONArray();
|
||||
try {
|
||||
List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id());
|
||||
|
||||
@@ -7,10 +7,15 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Circle;
|
||||
@@ -117,6 +122,15 @@ public class ChatPageController {
|
||||
private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده
|
||||
private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده
|
||||
private final Map<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;
|
||||
@@ -156,6 +170,12 @@ public class ChatPageController {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static ChatPageController get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void initCurrentUserId() {
|
||||
try {
|
||||
String meStr = org.to.telegramfinalproject.Client.Session
|
||||
@@ -668,7 +688,10 @@ public class ChatPageController {
|
||||
fMessageId, // message_id
|
||||
"", "", "", // forwarded_from, forwarded_by, reply_to_id
|
||||
false, // edited
|
||||
null // reactions
|
||||
null, // reactions
|
||||
null,
|
||||
null
|
||||
|
||||
);
|
||||
|
||||
// آپدیت پیشنمایش لیست چتها
|
||||
@@ -691,15 +714,170 @@ public class ChatPageController {
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
private void openFileChooser() {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Select a file to send");
|
||||
fc.setTitle("Select image or audio");
|
||||
fc.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("Images", "*.png","*.jpg","*.jpeg","*.gif","*.bmp","*.webp"),
|
||||
new FileChooser.ExtensionFilter("Audio", "*.mp3","*.wav","*.m4a","*.ogg","*.aac")
|
||||
);
|
||||
|
||||
File file = fc.showOpenDialog(attachmentButton.getScene().getWindow());
|
||||
if (file != null) {
|
||||
System.out.println("Selected file: " + file.getAbsolutePath());
|
||||
// TODO: actually send file
|
||||
addSystemMessage("Attached file: " + file.getName());
|
||||
if (file == null) return;
|
||||
|
||||
String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO"
|
||||
if (type == null) { toast("Only image or audio"); return; }
|
||||
|
||||
if (currentChat == null) { toast("Not available chat"); return; }
|
||||
UUID receiverId = currentChat.getId(); // همون chat_id
|
||||
String receiverType = currentChat.getType(); // "private" | "group" | "channel"
|
||||
|
||||
String caption = (messageInput != null) ? messageInput.getText().trim() : "";
|
||||
if (messageInput != null) messageInput.clear();
|
||||
|
||||
// 1) message_id را همینجا بساز تا Pending به همین ID وصل شود
|
||||
UUID messageId = UUID.randomUUID();
|
||||
|
||||
// 2) حباب Pending (نسخهای که messageId میگیرد)
|
||||
addPendingMediaBubble(messageId.toString(), file, type, caption);
|
||||
|
||||
// 3) ارسال واقعی با همین messageId
|
||||
new Thread(() -> {
|
||||
ActionHandler ah = ActionHandler.getInstance();
|
||||
ah.sendMediaMessage(messageId, receiverId, receiverType, type, file, caption);
|
||||
// اگر ACK success برگشت، خود ActionHandler میتونه removePendingBubble(messageId) صدا بزنه
|
||||
// وگرنه در onRealTimeNewMessage که پیام واقعی آمد، پاک میکنیم (کد آن را قبلاً دادم).
|
||||
}, "Media-Uploader").start();
|
||||
}
|
||||
|
||||
/** حدس نوع فایل: IMAGE یا AUDIO */
|
||||
private String guessType(File f) {
|
||||
String name = f.getName().toLowerCase();
|
||||
if (name.matches(".*\\.(png|jpg|jpeg|gif|bmp|webp)$")) return "IMAGE";
|
||||
if (name.matches(".*\\.(mp3|wav|m4a|ogg|aac)$")) return "AUDIO";
|
||||
// اگر خواستی دقیقتر: با Files.probeContentType هم تست کن
|
||||
return null;
|
||||
}
|
||||
|
||||
// /** ساخت یک حباب «درحال ارسال…» */
|
||||
// private HBox addPendingMediaBubble(File file, String type, String caption) {
|
||||
// HBox root = new HBox(8);
|
||||
// root.getStyleClass().add("bubble-outgoing"); // استایل دلخواهت
|
||||
// root.setFillHeight(true);
|
||||
//
|
||||
// ImageView iv = null;
|
||||
// if ("IMAGE".equalsIgnoreCase(type)) {
|
||||
// iv = new ImageView(new Image(file.toURI().toString(), 360, 360, true, true, true));
|
||||
// iv.setPreserveRatio(true);
|
||||
// iv.setFitWidth(240); // سایز معقول برای Pending
|
||||
// iv.setFitHeight(240);
|
||||
// root.getChildren().add(iv);
|
||||
// } else if ("AUDIO".equalsIgnoreCase(type)) {
|
||||
// // برای صدا یک آیکون ساده و نام فایل
|
||||
// ImageView icon = new ImageView(); // اگر آیکون داری اینجا بگذار
|
||||
// icon.setFitWidth(24); icon.setFitHeight(24);
|
||||
// Label name = new Label(file.getName());
|
||||
// HBox audioBox = new HBox(6, icon, name);
|
||||
// root.getChildren().add(audioBox);
|
||||
// }
|
||||
//
|
||||
// VBox right = new VBox(4);
|
||||
// if (caption != null && !caption.isBlank()) {
|
||||
// Label cap = new Label(caption);
|
||||
// cap.getStyleClass().add("msg-caption");
|
||||
// cap.setWrapText(true);
|
||||
// right.getChildren().add(cap);
|
||||
// }
|
||||
//
|
||||
// HBox statusRow = new HBox(6);
|
||||
// ProgressIndicator spinner = new ProgressIndicator();
|
||||
// spinner.setPrefSize(16, 16);
|
||||
// Label status = new Label("Sending...");
|
||||
// status.getStyleClass().add("msg-status");
|
||||
// Region spacer = new Region();
|
||||
// HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
// statusRow.getChildren().addAll(spinner, status, spacer);
|
||||
//
|
||||
// right.getChildren().add(statusRow);
|
||||
// root.getChildren().add(right);
|
||||
//
|
||||
// messageContainer.getChildren().add(root);
|
||||
// pendingBubbles.addLast(root);
|
||||
//
|
||||
// return root;
|
||||
// }
|
||||
//
|
||||
// /** وقتی پیام واقعی (از خودِ کاربر) برای همین چت رسید، یکی از Pendingها را حذف کن. */
|
||||
// public void removeOnePendingBubble() {
|
||||
// HBox node = pendingBubbles.pollFirst();
|
||||
// if (node != null) {
|
||||
// messageContainer.getChildren().remove(node);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
private HBox addPendingMediaBubble(String messageId, File file, String type, String caption) {
|
||||
HBox root = new HBox(8);
|
||||
root.setFillHeight(true);
|
||||
root.setAlignment(Pos.CENTER_RIGHT); // چون outgoing است
|
||||
|
||||
// preview
|
||||
if ("IMAGE".equalsIgnoreCase(type)) {
|
||||
ImageView iv = new ImageView(new Image(file.toURI().toString(), 240, 240, true, true, true));
|
||||
iv.setPreserveRatio(true);
|
||||
iv.setFitWidth(240);
|
||||
iv.setFitHeight(240);
|
||||
root.getChildren().add(iv);
|
||||
} else if ("AUDIO".equalsIgnoreCase(type)) {
|
||||
Label name = new Label(file.getName());
|
||||
root.getChildren().add(new HBox(6, new Label("🎵"), name));
|
||||
}
|
||||
|
||||
VBox right = new VBox(4);
|
||||
if (caption != null && !caption.isBlank()) {
|
||||
Label cap = new Label(caption);
|
||||
cap.setWrapText(true);
|
||||
right.getChildren().add(cap);
|
||||
}
|
||||
|
||||
HBox statusRow = new HBox(6);
|
||||
ProgressIndicator spinner = new ProgressIndicator();
|
||||
spinner.setPrefSize(14, 14);
|
||||
Label status = new Label("Sending...");
|
||||
status.getProperties().put("role", "statusLabel"); // برای آپدیت بعدی
|
||||
statusRow.getChildren().addAll(spinner, status);
|
||||
right.getChildren().add(statusRow);
|
||||
|
||||
root.getChildren().add(right);
|
||||
|
||||
// برچسب messageId روی نود
|
||||
if (messageId != null) root.getProperties().put("messageId", messageId);
|
||||
|
||||
messageContainer.getChildren().add(root);
|
||||
|
||||
// ثبت در مپ/صف
|
||||
if (messageId != null) pendingById.put(messageId, root);
|
||||
pendingBubbles.addLast(root);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
public void removePendingBubble(String messageId) {
|
||||
if (messageId == null) return;
|
||||
HBox node = pendingById.remove(messageId);
|
||||
if (node != null) {
|
||||
pendingBubbles.remove(node);
|
||||
messageContainer.getChildren().remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void toast(String msg) {
|
||||
// هر جور که خودت نوتیف/Toast داری
|
||||
System.out.println("ℹ️ " + msg);
|
||||
if (messageInput != null) {
|
||||
messageInput.setTooltip(new Tooltip(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1311,6 +1489,58 @@ public class ChatPageController {
|
||||
|
||||
private final java.util.Map<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) {
|
||||
messageContainer.getChildren().clear();
|
||||
|
||||
@@ -1331,10 +1561,14 @@ public class ChatPageController {
|
||||
String senderId = str(m, "sender_id");
|
||||
String senderName = str(m, "sender_name");
|
||||
String type = str(m, "message_type");
|
||||
String content = str(m, "content");
|
||||
String content = str(m, "content"); // این همون کپشنه برای IMAGE
|
||||
String whenStr = str(m, "send_at");
|
||||
String msgId = str(m, "message_id");
|
||||
|
||||
// 👇 جدید: URL ها
|
||||
String fileUrl = str(m, "file_url");
|
||||
String thumbUrl = str(m, "thumb_url");
|
||||
|
||||
// فوروارد / ریپلای / ادیت / ریاکشن
|
||||
String fwdFrom = nz(str(m, "forwarded_from"));
|
||||
String fwdBy = nz(str(m, "forwarded_by"));
|
||||
@@ -1352,17 +1586,17 @@ public class ChatPageController {
|
||||
|
||||
java.time.LocalDateTime ts = parseWhen(whenStr);
|
||||
|
||||
// نمایش
|
||||
// 👇 امضای جدید addBubble با fileUrl/thumbUrl
|
||||
addBubble(outgoing, senderName, type, content, ts, msgId,
|
||||
fwdFrom, fwdBy, replyToId, edited, reactions);
|
||||
fwdFrom, fwdBy, replyToId, edited, reactions, fileUrl, thumbUrl);
|
||||
}
|
||||
|
||||
// کمی فاصله بین پیامها
|
||||
messageContainer.setSpacing(8);
|
||||
messageScrollPane.layout();
|
||||
messageScrollPane.setVvalue(1.0);
|
||||
}
|
||||
|
||||
|
||||
private String shortId(String id){
|
||||
return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length()));
|
||||
}
|
||||
@@ -1501,104 +1735,216 @@ public class ChatPageController {
|
||||
|
||||
|
||||
|
||||
// private void addBubble(
|
||||
// boolean outgoing,
|
||||
// String displayName,
|
||||
// String type,
|
||||
// String content,
|
||||
// java.time.LocalDateTime sentAt,
|
||||
// String messageId,
|
||||
// String forwardedFrom,
|
||||
// String forwardedBy,
|
||||
// String replyToId,
|
||||
// boolean edited,
|
||||
// org.json.JSONArray reactions
|
||||
// ) {
|
||||
// // === Meta (نام + زمان) ===
|
||||
// String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt);
|
||||
// if (edited) metaText += " (edited)";
|
||||
// Label meta = new Label(metaText);
|
||||
// meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
|
||||
// meta.setWrapText(true);
|
||||
// // برچسب برای آپدیتهای بعدی (edit)
|
||||
// meta.getProperties().put("role", "metaLabel");
|
||||
//
|
||||
// // === متن/نوع پیام ===
|
||||
// String t = type == null ? "" : type.trim().toUpperCase();
|
||||
// boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
|
||||
// String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
|
||||
//
|
||||
// Label msg = new Label(bodyText);
|
||||
// msg.setWrapText(true);
|
||||
// msg.setMinHeight(Region.USE_PREF_SIZE);
|
||||
// // برچسب برای آپدیتهای بعدی (edit)
|
||||
// msg.getProperties().put("role", "msgLabel");
|
||||
//
|
||||
// // === رنگ بابلها
|
||||
// boolean dark = themeManager.isDarkMode();
|
||||
// String mine = dark ? "#2b7cff" : "#d8ecff"; // outgoing (من)
|
||||
// String theirs = dark ? "#2c333a" : "#f2f4f7"; // incoming (خیلی روشن بهجای سفید)
|
||||
// String bg = outgoing ? mine : theirs;
|
||||
//
|
||||
// msg.setStyle(
|
||||
// "-fx-background-color:" + bg + ";" +
|
||||
// "-fx-padding:8 12;" +
|
||||
// "-fx-background-radius:12;" +
|
||||
// "-fx-max-width: 520;"
|
||||
// );
|
||||
//
|
||||
// // === بدنهی بابل ===
|
||||
// VBox bubble = new VBox(4);
|
||||
// bubble.getChildren().add(meta);
|
||||
//
|
||||
// // برچسبگذاری بابل برای پیدا کردنش در آپدیتهای realtime
|
||||
// if (messageId != null && !messageId.isBlank()) {
|
||||
// bubble.getProperties().put("messageId", messageId);
|
||||
// }
|
||||
//
|
||||
// // Forward header (اختیاری)
|
||||
// if (hasVal(forwardedFrom) || hasVal(forwardedBy)) {
|
||||
// bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy));
|
||||
// }
|
||||
//
|
||||
// // Reply preview (اختیاری)
|
||||
// if (hasVal(replyToId)) {
|
||||
// bubble.getChildren().add(buildReplyBoxFromIndex(replyToId));
|
||||
// }
|
||||
//
|
||||
// // متن اصلی
|
||||
// bubble.getChildren().add(msg);
|
||||
//
|
||||
// // Reactions (اختیاری) + برچسب برای تعویض سریع در ریِلتایم
|
||||
// if (reactions != null && reactions.length() > 0) {
|
||||
// Node rxBar = buildReactionsBarFromJson(reactions, dark);
|
||||
// rxBar.getProperties().put("role", "reactionsBar");
|
||||
// bubble.getChildren().add(rxBar);
|
||||
// }
|
||||
//
|
||||
// // === ردیف چیدمان راست/چپ ===
|
||||
// HBox row = new HBox(bubble);
|
||||
// row.setFillHeight(true);
|
||||
// row.setSpacing(4);
|
||||
// row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);
|
||||
// row.setPadding(new Insets(2, 6, 2, 6));
|
||||
//
|
||||
// // اضافه به کانتینر
|
||||
// messageContainer.getChildren().add(row);
|
||||
//
|
||||
// // ایندکس نود برای آپدیت/حذف realtime
|
||||
// if (messageId != null && !messageId.isBlank()) {
|
||||
// messageNodes.put(messageId, row);
|
||||
// }
|
||||
//
|
||||
// boolean isMine = outgoing;
|
||||
//
|
||||
// // منوی راستکلیک/کلیک (بدون تغییر در ساختار کدت)
|
||||
// ContextMenu menu = buildMessageMenu(isMine, messageId, type, content);
|
||||
// row.setOnContextMenuRequested(ev -> {
|
||||
// menu.show(row, ev.getScreenX(), ev.getScreenY());
|
||||
// ev.consume();
|
||||
// });
|
||||
// row.setOnMouseClicked(ev -> {
|
||||
// if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) {
|
||||
// menu.show(row, ev.getScreenX(), ev.getScreenY());
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
private void addBubble(
|
||||
boolean outgoing,
|
||||
String displayName,
|
||||
String type,
|
||||
String content,
|
||||
String content, // برای IMAGE = کپشن
|
||||
java.time.LocalDateTime sentAt,
|
||||
String messageId,
|
||||
String forwardedFrom,
|
||||
String forwardedBy,
|
||||
String replyToId,
|
||||
boolean edited,
|
||||
org.json.JSONArray reactions
|
||||
org.json.JSONArray reactions,
|
||||
String fileUrl, // 👈 جدید
|
||||
String thumbUrl // 👈 جدید
|
||||
) {
|
||||
// === Meta (نام + زمان) ===
|
||||
String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt);
|
||||
if (edited) metaText += " (edited)";
|
||||
Label meta = new Label(metaText);
|
||||
meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
|
||||
meta.setWrapText(true);
|
||||
// برچسب برای آپدیتهای بعدی (edit)
|
||||
meta.getProperties().put("role", "metaLabel");
|
||||
|
||||
// === متن/نوع پیام ===
|
||||
String t = type == null ? "" : type.trim().toUpperCase();
|
||||
boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
|
||||
String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
|
||||
|
||||
Label msg = new Label(bodyText);
|
||||
msg.setWrapText(true);
|
||||
msg.setMinHeight(Region.USE_PREF_SIZE);
|
||||
// برچسب برای آپدیتهای بعدی (edit)
|
||||
msg.getProperties().put("role", "msgLabel");
|
||||
|
||||
// === رنگ بابلها
|
||||
// رنگ پسزمینه برای متن (برای عکس پسزمینه نمیذاریم تا تمیز باشه)
|
||||
boolean dark = themeManager.isDarkMode();
|
||||
String mine = dark ? "#2b7cff" : "#d8ecff"; // outgoing (من)
|
||||
String theirs = dark ? "#2c333a" : "#f2f4f7"; // incoming (خیلی روشن بهجای سفید)
|
||||
String bg = outgoing ? mine : theirs;
|
||||
String mine = dark ? "#2b7cff" : "#d8ecff";
|
||||
String theirs = dark ? "#2c333a" : "#f2f4f7";
|
||||
|
||||
msg.setStyle(
|
||||
"-fx-background-color:" + bg + ";" +
|
||||
"-fx-padding:8 12;" +
|
||||
"-fx-background-radius:12;" +
|
||||
"-fx-max-width: 520;"
|
||||
);
|
||||
|
||||
// === بدنهی بابل ===
|
||||
VBox bubble = new VBox(4);
|
||||
bubble.getChildren().add(meta);
|
||||
|
||||
// برچسبگذاری بابل برای پیدا کردنش در آپدیتهای realtime
|
||||
if (messageId != null && !messageId.isBlank()) {
|
||||
bubble.getProperties().put("messageId", messageId);
|
||||
}
|
||||
|
||||
// Forward header (اختیاری)
|
||||
if (hasVal(forwardedFrom) || hasVal(forwardedBy)) {
|
||||
bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy));
|
||||
}
|
||||
|
||||
// Reply preview (اختیاری)
|
||||
if (hasVal(replyToId)) {
|
||||
bubble.getChildren().add(buildReplyBoxFromIndex(replyToId));
|
||||
}
|
||||
|
||||
// متن اصلی
|
||||
bubble.getChildren().add(msg);
|
||||
if (isText) {
|
||||
// متن
|
||||
String bodyText = content == null ? "" : content;
|
||||
Label msg = new Label(bodyText);
|
||||
msg.setWrapText(true);
|
||||
msg.setMinHeight(Region.USE_PREF_SIZE);
|
||||
msg.getProperties().put("role", "msgLabel");
|
||||
String bg = outgoing ? mine : theirs;
|
||||
msg.setStyle("-fx-background-color:" + bg + ";" +
|
||||
"-fx-padding:8 12;" +
|
||||
"-fx-background-radius:12;" +
|
||||
"-fx-max-width: 520;");
|
||||
bubble.getChildren().add(msg);
|
||||
|
||||
} else if ("IMAGE".equals(t)) {
|
||||
// 👇 نمایش تصویر از روی URL سرور + کپشن اختیاری
|
||||
Node imageNode = buildImageNode(fileUrl, thumbUrl, content);
|
||||
bubble.getChildren().add(imageNode);
|
||||
|
||||
} else if ("AUDIO".equals(t)) {
|
||||
// میتونی بعداً کاملش کنی
|
||||
Label ph = new Label("🎵 Audio");
|
||||
ph.setWrapText(true);
|
||||
String bg = outgoing ? mine : theirs;
|
||||
ph.setStyle("-fx-background-color:" + bg + ";" +
|
||||
"-fx-padding:8 12;" +
|
||||
"-fx-background-radius:12;" +
|
||||
"-fx-max-width: 520;");
|
||||
bubble.getChildren().add(ph);
|
||||
|
||||
} else {
|
||||
// ناشناخته
|
||||
Label ph = new Label("[" + t + "]");
|
||||
ph.setWrapText(true);
|
||||
String bg = outgoing ? mine : theirs;
|
||||
ph.setStyle("-fx-background-color:" + bg + ";" +
|
||||
"-fx-padding:8 12;" +
|
||||
"-fx-background-radius:12;" +
|
||||
"-fx-max-width: 520;");
|
||||
bubble.getChildren().add(ph);
|
||||
}
|
||||
|
||||
// Reactions (اختیاری) + برچسب برای تعویض سریع در ریِلتایم
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
Node rxBar = buildReactionsBarFromJson(reactions, dark);
|
||||
rxBar.getProperties().put("role", "reactionsBar");
|
||||
bubble.getChildren().add(rxBar);
|
||||
}
|
||||
|
||||
// === ردیف چیدمان راست/چپ ===
|
||||
HBox row = new HBox(bubble);
|
||||
row.setFillHeight(true);
|
||||
row.setSpacing(4);
|
||||
row.setAlignment(outgoing ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);
|
||||
row.setPadding(new Insets(2, 6, 2, 6));
|
||||
|
||||
// اضافه به کانتینر
|
||||
messageContainer.getChildren().add(row);
|
||||
|
||||
// ایندکس نود برای آپدیت/حذف realtime
|
||||
if (messageId != null && !messageId.isBlank()) {
|
||||
messageNodes.put(messageId, row);
|
||||
}
|
||||
|
||||
boolean isMine = outgoing;
|
||||
|
||||
// منوی راستکلیک/کلیک (بدون تغییر در ساختار کدت)
|
||||
ContextMenu menu = buildMessageMenu(isMine, messageId, type, content);
|
||||
row.setOnContextMenuRequested(ev -> {
|
||||
menu.show(row, ev.getScreenX(), ev.getScreenY());
|
||||
ev.consume();
|
||||
});
|
||||
row.setOnContextMenuRequested(ev -> { menu.show(row, ev.getScreenX(), ev.getScreenY()); ev.consume(); });
|
||||
row.setOnMouseClicked(ev -> {
|
||||
if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY && ev.getClickCount() == 1) {
|
||||
menu.show(row, ev.getScreenX(), ev.getScreenY());
|
||||
@@ -1606,12 +1952,96 @@ public class ChatPageController {
|
||||
});
|
||||
}
|
||||
|
||||
private Node buildImageNode(String fileUrl, String thumbUrl, String caption) {
|
||||
String url = hasVal(fileUrl) ? absolute(fileUrl) : null;
|
||||
if (!hasVal(url)) return new Label("❌ Image not available");
|
||||
|
||||
ImageView iv = new ImageView();
|
||||
iv.setPreserveRatio(true);
|
||||
iv.setSmooth(true);
|
||||
iv.setFitWidth(360);
|
||||
iv.setFitHeight(360);
|
||||
|
||||
Image img = new Image(url, 360, 360, true, true, true);
|
||||
iv.setImage(img);
|
||||
|
||||
img.errorProperty().addListener((obs, wasErr, isErr) -> {
|
||||
if (isErr) {
|
||||
System.err.println("⚠️ Image load failed: " + url + " | ex=" + img.getException());
|
||||
Label err = new Label("❌ Failed to load image");
|
||||
VBox fallback = new VBox(4, err);
|
||||
Platform.runLater(() -> {
|
||||
if (iv.getParent() instanceof VBox v) {
|
||||
int idx = v.getChildren().indexOf(iv);
|
||||
if (idx >= 0) v.getChildren().set(idx, fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
iv.setOnMouseClicked(e -> openImagePreviewDialog(url));
|
||||
|
||||
VBox box = new VBox(6, iv);
|
||||
if (hasVal(caption)) {
|
||||
Label cap = new Label(caption);
|
||||
cap.setWrapText(true);
|
||||
cap.setStyle("-fx-padding:6 8; -fx-background-radius:10; -fx-background-color: transparent; -fx-max-width: 520;");
|
||||
box.getChildren().add(cap);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void openImagePreviewDialog(String fullUrl) {
|
||||
if (fullUrl == null || fullUrl.isBlank()) return;
|
||||
|
||||
// IMPORTANT: fullUrl همین حالا absolute است؛ دوباره absolute(...) نزن
|
||||
String url = fullUrl;
|
||||
|
||||
ImageView iv = new ImageView(new Image(url, true));
|
||||
iv.setPreserveRatio(true);
|
||||
|
||||
ScrollPane sp = new ScrollPane(iv);
|
||||
sp.setPannable(true);
|
||||
sp.setFitToWidth(true);
|
||||
sp.setFitToHeight(true);
|
||||
|
||||
Stage st = new Stage();
|
||||
st.setTitle("Preview");
|
||||
st.initOwner(attachmentButton.getScene().getWindow());
|
||||
st.setScene(new Scene(sp, 900, 700));
|
||||
st.addEventHandler(KeyEvent.KEY_PRESSED, e -> { if (e.getCode() == KeyCode.ESCAPE) st.close(); });
|
||||
st.show();
|
||||
}
|
||||
|
||||
|
||||
private String absolute(String pathOrUrl) {
|
||||
if (pathOrUrl == null || pathOrUrl.isBlank()) return null;
|
||||
if (pathOrUrl.startsWith("http")) return pathOrUrl;
|
||||
|
||||
// اگر نسبی است مثل /images/2025-09-06/xxx.jpg یا /audios/...
|
||||
String rel = pathOrUrl.startsWith("/") ? pathOrUrl.substring(1) : pathOrUrl;
|
||||
|
||||
// حالت لوکال: file://
|
||||
java.nio.file.Path p = java.nio.file.Paths.get(UPLOADS_DIR, rel.replace("/", java.io.File.separator));
|
||||
java.net.URI uri = p.toUri(); // میشود file:///C:/Users/.../uploads/images/...
|
||||
return uri.toString();
|
||||
|
||||
// اگر خواستی از HTTP بخوانی، بهجای return بالا این را برگردان:
|
||||
// return HTTP_BASE + (pathOrUrl.startsWith("/") ? pathOrUrl : "/" + pathOrUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private ContextMenu buildMessageMenu(boolean isMine, String messageId, String type, String content) {
|
||||
ContextMenu menu = new ContextMenu();
|
||||
|
||||
// --- 2.1 نوار ریکشن بالای منو (مثل تلگرام) ---
|
||||
HBox reactions = new HBox(8);
|
||||
String[] emojis = {"👍","👎","😂","😭","🕊️","⚡"};
|
||||
String[] emojis = {"👍","👎","😂","😭","⚡"};
|
||||
for (String e : emojis) {
|
||||
Button b = new Button(e);
|
||||
b.getStyleClass().add("reaction-btn");
|
||||
@@ -1724,32 +2154,26 @@ public class ChatPageController {
|
||||
}
|
||||
|
||||
|
||||
private javafx.scene.Node buildReactionsBarFromJson(org.json.JSONArray arr, boolean dark) {
|
||||
javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(6);
|
||||
bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
org.json.JSONObject ro = arr.optJSONObject(i);
|
||||
if (ro == null) continue;
|
||||
String emoji = ro.optString("emoji", "👍");
|
||||
int count = ro.optInt("count", 1);
|
||||
boolean byMe = ro.optBoolean("by_me", false);
|
||||
private Node buildReactionsBarFromJson(org.json.JSONArray reactions, boolean dark) {
|
||||
HBox bar = new HBox(6);
|
||||
for (int i = 0; i < reactions.length(); i++) {
|
||||
var r = reactions.getJSONObject(i);
|
||||
String emo = r.optString("emoji", "👍"); // 👈 باید کاراکتر واقعی باشه
|
||||
int cnt = r.optInt("count", 1);
|
||||
|
||||
Label chip = new Label(emo + " " + cnt);
|
||||
chip.getStyleClass().add("emoji-label"); // 👈 کلاس CSS برای ایموجی
|
||||
chip.setStyle("-fx-background-color:" + (dark ? "#39424a" : "#e9eef3") +
|
||||
"; -fx-padding:3 8; -fx-background-radius:12;");
|
||||
|
||||
String chipBg = byMe ? (dark ? "#215a9f" : "#d1e8ff")
|
||||
: (dark ? "#2f3942" : "#eef3f7");
|
||||
String text = emoji + (count > 0 ? (" " + count) : "");
|
||||
Label chip = new Label(text);
|
||||
chip.setStyle(
|
||||
"-fx-font-size: 12;" +
|
||||
"-fx-background-radius: 12;" +
|
||||
"-fx-padding: 2 8;" +
|
||||
"-fx-background-color: " + chipBg + ";"
|
||||
);
|
||||
bar.getChildren().add(chip);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "…" : s; }
|
||||
|
||||
public boolean isSameChat(UUID chatId, String type) {
|
||||
@@ -1799,56 +2223,209 @@ public class ChatPageController {
|
||||
// }
|
||||
|
||||
|
||||
public void onRealTimeNewMessage(JSONObject m) {
|
||||
// public void onRealTimeNewMessage(JSONObject m) {
|
||||
// try {
|
||||
// String chatIdStr = str(m,"receiver_id");
|
||||
// String chatType = str(m,"receiver_type");
|
||||
// if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
|
||||
//
|
||||
// UUID chatId = UUID.fromString(chatIdStr);
|
||||
// boolean isCurrent = isSameChat(chatId, chatType);
|
||||
//
|
||||
// // id → message_id fallback
|
||||
// if (!m.has("message_id") && m.has("id")) {
|
||||
// m.put("message_id", m.getString("id"));
|
||||
// }
|
||||
// String msgId = str(m,"message_id");
|
||||
// if (!hasVal(msgId)) return;
|
||||
//
|
||||
// // ✅ اگر قبلاً همین پیام داخل UI اضافه شده، دیگه دوباره نساز
|
||||
// if (messageNodes.containsKey(msgId)) return;
|
||||
//
|
||||
// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
|
||||
// : (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown");
|
||||
//
|
||||
// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
|
||||
// String content = str(m,"content");
|
||||
// String whenIso = str(m,"send_at");
|
||||
//
|
||||
// String fwdFrom = str(m,"forwarded_from");
|
||||
// String fwdBy = str(m,"forwarded_by");
|
||||
// String replyTo = str(m,"reply_to_id");
|
||||
// boolean edited = bool(m,"is_edited");
|
||||
// JSONArray reacts = arr(m,"reactions");
|
||||
//
|
||||
// LocalDateTime ts = parseWhen(whenIso);
|
||||
// if (ts == null) ts = LocalDateTime.now();
|
||||
//
|
||||
// // ایندکس برای ریپلای/ادیت/ریاکشنهای بعدی
|
||||
// msgIndex.put(msgId, m);
|
||||
//
|
||||
// // آپدیت لیست چتها (پریویو)
|
||||
// boolean incoming = true; // از سرور آمده → ورودی
|
||||
// updateChatListPreview(chatId, chatType, incoming, content, type);
|
||||
//
|
||||
// // اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد
|
||||
// if (!isCurrent) return;
|
||||
//
|
||||
// // اضافه کردن حباب بدون رفرش
|
||||
// addBubble(false, senderName, type, content, ts, msgId,
|
||||
// fwdFrom, fwdBy, replyTo, edited, reacts);
|
||||
//
|
||||
// // خوانده شد (در صورت نیاز)
|
||||
// if (currentChat != null) markAsRead(currentChat);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// public void onRealTimeNewMessage(JSONObject m) {
|
||||
// try {
|
||||
// String chatIdStr = str(m,"receiver_id");
|
||||
// String chatType = str(m,"receiver_type");
|
||||
// if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
|
||||
//
|
||||
// UUID chatId = UUID.fromString(chatIdStr);
|
||||
// boolean isCurrent = isSameChat(chatId, chatType);
|
||||
//
|
||||
// // id → message_id fallback
|
||||
// if (!m.has("message_id") && m.has("id")) {
|
||||
// m.put("message_id", m.getString("id"));
|
||||
// }
|
||||
// String msgId = str(m,"message_id");
|
||||
// if (!hasVal(msgId)) return;
|
||||
//
|
||||
// // اگر قبلاً تو UI هست، دوباره نساز
|
||||
// if (messageNodes.containsKey(msgId)) return;
|
||||
//
|
||||
// String senderId = str(m,"sender_id");
|
||||
// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
|
||||
// : (hasVal(senderId) ? shortId(senderId) : "Unknown");
|
||||
//
|
||||
// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
|
||||
// String content = str(m,"content"); // برای IMAGE = کپشن
|
||||
// String whenIso = str(m,"send_at");
|
||||
//
|
||||
// // 👇 جدید: URL ها برای عکس/صدا
|
||||
// String fileUrl = str(m,"file_url");
|
||||
// String thumbUrl = str(m,"thumb_url");
|
||||
//
|
||||
// String fwdFrom = str(m,"forwarded_from");
|
||||
// String fwdBy = str(m,"forwarded_by");
|
||||
// String replyTo = str(m,"reply_to_id");
|
||||
// boolean edited = bool(m,"is_edited");
|
||||
// JSONArray reacts = arr(m,"reactions");
|
||||
//
|
||||
// LocalDateTime ts = parseWhen(whenIso);
|
||||
// if (ts == null) ts = LocalDateTime.now();
|
||||
//
|
||||
// // اندیس پیام برای ریپلای/ادیت
|
||||
// msgIndex.put(msgId, m);
|
||||
//
|
||||
// // تشخیص خروجی/ورودی
|
||||
// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
|
||||
// ? Session.currentUser.getString("internal_uuid") : "";
|
||||
// boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
|
||||
//
|
||||
// // آپدیت لیست چتها (پریویوِ کوتاه)
|
||||
// String previewText = switch (type.toUpperCase()) {
|
||||
// case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
|
||||
// case "AUDIO" -> "🎵 Audio";
|
||||
// default -> content;
|
||||
// };
|
||||
// updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
|
||||
//
|
||||
// if (!isCurrent) return;
|
||||
//
|
||||
// // 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl)
|
||||
// addBubble(outgoing, senderName, type, content, ts, msgId,
|
||||
// fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl);
|
||||
//
|
||||
// if (currentChat != null) markAsRead(currentChat);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
public void onRealTimeNewMessage(org.json.JSONObject m) {
|
||||
try {
|
||||
String chatIdStr = str(m,"receiver_id");
|
||||
String chatType = str(m,"receiver_type");
|
||||
// 1) chat id/type با fallback
|
||||
String chatIdStr = nz(m.optString("receiver_id", m.optString("chat_id","")));
|
||||
String chatType = nz(m.optString("receiver_type", m.optString("chat_type","")));
|
||||
if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
|
||||
|
||||
UUID chatId = UUID.fromString(chatIdStr);
|
||||
boolean isCurrent = isSameChat(chatId, chatType);
|
||||
|
||||
// id → message_id fallback
|
||||
if (!m.has("message_id") && m.has("id")) {
|
||||
m.put("message_id", m.getString("id"));
|
||||
}
|
||||
// 2) message_id با fallback از id
|
||||
if (!m.has("message_id") && m.has("id")) m.put("message_id", m.getString("id"));
|
||||
String msgId = str(m,"message_id");
|
||||
if (!hasVal(msgId)) return;
|
||||
|
||||
// ✅ اگر قبلاً همین پیام داخل UI اضافه شده، دیگه دوباره نساز
|
||||
// تکراری نساز
|
||||
if (messageNodes.containsKey(msgId)) return;
|
||||
|
||||
// 3) sender/name
|
||||
String senderId = str(m,"sender_id");
|
||||
String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
|
||||
: (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown");
|
||||
: (hasVal(senderId) ? shortId(senderId) : "Unknown");
|
||||
|
||||
String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
|
||||
String content = str(m,"content");
|
||||
String whenIso = str(m,"send_at");
|
||||
// 4) نوع پیام (سرور ممکنه lowercase بده)
|
||||
String tRaw = nz(m.optString("message_type","TEXT"));
|
||||
String type = tRaw.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
|
||||
// 5) متن/کپشن (content یا text)
|
||||
String content = nz(m.optString("content", m.optString("text","")));
|
||||
|
||||
// 6) زمان
|
||||
String whenIso = nz(m.optString("send_at", m.optString("created_at","")));
|
||||
java.time.LocalDateTime ts = parseWhen(whenIso);
|
||||
if (ts == null) ts = java.time.LocalDateTime.now();
|
||||
|
||||
// 7) فایل: هم فرمت قدیم (file_url/thumb_url) هم جدید (media.url/thumbnail_url)
|
||||
String fileUrl = nz(m.optString("file_url",""));
|
||||
String thumbUrl = nz(m.optString("thumb_url",""));
|
||||
org.json.JSONObject media = m.optJSONObject("media");
|
||||
if (media != null) {
|
||||
if (!hasVal(fileUrl)) fileUrl = nz(media.optString("url",""));
|
||||
if (!hasVal(thumbUrl)) thumbUrl = nz(media.optString("thumbnail_url",""));
|
||||
// اگر width/height لازم شد، از media.optInt("width"), media.optInt("height") بخوان
|
||||
}
|
||||
|
||||
// 8) فوروارد/ریپلای/ادیت/ریاکشن
|
||||
String fwdFrom = str(m,"forwarded_from");
|
||||
String fwdBy = str(m,"forwarded_by");
|
||||
String replyTo = str(m,"reply_to_id");
|
||||
boolean edited = bool(m,"is_edited");
|
||||
JSONArray reacts = arr(m,"reactions");
|
||||
org.json.JSONArray reacts = arr(m,"reactions");
|
||||
|
||||
LocalDateTime ts = parseWhen(whenIso);
|
||||
if (ts == null) ts = LocalDateTime.now();
|
||||
// 9) outgoing
|
||||
String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
|
||||
? Session.currentUser.getString("internal_uuid") : "";
|
||||
boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
|
||||
|
||||
// ایندکس برای ریپلای/ادیت/ریاکشنهای بعدی
|
||||
// 10) ایندکس برای ریپلای/ادیتهای بعدی
|
||||
msgIndex.put(msgId, m);
|
||||
|
||||
// آپدیت لیست چتها (پریویو)
|
||||
boolean incoming = true; // از سرور آمده → ورودی
|
||||
updateChatListPreview(chatId, chatType, incoming, content, type);
|
||||
// 11) آپدیت پریویو لیست چتها
|
||||
String previewText = switch (type) {
|
||||
case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
|
||||
case "AUDIO" -> "🎵 Audio";
|
||||
default -> content;
|
||||
};
|
||||
updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
|
||||
|
||||
// اگر در چت فعلی نیستیم، فقط پریویو آپدیت شد؛ برگرد
|
||||
// 12) اگر چت جاری است، حباب بساز
|
||||
if (!isCurrent) return;
|
||||
|
||||
// اضافه کردن حباب بدون رفرش
|
||||
addBubble(false, senderName, type, content, ts, msgId,
|
||||
fwdFrom, fwdBy, replyTo, edited, reacts);
|
||||
addBubble(outgoing, senderName, type, content, ts, msgId,
|
||||
fwdFrom, fwdBy, replyTo, edited, reacts,
|
||||
fileUrl, thumbUrl);
|
||||
|
||||
// خوانده شد (در صورت نیاز)
|
||||
if (currentChat != null) markAsRead(currentChat);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -1856,6 +2433,8 @@ public class ChatPageController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) {
|
||||
var mc = MainController.getInstance();
|
||||
if (mc == null) return;
|
||||
@@ -3093,4 +3672,23 @@ public class ChatPageController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void updatePendingStatus(String messageId, String text) {
|
||||
HBox node = pendingById.get(messageId);
|
||||
if (node == null) return;
|
||||
// پیدا کردن لیبل وضعیت
|
||||
if (node.getChildren().size() >= 2 && node.getChildren().get(1) instanceof VBox v) {
|
||||
for (Node n : v.getChildren()) {
|
||||
if (n instanceof HBox row) {
|
||||
for (Node c : row.getChildren()) {
|
||||
if (c instanceof Label l && "statusLabel".equals(l.getProperties().get("role"))) {
|
||||
l.setText(text); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -256,7 +256,6 @@ public class EditProfileController {
|
||||
}
|
||||
|
||||
if (!anyError) {
|
||||
// 1) اگر صفحهی MyProfile باز است
|
||||
var mp = MyProfileController.getInstance();
|
||||
if (mp != null) {
|
||||
mp.setProfileData(
|
||||
|
||||
@@ -77,4 +77,20 @@
|
||||
/* نسخهی قرمز برای UNBLOCK */
|
||||
.button.footer-link-btn.danger {
|
||||
-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 {
|
||||
-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 {
|
||||
-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 contact’s 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 & 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 & 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 & 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 & 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>
|
||||
Reference in New Issue
Block a user