Sending image
@@ -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;
|
||||||
@@ -4233,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()
|
||||||
@@ -4282,36 +4286,117 @@ public class ActionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) {
|
||||||
|
// if (file == null) {
|
||||||
|
// System.out.println("❌ File is null");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (!file.exists()) {
|
||||||
|
// System.out.println("❌ File not found: " + file.getAbsolutePath());
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (file.isDirectory()) {
|
||||||
|
// System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath());
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final UUID messageId = UUID.randomUUID();
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// String mime = detectMime(file, type.toUpperCase());
|
||||||
|
// if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*";
|
||||||
|
//
|
||||||
|
// JSONObject header = new JSONObject()
|
||||||
|
// .put("message_id", messageId.toString())
|
||||||
|
// .put("sender_id", Session.getUserUUID())
|
||||||
|
// .put("receiver_type", receiverType) // private|group|channel
|
||||||
|
// .put("receiver_id", receiverId.toString())
|
||||||
|
// .put("message_type", type.toUpperCase()) // IMAGE | AUDIO
|
||||||
|
// .put("file_name", file.getName())
|
||||||
|
// .put("mime_type", mime)
|
||||||
|
// .put("text", caption == null ? "" : caption);
|
||||||
|
//
|
||||||
|
// byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
// long contentLen = file.length();
|
||||||
|
//
|
||||||
|
// BlockingQueue<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", Session.getUserUUID())
|
.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();
|
||||||
@@ -4320,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];
|
||||||
@@ -4342,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 {
|
||||||
@@ -4360,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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4631,4 +4724,7 @@ public class ActionHandler {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -124,6 +124,7 @@ public class ChatPageController {
|
|||||||
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 Deque<HBox> pendingBubbles = new ArrayDeque<>();
|
||||||
|
|
||||||
|
private final Map<String, HBox> pendingById = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
// جایی عمومی (مثلا بالای کلاس)
|
// جایی عمومی (مثلا بالای کلاس)
|
||||||
@@ -169,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
|
||||||
@@ -707,52 +714,39 @@ public class ChatPageController {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void openFileChooser() {
|
private void openFileChooser() {
|
||||||
FileChooser fc = new FileChooser();
|
FileChooser fc = new FileChooser();
|
||||||
fc.setTitle("Select image or audio");
|
fc.setTitle("Select image or audio");
|
||||||
|
|
||||||
// فقط عکس/صدا
|
|
||||||
fc.getExtensionFilters().addAll(
|
fc.getExtensionFilters().addAll(
|
||||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp"),
|
new FileChooser.ExtensionFilter("Images", "*.png","*.jpg","*.jpeg","*.gif","*.bmp","*.webp"),
|
||||||
new FileChooser.ExtensionFilter("Audio", "*.mp3", "*.wav", "*.m4a", "*.ogg", "*.aac")
|
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) return;
|
if (file == null) return;
|
||||||
|
|
||||||
// نوع (IMAGE/AUDIO) را از اسم/محتوا حدس بزنیم
|
String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO"
|
||||||
String type = guessType(file);
|
if (type == null) { toast("Only image or audio"); return; }
|
||||||
if (type == null) {
|
|
||||||
toast("Only Image and Aduio");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentChatId = String.valueOf(currentChat.getId());
|
|
||||||
Session.currentChatType = currentChat.getType();
|
|
||||||
|
|
||||||
// مقصد چت از Session
|
if (currentChat == null) { toast("Not available chat"); return; }
|
||||||
if (Session.currentChatId == null || Session.currentChatType == null) {
|
UUID receiverId = currentChat.getId(); // همون chat_id
|
||||||
toast("Not available chat");
|
String receiverType = currentChat.getType(); // "private" | "group" | "channel"
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UUID receiverId = UUID.fromString(Session.currentChatId);
|
|
||||||
String receiverType = Session.currentChatType; // "private" | "group" | "channel"
|
|
||||||
|
|
||||||
String caption = (messageInput != null) ? messageInput.getText().trim() : "";
|
String caption = (messageInput != null) ? messageInput.getText().trim() : "";
|
||||||
if (messageInput != null) messageInput.clear();
|
if (messageInput != null) messageInput.clear();
|
||||||
|
|
||||||
// 1) حباب Pending فوری در UI
|
// 1) message_id را همینجا بساز تا Pending به همین ID وصل شود
|
||||||
HBox pending = addPendingMediaBubble(file, type, caption);
|
UUID messageId = UUID.randomUUID();
|
||||||
|
|
||||||
// 2) ارسال واقعی در بکگراند با ActionHandler
|
// 2) حباب Pending (نسخهای که messageId میگیرد)
|
||||||
|
addPendingMediaBubble(messageId.toString(), file, type, caption);
|
||||||
|
|
||||||
|
// 3) ارسال واقعی با همین messageId
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
ActionHandler ah = ActionHandler.getInstance();
|
ActionHandler ah = ActionHandler.getInstance();
|
||||||
ah.sendMediaMessage(receiverId, receiverType, type, file, caption);
|
ah.sendMediaMessage(messageId, receiverId, receiverType, type, file, caption);
|
||||||
|
// اگر ACK success برگشت، خود ActionHandler میتونه removePendingBubble(messageId) صدا بزنه
|
||||||
// نکته: پیام واقعی بعد از ذخیره روی سرور، از طریق Listener برمیگرده.
|
// وگرنه در onRealTimeNewMessage که پیام واقعی آمد، پاک میکنیم (کد آن را قبلاً دادم).
|
||||||
// وقتی رسید، در Listener این متد را صدا بزنید تا یک حباب Pending حذف شود:
|
|
||||||
// Platform.runLater(() -> removeOnePendingBubble());
|
|
||||||
}, "Media-Uploader").start();
|
}, "Media-Uploader").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,62 +759,120 @@ public class ChatPageController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ساخت یک حباب «درحال ارسال…» */
|
// /** ساخت یک حباب «درحال ارسال…» */
|
||||||
private HBox addPendingMediaBubble(File file, String type, String caption) {
|
// private HBox addPendingMediaBubble(File file, String type, String caption) {
|
||||||
HBox root = new HBox(8);
|
// HBox root = new HBox(8);
|
||||||
root.getStyleClass().add("bubble-outgoing"); // استایل دلخواهت
|
// root.getStyleClass().add("bubble-outgoing"); // استایل دلخواهت
|
||||||
root.setFillHeight(true);
|
// root.setFillHeight(true);
|
||||||
|
//
|
||||||
|
// ImageView iv = null;
|
||||||
|
// if ("IMAGE".equalsIgnoreCase(type)) {
|
||||||
|
// iv = new ImageView(new Image(file.toURI().toString(), 360, 360, true, true, true));
|
||||||
|
// iv.setPreserveRatio(true);
|
||||||
|
// iv.setFitWidth(240); // سایز معقول برای Pending
|
||||||
|
// iv.setFitHeight(240);
|
||||||
|
// root.getChildren().add(iv);
|
||||||
|
// } else if ("AUDIO".equalsIgnoreCase(type)) {
|
||||||
|
// // برای صدا یک آیکون ساده و نام فایل
|
||||||
|
// ImageView icon = new ImageView(); // اگر آیکون داری اینجا بگذار
|
||||||
|
// icon.setFitWidth(24); icon.setFitHeight(24);
|
||||||
|
// Label name = new Label(file.getName());
|
||||||
|
// HBox audioBox = new HBox(6, icon, name);
|
||||||
|
// root.getChildren().add(audioBox);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// VBox right = new VBox(4);
|
||||||
|
// if (caption != null && !caption.isBlank()) {
|
||||||
|
// Label cap = new Label(caption);
|
||||||
|
// cap.getStyleClass().add("msg-caption");
|
||||||
|
// cap.setWrapText(true);
|
||||||
|
// right.getChildren().add(cap);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// HBox statusRow = new HBox(6);
|
||||||
|
// ProgressIndicator spinner = new ProgressIndicator();
|
||||||
|
// spinner.setPrefSize(16, 16);
|
||||||
|
// Label status = new Label("Sending...");
|
||||||
|
// status.getStyleClass().add("msg-status");
|
||||||
|
// Region spacer = new Region();
|
||||||
|
// HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
// statusRow.getChildren().addAll(spinner, status, spacer);
|
||||||
|
//
|
||||||
|
// right.getChildren().add(statusRow);
|
||||||
|
// root.getChildren().add(right);
|
||||||
|
//
|
||||||
|
// messageContainer.getChildren().add(root);
|
||||||
|
// pendingBubbles.addLast(root);
|
||||||
|
//
|
||||||
|
// return root;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** وقتی پیام واقعی (از خودِ کاربر) برای همین چت رسید، یکی از Pendingها را حذف کن. */
|
||||||
|
// public void removeOnePendingBubble() {
|
||||||
|
// HBox node = pendingBubbles.pollFirst();
|
||||||
|
// if (node != null) {
|
||||||
|
// messageContainer.getChildren().remove(node);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
ImageView iv = null;
|
|
||||||
|
private HBox addPendingMediaBubble(String messageId, File file, String type, String caption) {
|
||||||
|
HBox root = new HBox(8);
|
||||||
|
root.setFillHeight(true);
|
||||||
|
root.setAlignment(Pos.CENTER_RIGHT); // چون outgoing است
|
||||||
|
|
||||||
|
// preview
|
||||||
if ("IMAGE".equalsIgnoreCase(type)) {
|
if ("IMAGE".equalsIgnoreCase(type)) {
|
||||||
iv = new ImageView(new Image(file.toURI().toString(), 360, 360, true, true, true));
|
ImageView iv = new ImageView(new Image(file.toURI().toString(), 240, 240, true, true, true));
|
||||||
iv.setPreserveRatio(true);
|
iv.setPreserveRatio(true);
|
||||||
iv.setFitWidth(240); // سایز معقول برای Pending
|
iv.setFitWidth(240);
|
||||||
iv.setFitHeight(240);
|
iv.setFitHeight(240);
|
||||||
root.getChildren().add(iv);
|
root.getChildren().add(iv);
|
||||||
} else if ("AUDIO".equalsIgnoreCase(type)) {
|
} else if ("AUDIO".equalsIgnoreCase(type)) {
|
||||||
// برای صدا یک آیکون ساده و نام فایل
|
|
||||||
ImageView icon = new ImageView(); // اگر آیکون داری اینجا بگذار
|
|
||||||
icon.setFitWidth(24); icon.setFitHeight(24);
|
|
||||||
Label name = new Label(file.getName());
|
Label name = new Label(file.getName());
|
||||||
HBox audioBox = new HBox(6, icon, name);
|
root.getChildren().add(new HBox(6, new Label("🎵"), name));
|
||||||
root.getChildren().add(audioBox);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VBox right = new VBox(4);
|
VBox right = new VBox(4);
|
||||||
if (caption != null && !caption.isBlank()) {
|
if (caption != null && !caption.isBlank()) {
|
||||||
Label cap = new Label(caption);
|
Label cap = new Label(caption);
|
||||||
cap.getStyleClass().add("msg-caption");
|
|
||||||
cap.setWrapText(true);
|
cap.setWrapText(true);
|
||||||
right.getChildren().add(cap);
|
right.getChildren().add(cap);
|
||||||
}
|
}
|
||||||
|
|
||||||
HBox statusRow = new HBox(6);
|
HBox statusRow = new HBox(6);
|
||||||
ProgressIndicator spinner = new ProgressIndicator();
|
ProgressIndicator spinner = new ProgressIndicator();
|
||||||
spinner.setPrefSize(16, 16);
|
spinner.setPrefSize(14, 14);
|
||||||
Label status = new Label("Sending...");
|
Label status = new Label("Sending...");
|
||||||
status.getStyleClass().add("msg-status");
|
status.getProperties().put("role", "statusLabel"); // برای آپدیت بعدی
|
||||||
Region spacer = new Region();
|
statusRow.getChildren().addAll(spinner, status);
|
||||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
|
||||||
statusRow.getChildren().addAll(spinner, status, spacer);
|
|
||||||
|
|
||||||
right.getChildren().add(statusRow);
|
right.getChildren().add(statusRow);
|
||||||
|
|
||||||
root.getChildren().add(right);
|
root.getChildren().add(right);
|
||||||
|
|
||||||
|
// برچسب messageId روی نود
|
||||||
|
if (messageId != null) root.getProperties().put("messageId", messageId);
|
||||||
|
|
||||||
messageContainer.getChildren().add(root);
|
messageContainer.getChildren().add(root);
|
||||||
|
|
||||||
|
// ثبت در مپ/صف
|
||||||
|
if (messageId != null) pendingById.put(messageId, root);
|
||||||
pendingBubbles.addLast(root);
|
pendingBubbles.addLast(root);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** وقتی پیام واقعی (از خودِ کاربر) برای همین چت رسید، یکی از Pendingها را حذف کن. */
|
|
||||||
public void removeOnePendingBubble() {
|
public void removePendingBubble(String messageId) {
|
||||||
HBox node = pendingBubbles.pollFirst();
|
if (messageId == null) return;
|
||||||
|
HBox node = pendingById.remove(messageId);
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
|
pendingBubbles.remove(node);
|
||||||
messageContainer.getChildren().remove(node);
|
messageContainer.getChildren().remove(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void toast(String msg) {
|
private void toast(String msg) {
|
||||||
// هر جور که خودت نوتیف/Toast داری
|
// هر جور که خودت نوتیف/Toast داری
|
||||||
System.out.println("ℹ️ " + msg);
|
System.out.println("ℹ️ " + msg);
|
||||||
@@ -2091,32 +2143,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) {
|
||||||
@@ -2224,67 +2270,150 @@ public class ChatPageController {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
public void onRealTimeNewMessage(JSONObject m) {
|
// public void onRealTimeNewMessage(JSONObject m) {
|
||||||
|
// try {
|
||||||
|
// String chatIdStr = str(m,"receiver_id");
|
||||||
|
// String chatType = str(m,"receiver_type");
|
||||||
|
// if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
|
||||||
|
//
|
||||||
|
// UUID chatId = UUID.fromString(chatIdStr);
|
||||||
|
// boolean isCurrent = isSameChat(chatId, chatType);
|
||||||
|
//
|
||||||
|
// // id → message_id fallback
|
||||||
|
// if (!m.has("message_id") && m.has("id")) {
|
||||||
|
// m.put("message_id", m.getString("id"));
|
||||||
|
// }
|
||||||
|
// String msgId = str(m,"message_id");
|
||||||
|
// if (!hasVal(msgId)) return;
|
||||||
|
//
|
||||||
|
// // اگر قبلاً تو UI هست، دوباره نساز
|
||||||
|
// if (messageNodes.containsKey(msgId)) return;
|
||||||
|
//
|
||||||
|
// String senderId = str(m,"sender_id");
|
||||||
|
// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
|
||||||
|
// : (hasVal(senderId) ? shortId(senderId) : "Unknown");
|
||||||
|
//
|
||||||
|
// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
|
||||||
|
// String content = str(m,"content"); // برای IMAGE = کپشن
|
||||||
|
// String whenIso = str(m,"send_at");
|
||||||
|
//
|
||||||
|
// // 👇 جدید: URL ها برای عکس/صدا
|
||||||
|
// String fileUrl = str(m,"file_url");
|
||||||
|
// String thumbUrl = str(m,"thumb_url");
|
||||||
|
//
|
||||||
|
// String fwdFrom = str(m,"forwarded_from");
|
||||||
|
// String fwdBy = str(m,"forwarded_by");
|
||||||
|
// String replyTo = str(m,"reply_to_id");
|
||||||
|
// boolean edited = bool(m,"is_edited");
|
||||||
|
// JSONArray reacts = arr(m,"reactions");
|
||||||
|
//
|
||||||
|
// LocalDateTime ts = parseWhen(whenIso);
|
||||||
|
// if (ts == null) ts = LocalDateTime.now();
|
||||||
|
//
|
||||||
|
// // اندیس پیام برای ریپلای/ادیت
|
||||||
|
// msgIndex.put(msgId, m);
|
||||||
|
//
|
||||||
|
// // تشخیص خروجی/ورودی
|
||||||
|
// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
|
||||||
|
// ? Session.currentUser.getString("internal_uuid") : "";
|
||||||
|
// boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
|
||||||
|
//
|
||||||
|
// // آپدیت لیست چتها (پریویوِ کوتاه)
|
||||||
|
// String previewText = switch (type.toUpperCase()) {
|
||||||
|
// case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
|
||||||
|
// case "AUDIO" -> "🎵 Audio";
|
||||||
|
// default -> content;
|
||||||
|
// };
|
||||||
|
// updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
|
||||||
|
//
|
||||||
|
// if (!isCurrent) return;
|
||||||
|
//
|
||||||
|
// // 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl)
|
||||||
|
// addBubble(outgoing, senderName, type, content, ts, msgId,
|
||||||
|
// fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl);
|
||||||
|
//
|
||||||
|
// if (currentChat != null) markAsRead(currentChat);
|
||||||
|
//
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// e.printStackTrace();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
public void onRealTimeNewMessage(org.json.JSONObject m) {
|
||||||
try {
|
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 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(senderId) ? shortId(senderId) : "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"); // برای IMAGE = کپشن
|
String tRaw = nz(m.optString("message_type","TEXT"));
|
||||||
String whenIso = str(m,"send_at");
|
String type = tRaw.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
|
|
||||||
// 👇 جدید: URL ها برای عکس/صدا
|
// 5) متن/کپشن (content یا text)
|
||||||
String fileUrl = str(m,"file_url");
|
String content = nz(m.optString("content", m.optString("text","")));
|
||||||
String thumbUrl = str(m,"thumb_url");
|
|
||||||
|
|
||||||
|
// 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();
|
|
||||||
|
|
||||||
// اندیس پیام برای ریپلای/ادیت
|
|
||||||
msgIndex.put(msgId, m);
|
|
||||||
|
|
||||||
// تشخیص خروجی/ورودی
|
|
||||||
String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
|
String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid"))
|
||||||
? Session.currentUser.getString("internal_uuid") : "";
|
? Session.currentUser.getString("internal_uuid") : "";
|
||||||
boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
|
boolean outgoing = hasVal(senderId) && senderId.equalsIgnoreCase(myId);
|
||||||
|
|
||||||
// آپدیت لیست چتها (پریویوِ کوتاه)
|
// 10) ایندکس برای ریپلای/ادیتهای بعدی
|
||||||
String previewText = switch (type.toUpperCase()) {
|
msgIndex.put(msgId, m);
|
||||||
|
|
||||||
|
// 11) آپدیت پریویو لیست چتها
|
||||||
|
String previewText = switch (type) {
|
||||||
case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
|
case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo");
|
||||||
case "AUDIO" -> "🎵 Audio";
|
case "AUDIO" -> "🎵 Audio";
|
||||||
default -> content;
|
default -> content;
|
||||||
};
|
};
|
||||||
updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
|
updateChatListPreview(chatId, chatType, !outgoing, previewText, type);
|
||||||
|
|
||||||
|
// 12) اگر چت جاری است، حباب بساز
|
||||||
if (!isCurrent) return;
|
if (!isCurrent) return;
|
||||||
|
|
||||||
// 👇 امضای جدیدِ addBubble (با fileUrl/thumbUrl)
|
|
||||||
addBubble(outgoing, senderName, type, content, ts, msgId,
|
addBubble(outgoing, senderName, type, content, ts, msgId,
|
||||||
fwdFrom, fwdBy, replyTo, edited, reacts, fileUrl, thumbUrl);
|
fwdFrom, fwdBy, replyTo, edited, reacts,
|
||||||
|
fileUrl, thumbUrl);
|
||||||
|
|
||||||
if (currentChat != null) markAsRead(currentChat);
|
if (currentChat != null) markAsRead(currentChat);
|
||||||
|
|
||||||
@@ -2294,6 +2423,7 @@ public class ChatPageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) {
|
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;
|
||||||
@@ -3523,4 +3653,23 @@ public class ChatPageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void updatePendingStatus(String messageId, String text) {
|
||||||
|
HBox node = pendingById.get(messageId);
|
||||||
|
if (node == null) return;
|
||||||
|
// پیدا کردن لیبل وضعیت
|
||||||
|
if (node.getChildren().size() >= 2 && node.getChildren().get(1) instanceof VBox v) {
|
||||||
|
for (Node n : v.getChildren()) {
|
||||||
|
if (n instanceof HBox row) {
|
||||||
|
for (Node c : row.getChildren()) {
|
||||||
|
if (c instanceof Label l && "statusLabel".equals(l.getProperties().get("role"))) {
|
||||||
|
l.setText(text); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -78,3 +78,19 @@
|
|||||||
.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";
|
||||||
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |