From 6fc2b2e2ed4af228e553a1c9e21afb1cc2bf8e80 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Mon, 8 Sep 2025 00:50:48 +0330 Subject: [PATCH 1/8] Fix bugs in add contact to chat --- .../UI/ChatItemController.java | 7 ++ .../UI/ContactsController.java | 28 +++-- .../UI/MainController.java | 112 +++++++++++++++--- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index fc734c2..273798a 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -7,6 +7,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.scene.shape.Circle; import org.to.telegramfinalproject.Client.AvatarLocalResolver; +import org.to.telegramfinalproject.Models.ChatEntry; import java.util.Objects; @@ -141,4 +142,10 @@ public class ChatItemController { } } + private String safeTitle(ChatEntry e) { + if (e.getName() != null && !e.getName().isBlank()) return e.getName(); + if (e.getDisplayId() != null && !e.getDisplayId().isBlank()) return e.getDisplayId(); + return "Unknown"; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java b/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java index 21b017f..316db81 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java @@ -90,10 +90,9 @@ public class ContactsController { return new ArrayList<>(Session.contactEntries); } - // در غیر این صورت از سرور می‌گیریم (اختیاری) - // اگر API «view_contacts» داری، اینجا بفرست: + JSONObject req = new JSONObject() - .put("action", "view_contacts") // 🔧 اگر نام اکشن‌ات فرق دارد، تغییر بده + .put("action", "view_contacts") .put("user_id", Session.getUserUUID()); JSONObject res = ActionHandler.sendWithResponse(req); @@ -122,7 +121,6 @@ public class ContactsController { fetched.add(new ContactEntry(contactId, userId, contactDisplay, profileName, imageUrl, isBlocked, lastSeen)); } - // اگر می‌خواهی تو سشن هم نگه داری: if (Session.contactEntries == null) Session.contactEntries = new ArrayList<>(); Session.contactEntries.clear(); Session.contactEntries.addAll(fetched); @@ -161,7 +159,6 @@ public class ContactsController { item.getStyleClass().add("contact-item"); item.setCursor(Cursor.HAND); - // آواتار ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl)); avatar.setFitWidth(58); avatar.setFitHeight(58); @@ -183,13 +180,24 @@ public class ContactsController { private Image loadAvatarSafe(String urlOrResource) { try { - if (urlOrResource != null && urlOrResource.startsWith("/")) { - return new Image(Objects.requireNonNull(getClass().getResourceAsStream(urlOrResource))); + if (urlOrResource != null) { + // حالت Resource داخلی + if (urlOrResource.startsWith("/")) { + return new Image(Objects.requireNonNull( + getClass().getResourceAsStream(urlOrResource))); + } + // حالت URL وب یا مسیر فایل + if (urlOrResource.startsWith("http://") || + urlOrResource.startsWith("https://") || + urlOrResource.startsWith("file:")) { + return new Image(urlOrResource, true); // true = لود async + } } - // اگر URL وب هم داری، می‌تونی مستقیم Image(url) بسازی + // fallback به پیش‌فرض return new Image(Objects.requireNonNull( getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png"))); - } catch (Exception ignore) { + } catch (Exception e) { + // هر مشکلی → پیش‌فرض return new Image(Objects.requireNonNull( getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png"))); } @@ -305,4 +313,4 @@ public class ContactsController { } -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 5189b2c..c970969 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -13,6 +13,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.shape.Circle; import javafx.util.Duration; +import org.json.JSONObject; import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Client.ActionHandler; @@ -96,8 +97,12 @@ public class MainController { // Keep track of the scene user comes from private final Deque navigationStack = new ArrayDeque<>(); private final Map itemControllers = new HashMap<>(); + private final java.util.Set enrichInFlight = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + private static final String SAVED_TITLE = "Saved Messages"; + private static final String SAVED_AVATAR = "/org/to/telegramfinalproject/Avatars/saved_messages.png"; + //For realtime handling public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs, boolean isIncoming, String lastPreview) { @@ -351,10 +356,8 @@ public class MainController { } } - // اگر “Archived Chats” یا هدر دیگری داری، قبلش اضافه کن (اختیاری) - // addArchivedHeaderIfYouHaveOne(); +// addArchivedHeaderIfYouHaveOne(); - // 1) همیشه Saved اول بیاد (اگر وجود داشت) if (saved != null) { addChatNode(saved); } @@ -408,33 +411,98 @@ public class MainController { // } // } - private void addChatNode(ChatEntry chat) { try { FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml")); Node item = fx.load(); ChatItemController cc = fx.getController(); - String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); + // --- saved detection + boolean saved = isSaved(chat); - String timeText = chat.getLastMessageTime() == null - ? "" - : formatChatTime(chat.getLastMessageTime()); + String title = safeTitle(chat); + String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); + String timeText = chat.getLastMessageTime() == null ? "" : formatChatTime(chat.getLastMessageTime()); + String imageUrl = safeImage(chat.getImageUrl()); - // If chat has a profile picture, pass it; otherwise null - String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty()) - ? chat.getImageUrl() - : null; + // --- force Saved Messages title & avatar + if (saved) { + title = SAVED_TITLE; + imageUrl = SAVED_AVATAR; + } - cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType()); + cc.setChatData(title, preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType()); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); itemControllers.put(chat.getId(), cc); + + // --- no enrich for Saved + boolean needName = (chat.getName() == null || chat.getName().isBlank()); + boolean needImage = (chat.getImageUrl() == null || chat.getImageUrl().isBlank()); + if (!saved && (needName || needImage)) { + enrichChatEntryAsync(chat); + } } catch (Exception ex) { ex.printStackTrace(); } } + private boolean isSaved(ChatEntry e) { + if (e == null) return false; + if (e.isSavedMessages()) return true; + return "saved".equalsIgnoreCase(e.getType()); // اگر type اختصاصی داری + } + + + public void updateSingleChatCell(ChatEntry entry) { + ChatItemController cc = itemControllers.get(entry.getId()); + if (cc != null) { + Platform.runLater(() -> cc.setChatData( + safeTitle(entry), + entry.getLastMessagePreview() == null ? "" : entry.getLastMessagePreview(), + entry.getLastMessageTime() == null ? "" : formatChatTime(entry.getLastMessageTime()), + entry.getUnreadCount(), + safeImage(entry.getImageUrl()), + entry.getType() + )); + } else { + refreshChatListUI(); // اگر پیدا نشد، کل لیست را رفرش کن + } + } + + public void enrichChatEntryAsync(ChatEntry entry) { + if (entry == null) return; + // اگر در حال دریافت هستیم، تکراری نفرست + if (!enrichInFlight.add(entry.getId())) return; + + new Thread(() -> { + try { + JSONObject req = new JSONObject() + .put("action", "get_header_info") + .put("receiver_id", entry.getId().toString()) + .put("receiver_type", entry.getType()) + .put("viewer_id", Session.getUserUUID()); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + JSONObject d = res.optJSONObject("data"); + if (d != null) { + String name = d.optString("name", ""); + String image = d.optString("image_url", ""); + Platform.runLater(() -> { + if (!name.isBlank()) entry.setName(name); + if (!image.isBlank()) entry.setImageUrl(image); + updateSingleChatCell(entry); // فقط همان آیتم را نوسازی کن + }); + } + } + } catch (Exception ignored) { + } finally { + enrichInFlight.remove(entry.getId()); + } + }).start(); + } + private String mapTypeToLabel(String t) { switch (t.toUpperCase()) { @@ -1201,13 +1269,11 @@ public class MainController { if (Session.chatList == null) Session.chatList = new ArrayList<>(); Session.chatList.add(ce); } - // اگر activeChats استفاده می‌کنی: if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) { Session.activeChats.add(ce); } } catch (Exception ignore) {} - // (اختیاری) مرتب‌سازی بر اساس زمان آخرین پیام Comparator byTimeDesc = (a,b) -> { LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime(); if (t1 == null && t2 == null) return 0; @@ -1340,5 +1406,21 @@ public class MainController { } + // title fallback: اول name بعد displayId، در نهایت پیش‌فرض + private String safeTitle(ChatEntry e) { + if (e.getName() != null && !e.getName().isBlank()) return e.getName(); + if (e.getDisplayId() != null && !e.getDisplayId().isBlank()) return e.getDisplayId(); + if ("saved".equalsIgnoreCase(e.getType()) || e.isSavedMessages()) return "Saved Messages"; + return "Unknown"; + } + + // image fallback: اگر خالی بود، آواتار پیش‌فرض + private String safeImage(String img) { + return (img != null && !img.isBlank()) + ? img + : "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + } + + } \ No newline at end of file From 77ac3d37604c92b03a62200459291aea52050b04 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Mon, 8 Sep 2025 00:51:08 +0330 Subject: [PATCH 2/8] Files --- .../0d3d9920-1bbe-4dca-816f-bcced6cb1b94.jpg | Bin 0 -> 7851 bytes .../19b9a652-30d8-426a-896e-dcfd30fe3da3.jpg | Bin 0 -> 3804 bytes .../8020b7a3-5ff0-4ab1-aa62-9a4825bcfbab.jpg | Bin 0 -> 3627 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 uploads/images/2025-09-08/0d3d9920-1bbe-4dca-816f-bcced6cb1b94.jpg create mode 100644 uploads/images/2025-09-08/19b9a652-30d8-426a-896e-dcfd30fe3da3.jpg create mode 100644 uploads/images/2025-09-08/8020b7a3-5ff0-4ab1-aa62-9a4825bcfbab.jpg diff --git a/uploads/images/2025-09-08/0d3d9920-1bbe-4dca-816f-bcced6cb1b94.jpg b/uploads/images/2025-09-08/0d3d9920-1bbe-4dca-816f-bcced6cb1b94.jpg new file mode 100644 index 0000000000000000000000000000000000000000..05084e6f9ba909f30ddadac9c3ccffe6f0a878e1 GIT binary patch literal 7851 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 literal 0 HcmV?d00001 diff --git a/uploads/images/2025-09-08/19b9a652-30d8-426a-896e-dcfd30fe3da3.jpg b/uploads/images/2025-09-08/19b9a652-30d8-426a-896e-dcfd30fe3da3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ee48cbbb5c161955f1359fafee12778acbae28ff GIT binary patch literal 3804 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 literal 0 HcmV?d00001 diff --git a/uploads/images/2025-09-08/8020b7a3-5ff0-4ab1-aa62-9a4825bcfbab.jpg b/uploads/images/2025-09-08/8020b7a3-5ff0-4ab1-aa62-9a4825bcfbab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6436ef050eae9fd98b8edf56245767a35fe5c20e GIT binary patch literal 3627 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 literal 0 HcmV?d00001 From 98a5d85f55fd12d5766f4af04c8344edad40371c Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Mon, 8 Sep 2025 08:23:04 +0330 Subject: [PATCH 3/8] Delete / leave / Archive in group --- src/main/java/module-info.java | 1 + .../UI/ChatPageController.java | 1949 +++++------------ .../UI/GroupInfoController.java | 39 +- .../UI/LeaveGroupFlow.java | 359 +++ .../UI/MainController.java | 1 - .../UI/OwnerTransferDialogController.java | 271 +++ .../Fxml/owner_transfer_dialog.fxml | 44 + .../01919d14-e3f5-4dd4-b579-21fe59c5bfed.jpg | Bin 0 -> 39335 bytes .../ce62f491-c4bb-4529-afd0-251965eb69c1.jpg | Bin 0 -> 12717 bytes 9 files changed, 1270 insertions(+), 1394 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/UI/LeaveGroupFlow.java create mode 100644 src/main/java/org/to/telegramfinalproject/UI/OwnerTransferDialogController.java create mode 100644 src/main/resources/org/to/telegramfinalproject/Fxml/owner_transfer_dialog.fxml create mode 100644 uploads/avatars/2025-09-08/01919d14-e3f5-4dd4-b579-21fe59c5bfed.jpg create mode 100644 uploads/avatars/2025-09-08/ce62f491-c4bb-4529-afd0-251965eb69c1.jpg diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 9741c93..aac8ea4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -15,6 +15,7 @@ module org.to.telegramfinalproject { requires spark.core; requires javax.servlet.api; requires mp3agic; + requires jdk.internal.le; // FXML کنترلرها در این پکیج‌اند: opens org.to.telegramfinalproject.UI to javafx.fxml; diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index a5546e3..9730403 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.UI; import javafx.application.Platform; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Insets; @@ -20,6 +21,7 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; import javafx.stage.FileChooser; +import javafx.stage.Modality; import javafx.stage.Stage; import org.json.JSONArray; import org.json.JSONObject; @@ -30,14 +32,16 @@ import org.to.telegramfinalproject.Models.ChatEntry; import java.io.File; import java.io.IOException; +import java.net.URL; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.stream.Collectors; + -import static org.to.telegramfinalproject.Client.Session.currentChatId; public class ChatPageController { @@ -73,7 +77,8 @@ public class ChatPageController { @FXML private ImageView searchIcon; - @FXML private VBox blockedPane; + @FXML + private VBox blockedPane; @FXML private Button chatMoreButton; // 3-dots button @@ -89,64 +94,72 @@ public class ChatPageController { private MenuItem archiveItem; // ===== Group & Channel more button ===== - @FXML private MenuItem viewGroupItem; - @FXML private MenuItem leaveGroupItem; + @FXML + private MenuItem viewGroupItem; + @FXML + private MenuItem leaveGroupItem; - @FXML private MenuItem viewChannelItem; - @FXML private MenuItem leaveChannelItem; + @FXML + private MenuItem viewChannelItem; + @FXML + private MenuItem leaveChannelItem; // ===== send icon ===== @FXML private ImageView sendIcon; // ===== For Searching System ===== - @FXML private VBox composerPane; - @FXML private VBox joinPane; - @FXML private VBox addContactPane; + @FXML + private VBox composerPane; + @FXML + private VBox joinPane; + @FXML + private VBox addContactPane; // For search handling - @FXML private Button joinButton; - @FXML private Button addContactButton; + @FXML + private Button joinButton; + @FXML + private Button addContactButton; // Handle View chat - @FXML private Button unblockBtn; - @FXML private VBox readOnlyPane; - @FXML private Label readOnlyLabel; + @FXML + private Button unblockBtn; + @FXML + private VBox readOnlyPane; + @FXML + private Label readOnlyLabel; private ChatViewMode currentMode = ChatViewMode.NORMAL; - // --- state for interactions --- - private String pendingReplyToId = null; // اگه کاربر ریپلای رو زده - private String pendingEditMsgId = null; // اگه کاربر ادیت رو شروع کرده + private String pendingReplyToId = null; + private String pendingEditMsgId = null; private final Map messageNodes = new HashMap<>(); private final Deque pendingBubbles = new ArrayDeque<>(); private final Map 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 static final String UPLOADS_DIR = "C:/Users/User/Desktop/Project/uploads"; + private static final String HTTP_BASE = "http://localhost:8080"; private JSONObject lastHeaderData = null; // ===== Time formatter for messages ===== - private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); - private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); - private static final String YESTERDAY_LABEL = "Yesterday"; + private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); + private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; private boolean blockedByMeFlag = false; - private boolean blockedMeFlag = false; + private boolean blockedMeFlag = false; private volatile boolean justJoinedThisChat = false; - // ===== state ===== private String chatName; private final ThemeManager themeManager = ThemeManager.getInstance(); @@ -159,10 +172,15 @@ public class ChatPageController { private ChatEntry currentChat; private UUID me; + public ChatEntry current(){ + return this.currentChat; + } + // ----- helpers for safe strings ----- private static String nz(String s) { return s == null ? "" : s.trim(); } + private static boolean hasVal(String s) { if (s == null) return false; String t = s.trim(); @@ -170,8 +188,6 @@ public class ChatPageController { } - - public static ChatPageController get() { return instance; } @@ -195,13 +211,27 @@ public class ChatPageController { } private static String str(org.json.JSONObject j, String k) { - try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; } + try { + return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; + } catch (Exception e) { + return ""; + } } + private static boolean bool(org.json.JSONObject j, String k) { - try { return (j.has(k) && !j.isNull(k)) && j.getBoolean(k); } catch (Exception e) { return false; } + try { + return (j.has(k) && !j.isNull(k)) && j.getBoolean(k); + } catch (Exception e) { + return false; + } } + private static org.json.JSONArray arr(org.json.JSONObject j, String k) { - try { return (j.has(k) && !j.isNull(k)) ? j.getJSONArray(k) : null; } catch (Exception e) { return null; } + try { + return (j.has(k) && !j.isNull(k)) ? j.getJSONArray(k) : null; + } catch (Exception e) { + return null; + } } @@ -219,7 +249,6 @@ public class ChatPageController { } } - /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ private LocalDateTime parseWhen(String iso) { if (iso == null || iso.isEmpty()) return null; try { @@ -350,199 +379,14 @@ public class ChatPageController { themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme()); } -// private void initCurrentUserId() { -// try { -// String meStr = org.to.telegramfinalproject.Client.Session -// .currentUser.getString("internal_uuid"); -// me = UUID.fromString(meStr); -// } catch (Exception ignore) { -// me = null; -// } -// } -// -// private String formatWhen(LocalDateTime ts) { -// if (ts == null) return ""; -// LocalDate today = LocalDate.now(); -// LocalDate d = ts.toLocalDate(); -// -// if (d.isEqual(today)) { -// return FMT_HHMM.format(ts); -// } else if (d.isEqual(today.minusDays(1))) { -// return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); -// } else { -// return FMT_DATE_TIME.format(ts); -// } -// } -// -// /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ -// private LocalDateTime parseWhen(String iso) { -// if (iso == null || iso.isEmpty()) return null; -// try { -// return OffsetDateTime.parse(iso).toLocalDateTime(); -// } catch (Exception ignore) { -// try { -// return LocalDateTime.parse(iso); -// } catch (Exception e) { -// return null; -// } -// } -// } @FXML private void openSearchPanel() { MainController.getInstance().showSearchPanel(); } -// /** -// * Called by main controller when opening a chat. -// */ -// public void setChat(String chatName, String avatarPath) { -// this.chatName = chatName; -// -// // Header text (if present) -// if (chatTitle != null) chatTitle.setText(chatName); -// if (chatStatus != null) chatStatus.setText("last seen recently"); // or live status -// -// // Load avatar if provided -// if (userAvatar != null && avatarPath != null) { -// try { -// Image avatarImg = new Image(getClass().getResourceAsStream(avatarPath)); -// userAvatar.setImage(avatarImg); -// Circle clip = new Circle(18, 18, 18); // x, y, radius -// userAvatar.setClip(clip); -// } catch (Exception e) { -// System.err.println("Could not load avatar: " + avatarPath); -// } -// } -// -// addSystemMessage("Chat with " + chatName + " opened."); -// Platform.runLater(() -> messageInput.requestFocus()); -// } - - // ----- actions ----- - -// private void sendMessage() { -// String text = messageInput.getText() == null ? "" : messageInput.getText().trim(); -// if (!text.isEmpty()) { -// addMessage("You", text); -// messageInput.clear(); -// } -// } - - -// private void sendMessage() { -// // 0) Read and validate input -// String raw = messageInput.getText(); -// String text = (raw == null) ? "" : raw.trim(); -// if (text.isEmpty()) return; -// -// if (currentChat == null) { -// addSystemMessage("No chat is selected."); -// return; -// } -// -// // 1) Clear input immediately for good UX -// messageInput.clear(); -// -// // 2) Snapshot chat info (must be final for lambdas) -// final UUID targetChatId = currentChat.getId(); -// final String targetType = currentChat.getType(); // "private" | "group" | "channel" -// final String contentToSend = text; // effectively final -// -// // 3) Build the SAME JSON as your console method (for TEXT only) -// org.json.JSONObject req = new org.json.JSONObject(); -// req.put("action", "send_message"); -// req.put("receiver_type", targetType); -// req.put("receiver_id", targetChatId.toString()); -// req.put("content", contentToSend); -// req.put("message_type", "TEXT"); -// -// // 4) Send on a background thread -// new Thread(() -> { -// org.json.JSONObject resp; -// try { -// resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); -// } catch (Exception ex) { -// ex.printStackTrace(); -// Platform.runLater(() -> addSystemMessage("Send failed: " + ex.getMessage())); -// return; -// } -// -// // 5) Check status like your console method -// if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { -// String err = (resp != null) ? resp.optString("message", "No response") : "No response"; -// Platform.runLater(() -> addSystemMessage("Send failed: " + err)); -// return; -// } -// -// // 6) Extract fields (your console reads data.message_id; handle both shapes) -// org.json.JSONObject data = resp.optJSONObject("data"); -// String messageId = null; -// String sendAtIso = null; -// if (data != null) { -// // If server returns { data: { message_id, send_at, ... } } -// messageId = data.optString("message_id", null); -// -// // Some servers nest: { data: { message: {...} } } -// if (messageId == null) { -// org.json.JSONObject msgObj = data.optJSONObject("message"); -// if (msgObj != null) { -// messageId = msgObj.optString("message_id", null); -// sendAtIso = msgObj.optString("send_at", null); -// } -// } else { -// sendAtIso = data.optString("send_at", null); -// } -// } -// if (messageId == null) messageId = java.util.UUID.randomUUID().toString(); -// -// final java.time.LocalDateTime ts = -// (sendAtIso != null && !sendAtIso.isBlank()) ? parseWhen(sendAtIso) -// : java.time.LocalDateTime.now(); -// -// final String fMessageId = messageId; -// final java.time.LocalDateTime fTs = ts; -// -// // 7) Update UI on FX thread (render outgoing bubble + index for reply previews) -// Platform.runLater(() -> { -// // If user switched chats while sending, don’t render here -// if (currentChat == null || !currentChat.getId().equals(targetChatId)) return; -// -// addBubble( -// true, // outgoing -// "You", // display name -// "TEXT", // message type -// contentToSend, // content -// fTs, // timestamp -// fMessageId, // message_id -// "", "", "", // forwarded_from, forwarded_by, reply_to_id -// false, // edited -// null // reactions -// ); -// -// //Real time -// var mc = MainController.getInstance(); -// if (mc != null) { -// String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend); -// mc.onChatUpdated(targetChatId, targetType, fTs, /*isIncoming*/ false, preview); -// } -// -// // Keep it in msgIndex for reply previews -// org.json.JSONObject idx = new org.json.JSONObject(); -// idx.put("message_id", fMessageId); -// idx.put("message_type", "TEXT"); -// idx.put("content", contentToSend); -// idx.put("sender_name", "You"); -// idx.put("sender_id", (me != null) ? me.toString() : ""); -// idx.put("send_at", fTs.toString()); -// msgIndex.put(fMessageId, idx); -// }); -// }).start(); -// } - private void sendMessage() { - // 1) متن ورودی String raw = messageInput.getText(); String text = (raw == null) ? "" : raw.trim(); if (text.isEmpty()) return; @@ -552,15 +396,12 @@ public class ChatPageController { return; } - // UX بهتر: اینپوت را سریع خالی کن messageInput.clear(); - final UUID chatId = currentChat.getId(); - final String cType = currentChat.getType(); + final UUID chatId = currentChat.getId(); + final String cType = currentChat.getType(); + - // ========================= - // A) حالت EDIT - // ========================= if (pendingEditMsgId != null) { final String msgIdForEdit = pendingEditMsgId; pendingEditMsgId = null; @@ -574,9 +415,8 @@ public class ChatPageController { JSONObject resp = ActionHandler.sendWithResponse(req); Platform.runLater(() -> { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { - addSystemMessage("Edit failed: " + (resp == null ? "no response" : resp.optString("message",""))); + addSystemMessage("Edit failed: " + (resp == null ? "no response" : resp.optString("message", ""))); } else { - // ساده‌ترین راه: پیام‌ها را دوباره بخوان loadMessages(currentChat); } }); @@ -584,16 +424,11 @@ public class ChatPageController { return; } - // ========================= - // B) حالت REPLY - // ========================= if (pendingReplyToId != null) { final String replyTo = pendingReplyToId; pendingReplyToId = null; - // اگر بالای کامپوزر پریویو ریپلای گذاشته‌ای، پاکش کن (اختیاری) if (!composerPane.getChildren().isEmpty()) { - // اگر عنصر اول preview است، حذف کن composerPane.getChildren().remove(0); } @@ -608,10 +443,9 @@ public class ChatPageController { JSONObject resp = ActionHandler.sendWithResponse(req); Platform.runLater(() -> { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { - addSystemMessage("Reply failed: " + (resp == null ? "no response" : resp.optString("message",""))); + addSystemMessage("Reply failed: " + (resp == null ? "no response" : resp.optString("message", ""))); } else { - // می‌توانی مثل حالت عادی حباب optimistic بسازی. - // ساده: رفرش لیست پیام‌ها + loadMessages(currentChat); } }); @@ -619,9 +453,7 @@ public class ChatPageController { return; } - // ========================= - // C) حالت عادی (send_message) - // ========================= + final String contentToSend = text; JSONObject req = new JSONObject() @@ -647,7 +479,6 @@ public class ChatPageController { return; } - // استخراج message_id و زمان JSONObject data = resp.optJSONObject("data"); String messageId = null; String sendAtIso = null; @@ -656,7 +487,6 @@ public class ChatPageController { messageId = data.optString("message_id", null); sendAtIso = data.optString("send_at", null); - // یا { data: { message: {...} } } if (messageId == null) { JSONObject msgObj = data.optJSONObject("message"); if (msgObj != null) { @@ -675,7 +505,6 @@ public class ChatPageController { final LocalDateTime fTs = ts; Platform.runLater(() -> { - // اگر کاربر چت را عوض کرده بود، چیزی رندر نکن if (currentChat == null || !currentChat.getId().equals(chatId)) return; // حباب outgoing @@ -694,21 +523,19 @@ public class ChatPageController { ); - // آپدیت پیش‌نمایش لیست چت‌ها var mc = MainController.getInstance(); if (mc != null) { String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend); mc.onChatUpdated(chatId, cType, fTs, /*isIncoming*/ false, preview); } - // برای reply-preview بعدی، پیام را ایندکس کن JSONObject idx = new JSONObject(); - idx.put("message_id", fMessageId); + idx.put("message_id", fMessageId); idx.put("message_type", "TEXT"); - idx.put("content", contentToSend); - idx.put("sender_name", "You"); - idx.put("sender_id", (me != null) ? me.toString() : ""); - idx.put("send_at", fTs.toString()); + idx.put("content", contentToSend); + idx.put("sender_name", "You"); + idx.put("sender_id", (me != null) ? me.toString() : ""); + idx.put("send_at", fTs.toString()); msgIndex.put(fMessageId, idx); }); }).start(); @@ -718,103 +545,48 @@ public class ChatPageController { FileChooser fc = new FileChooser(); fc.setTitle("Select image or audio"); fc.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Images", "*.png","*.jpg","*.jpeg","*.gif","*.bmp","*.webp"), - new FileChooser.ExtensionFilter("Audio", "*.mp3","*.wav","*.m4a","*.ogg","*.aac") + new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp"), + new FileChooser.ExtensionFilter("Audio", "*.mp3", "*.wav", "*.m4a", "*.ogg", "*.aac") ); File file = fc.showOpenDialog(attachmentButton.getScene().getWindow()); if (file == null) return; - String type = guessType(file); // برگرداندن "IMAGE" یا "AUDIO" - if (type == null) { toast("Only image or audio"); return; } + String type = guessType(file); + if (type == null) { + toast("Only image or audio"); + return; + } - if (currentChat == null) { toast("Not available chat"); return; } - UUID receiverId = currentChat.getId(); // همون chat_id + if (currentChat == null) { + toast("Not available chat"); + return; + } + UUID receiverId = currentChat.getId(); 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"; + 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); @@ -844,18 +616,16 @@ public class ChatPageController { ProgressIndicator spinner = new ProgressIndicator(); spinner.setPrefSize(14, 14); Label status = new Label("Sending..."); - status.getProperties().put("role", "statusLabel"); // برای آپدیت بعدی + 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); @@ -874,33 +644,12 @@ public class ChatPageController { private void toast(String msg) { - // هر جور که خودت نوتیف/Toast داری System.out.println("ℹ️ " + msg); if (messageInput != null) { messageInput.setTooltip(new Tooltip(msg)); } } - // ----- UI helpers ----- - -// /** -// * Add a normal message bubble (very simple for now). -// */ -// public void addMessage(String sender, String content) { -// Label msg = new Label(sender + ": " + content); -// msg.setWrapText(true); -// -// boolean dark = themeManager.isDarkMode(); -// String bubbleColor = dark ? "#20405a" : "#4fa8f0"; -// String textColor = dark ? "#e8f1f8" : "#0f141a"; -// msg.setStyle( -// "-fx-background-color: " + bubbleColor + ";" + -// "-fx-text-fill: " + textColor + ";" + -// "-fx-padding: 6 10; -fx-background-radius: 10;" -// ); -// -// messageContainer.getChildren().add(msg); -// } public void addSystemMessage(String content) { Label sys = new Label(content); @@ -910,38 +659,6 @@ public class ChatPageController { messageScrollPane.setVvalue(1.0); } -// /** -// * Update all header/footer icons according to current theme. -// */ -// private void syncIconsWithTheme() { -// boolean dark = themeManager.isDarkMode(); -// // We use “_light” icons on dark backgrounds, and “_dark” on light backgrounds. -// String suffix = dark ? "_light.png" : "_dark.png"; -// -// // attachment -// if (attachmentIcon != null) { -// attachmentIcon.setImage(loadIcon("attachment" + suffix)); -// } -// // send -// if (sendIcon != null) { -// sendIcon.setImage(loadIcon("send_cyan2.png")); -// } -// // header icons -// if (searchIcon != null) { -// searchIcon.setImage(loadIcon("search" + suffix)); -// } -// if (moreIcon != null) { -// moreIcon.setImage(loadIcon("more" + suffix)); -// } -// -// // header text tint (if you’re not fully relying on CSS) -// if (chatTitle != null) chatTitle.setStyle(dark ? "-fx-text-fill:#e8f1f8;" : "-fx-text-fill:#0f141a;"); -// if (chatStatus != null) chatStatus.setStyle(dark ? "-fx-text-fill:#8ea1b2;" : "-fx-text-fill:#7e8a97;"); -// -// // View profile icon in more button -// ((ImageView) viewProfileItem.getGraphic()) -// .setImage(loadIcon("view_profile" + suffix)); -// } private Image loadIcon(String filename) { var url = getClass().getResource(ICON_BASE + filename); @@ -952,64 +669,6 @@ public class ChatPageController { return new Image(url.toExternalForm()); } -// public void showChat(ChatEntry entry) { -// this.currentChat = entry; -// -// chatTitle.setText(entry.getName()); -// -// // آواتار پیش‌فرض بر اساس نوع -// // ChatPageController.showChat(...) -// if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { -// Image img = AvatarLocalResolver.load(entry.getImageUrl()); -// if (img != null) { -// userAvatar.setImage(img); -// } else { -// // ⬇️ فال‌بک بر اساس نوع -// setDefaultHeaderAvatarByType(entry.getType()); -// } -// } else { -// setDefaultHeaderAvatarByType(entry.getType()); -// } - //// userAvatar.setClip(new Circle(20, 20, 20)); -// AvatarFX.circleClip(userAvatar, 36); -// -// -// -// fetchAndRenderHeader(entry); -// -// // پیام‌ها -// messageContainer.getChildren().clear(); -// loadMessages(entry); -// markAsRead(entry); -// -// Platform.runLater(() -> messageInput.requestFocus()); -// } - -// -// public void showChat(ChatEntry entry) { -// this.currentChat = entry; -// this.chatName = entry.getName(); // برای لاگ/منو -// -// chatTitle.setText(entry.getName()); -// if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { -// Image img = AvatarLocalResolver.load(entry.getImageUrl()); -// if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType()); -// } else { -// setDefaultHeaderAvatarByType(entry.getType()); -// } -// AvatarFX.circleClip(userAvatar, 36); -// -// fetchAndRenderHeader(entry); -// -// messageContainer.getChildren().clear(); -// loadMessages(entry); -// markAsRead(entry); -// -// applyMode(ChatViewMode.NORMAL); -// -// Platform.runLater(() -> messageInput.requestFocus()); -// } - public void showChat(ChatEntry entry) { this.currentChat = entry; @@ -1018,17 +677,17 @@ public class ChatPageController { chatTitle.setText(entry.getName()); if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { Image img = AvatarLocalResolver.load(entry.getImageUrl()); - if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType()); + if (img != null) userAvatar.setImage(img); + else setDefaultHeaderAvatarByType(entry.getType()); } else { setDefaultHeaderAvatarByType(entry.getType()); } AvatarFX.circleClip(userAvatar, 36); - // حالت اولیه (بدون انتظار هدر) if ("channel".equalsIgnoreCase(entry.getType())) { boolean canPostLocal = entry.isOwner() || entry.isAdmin() - || (entry.getPermissions()!=null && entry.getPermissions().optBoolean("can_post", false)); + || (entry.getPermissions() != null && entry.getPermissions().optBoolean("can_post", false)); applyMode(canPostLocal ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); } else { applyMode(ChatViewMode.NORMAL); @@ -1038,21 +697,19 @@ public class ChatPageController { loadMessages(entry); markAsRead(entry); - // حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم fetchAndRenderHeader(entry); - // === (3-dot menu + header click) === - configureHeaderActions(entry); - - requestBlockStatusByChat(entry); -} + // === (3-dot menu + header click) === + configureHeaderActions(entry); + requestBlockStatusByChat(entry); + } public void requestBlockStatusByChat(ChatEntry entry) { if (entry == null || !"private".equalsIgnoreCase(entry.getType())) return; - String viewerId = Session.getUserUUID(); // internal_uuid کاربر فعلی + String viewerId = Session.getUserUUID(); if (viewerId == null || viewerId.isBlank()) return; JSONObject req = new JSONObject() @@ -1066,7 +723,7 @@ public class ChatPageController { JSONObject data = res.optJSONObject("data"); boolean blockedByMe = data != null && data.optBoolean("blocked_by_me", false); - boolean blockedMe = data != null && data.optBoolean("blocked_me", false); + boolean blockedMe = data != null && data.optBoolean("blocked_me", false); Platform.runLater(() -> applyBlockUi(blockedByMe, blockedMe)); }).start(); @@ -1074,23 +731,20 @@ public class ChatPageController { public void applyBlockUi(boolean blockedByMe, boolean blockedMe) { this.blockedByMeFlag = blockedByMe; - this.blockedMeFlag = blockedMe; + this.blockedMeFlag = blockedMe; if (blockedByMe) { - // من طرف مقابل را بلاک کرده‌ام → فقط دکمه UNBLOCK نمایش بده if (readOnlyLabel != null) readOnlyLabel.setText(""); applyMode(ChatViewMode.BLOCKED); return; } if (blockedMe) { - // طرف مقابل من را بلاک کرده → متن read-only مخصوص if (readOnlyLabel != null) readOnlyLabel.setText("YOU ARE BLOCKED"); applyMode(ChatViewMode.READ_ONLY); return; } - // هیچ‌کس بلاک نکرده applyMode(ChatViewMode.NORMAL); } @@ -1098,6 +752,7 @@ public class ChatPageController { public void showChat(ChatEntry entry, ChatViewMode mode) { this.currentChat = entry; + Session.currentChatEntry = entry; // --- Header --- chatTitle.setText(entry.getName()); if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { @@ -1152,11 +807,13 @@ public class ChatPageController { deleteChatItem.setOnAction(e -> deleteChatButton(entry)); } case "group" -> { + archiveItem.setVisible(true); viewGroupItem.setVisible(true); leaveGroupItem.setVisible(true); + archiveItem.setOnAction(e -> toggleArchive(entry)); viewGroupItem.setOnAction(e -> openInfoScene(entry)); - leaveGroupItem.setOnAction(e -> leaveGroupButton(entry)); + leaveGroupItem.setOnAction(e -> onLeaveGroupMenuClicked(entry)); } case "channel" -> { viewChannelItem.setVisible(true); @@ -1357,7 +1014,7 @@ public class ChatPageController { if (!currentlyArchived) { // ARCHIVE req.put("action", "archive_chat") - .put("chat_type", entry.getType()); // سرور می‌خواهد + .put("chat_type", entry.getType()); } else { // UNARCHIVE req.put("action", "unarchive_chat"); @@ -1379,18 +1036,22 @@ public class ChatPageController { } Session.sortListsByLastMessage(); - // 2) تازه‌سازی UI لیست‌ها try { - MainController.getInstance().refreshChatListUI(); // پیاده‌سازی در بخش 3 - } catch (Exception ignore) {} - - // 3) اگر وارد نمای آرشیو هستیم و آن‌آرشیو شد، بلافاصله از لیست آرشیو حذف شود - if (Session.inArchivedView && !Session.isArchived(entry.getId())) { - try { MainController.getInstance().refreshArchivedListUI(); } catch (Exception ignore) {} + MainController.getInstance().refreshChatListUI(); + } catch (Exception ignore) { } - // 4) اگر تازه برای اولین بار آرشیو داریم، ردیف "Archived Chats" در بالای لیست ظاهر شود - try { MainController.getInstance().ensureArchivedHeaderRow(); } catch (Exception ignore) {} + if (Session.inArchivedView && !Session.isArchived(entry.getId())) { + try { + MainController.getInstance().refreshArchivedListUI(); + } catch (Exception ignore) { + } + } + + try { + MainController.getInstance().ensureArchivedHeaderRow(); + } catch (Exception ignore) { + } } else { String msg = (resp != null ? resp.optString("message", "Unknown error") @@ -1446,105 +1107,12 @@ public class ChatPageController { } -// private void renderMessages(org.json.JSONArray arr) { -// messageContainer.getChildren().clear(); -// -// String myId = Session.currentUser != null -// ? Session.currentUser.optString("internal_uuid", "") -// : ""; -// -// for (int i = 0; i < arr.length(); i++) { -// org.json.JSONObject m = arr.getJSONObject(i); -// -// String senderId = m.has("sender_id") && !m.isNull("sender_id") ? m.getString("sender_id") : ""; -// String senderName = m.has("sender_name") && !m.isNull("sender_name") ? m.getString("sender_name") : ""; -// String type = m.has("message_type") && !m.isNull("message_type") ? m.getString("message_type") : "TEXT"; -// String content = m.has("content") && !m.isNull("content") ? m.getString("content") : ""; -// String whenStr = m.has("send_at") && !m.isNull("send_at") ? m.getString("send_at") : null; -// -// boolean outgoing = senderId.equalsIgnoreCase(myId); -// -// String display = outgoing ? "You" -// : (!senderName.isEmpty() ? senderName -// : (senderId.isEmpty() ? "Unknown" -// : senderId.substring(0, Math.min(8, senderId.length())))); -// -// String text; -// switch (type.toUpperCase()) { -// case "IMAGE": text = "[Image]"; break; -// case "AUDIO": text = "[Audio]"; break; -// case "VIDEO": text = "[Video]"; break; -// case "FILE": text = "[File]"; break; -// default: text = content; break; -// } -// -// LocalDateTime ts = parseWhen(whenStr); -// addBubble(outgoing, display, text, ts); -// } -// -// messageScrollPane.layout(); -// messageScrollPane.setVvalue(1.0); -// } - - private final java.util.Map msgIndex = new java.util.HashMap<>(); -// private void renderMessages(org.json.JSONArray list) { -// messageContainer.getChildren().clear(); -// -// // برای reply-preview: ایندکس کردن پیام‌ها با message_id -// msgIndex.clear(); -// for (int i = 0; i < list.length(); i++) { -// org.json.JSONObject m = list.getJSONObject(i); -// String mid = str(m, "message_id"); -// if (!mid.isEmpty()) msgIndex.put(mid, m); -// } -// -// String myId = (Session.currentUser != null && Session.currentUser.has("internal_uuid")) -// ? Session.currentUser.getString("internal_uuid") : ""; -// -// for (int i = 0; i < list.length(); i++) { -// org.json.JSONObject m = list.getJSONObject(i); -// -// String senderId = str(m, "sender_id"); -// String senderName = str(m, "sender_name"); -// String type = str(m, "message_type"); -// String content = str(m, "content"); -// String whenStr = str(m, "send_at"); -// String msgId = str(m, "message_id"); -// -// // فوروارد / ریپلای / ادیت / ری‌اکشن -// String fwdFrom = nz(str(m, "forwarded_from")); -// String fwdBy = nz(str(m, "forwarded_by")); -// String replyToId = nz(str(m, "reply_to_id")); -// boolean edited = bool(m, "is_edited"); -// org.json.JSONArray reactions = arr(m, "reactions"); -// -// boolean outgoing = senderId.equalsIgnoreCase(myId); -// if (senderName == null || senderName.isBlank()) { -// senderName = outgoing ? "You" -// : (senderId == null || senderId.isBlank() -// ? "Unknown" -// : senderId.substring(0, Math.min(8, senderId.length()))); -// } -// -// java.time.LocalDateTime ts = parseWhen(whenStr); -// -// // نمایش -// addBubble(outgoing, senderName, type, content, ts, msgId, -// fwdFrom, fwdBy, replyToId, edited, reactions); -// } -// -// // کمی فاصله بین پیام‌ها -// messageContainer.setSpacing(8); -// messageScrollPane.layout(); -// messageScrollPane.setVvalue(1.0); -// } private 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); @@ -1558,22 +1126,20 @@ public class ChatPageController { 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"); // این همون کپشنه برای IMAGE - String whenStr = str(m, "send_at"); - String msgId = str(m, "message_id"); + 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"); - // 👇 جدید: URL ها - String fileUrl = str(m, "file_url"); - String thumbUrl = str(m, "thumb_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")); - String replyToId = nz(str(m, "reply_to_id")); - boolean edited = bool(m, "is_edited"); + 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); @@ -1586,7 +1152,6 @@ public class ChatPageController { java.time.LocalDateTime ts = parseWhen(whenStr); - // 👇 امضای جدید addBubble با fileUrl/thumbUrl addBubble(outgoing, senderName, type, content, ts, msgId, fwdFrom, fwdBy, replyToId, edited, reactions, fileUrl, thumbUrl); } @@ -1597,8 +1162,8 @@ public class ChatPageController { } - private String shortId(String id){ - return (id==null||id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8,id.length())); + private String shortId(String id) { + return (id == null || id.isEmpty()) ? "Unknown" : id.substring(0, Math.min(8, id.length())); } private void markAsRead(ChatEntry entry) { @@ -1609,242 +1174,12 @@ public class ChatPageController { ActionHandler.sendWithResponse(readReq); } -// private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { -// -// Label meta = new Label(displayName + " • " + formatWhen(sentAt)); -// meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); -// meta.setWrapText(true); -// -// Label msg = new Label(content); -// msg.setWrapText(true); -// -// boolean dark = themeManager.isDarkMode(); -// String mine = dark ? "#2b7cff" : "#d8ecff"; -// String theirs = dark ? "#2c333a" : "#ffffff"; -// String bg = outgoing ? mine : theirs; -// -// msg.setStyle( -// "-fx-background-color:" + bg + ";" + -// "-fx-padding:8 12;" + -// "-fx-background-radius:12;" + -// "-fx-max-width: 520;" -// ); -// msg.setMinHeight(Region.USE_PREF_SIZE); -// -// VBox bubble = new VBox(4, meta, msg); -// -// javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); -// row.setFillHeight(true); -// row.setSpacing(6); -// row.setAlignment(outgoing -// ? javafx.geometry.Pos.CENTER_RIGHT -// : javafx.geometry.Pos.CENTER_LEFT); -// -// messageContainer.getChildren().add(row); -// } - -// private void addBubble( -// boolean outgoing, -// String displayName, -// String type, -// String content, -// java.time.LocalDateTime sentAt, -// String messageId, -// String forwardedFrom, -// String forwardedBy, -// String replyToId, -// boolean edited, -// org.json.JSONArray reactions -// ) { -// String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt); -// if (edited) metaText += " (edited)"; -// Label meta = new Label(metaText); -// meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); -// meta.setWrapText(true); -// -// String t = type == null ? "" : type.trim().toUpperCase(); -// boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); -// String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t); -// -// Label msg = new Label(bodyText); -// msg.setWrapText(true); -// -// boolean dark = themeManager.isDarkMode(); -// String mine = dark ? "#2b7cff" : "#d8ecff"; -// String theirs = dark ? "#2c333a" : "#f2f4f7"; -// String bg = outgoing ? mine : theirs; -// -// msg.setStyle( -// "-fx-background-color:" + bg + ";" + -// "-fx-padding:8 12;" + -// "-fx-background-radius:12;" + -// "-fx-max-width: 520;" -// ); -// msg.setMinHeight(Region.USE_PREF_SIZE); -// -// // بدنه حباب -// VBox bubble = new VBox(4); // spacing عمودی داخل حباب -// bubble.getChildren().add(meta); -// -// // Forward header (اختیاری) -// if (hasVal(forwardedFrom) || hasVal(forwardedBy)) { -// bubble.getChildren().add(buildForwardHeader(forwardedFrom, forwardedBy)); -// } -// -// // Reply preview (اختیاری) -// if (hasVal(replyToId)) { -// bubble.getChildren().add(buildReplyBoxFromIndex(replyToId)); -// } -// -// // متن اصلی -// bubble.getChildren().add(msg); -// -// // Reactions (اختیاری) -// if (reactions != null && reactions.length() > 0) { -// bubble.getChildren().add(buildReactionsBarFromJson(reactions, dark)); -// } -// -// // چیدمان راست/چپ -// javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); -// row.setFillHeight(true); -// row.setSpacing(4); -// row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT -// : javafx.geometry.Pos.CENTER_LEFT); -// -// row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6)); -// -// messageContainer.getChildren().add(row); -// -// // ... inside addBubble(...) after creating 'row' -// 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, -// 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, // برای IMAGE = کپشن + String content, java.time.LocalDateTime sentAt, String messageId, String forwardedFrom, @@ -1852,8 +1187,8 @@ public class ChatPageController { String replyToId, boolean edited, org.json.JSONArray reactions, - String fileUrl, // 👈 جدید - String thumbUrl // 👈 جدید + String fileUrl, + String thumbUrl ) { String metaText = (displayName == null ? "" : displayName) + " • " + formatWhen(sentAt); if (edited) metaText += " (edited)"; @@ -1865,9 +1200,8 @@ public class ChatPageController { String t = type == null ? "" : type.trim().toUpperCase(); boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t); - // رنگ پس‌زمینه برای متن (برای عکس پس‌زمینه نمی‌ذاریم تا تمیز باشه) boolean dark = themeManager.isDarkMode(); - String mine = dark ? "#2b7cff" : "#d8ecff"; + String mine = dark ? "#2b7cff" : "#d8ecff"; String theirs = dark ? "#2c333a" : "#f2f4f7"; VBox bubble = new VBox(4); @@ -1898,12 +1232,10 @@ public class ChatPageController { 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; @@ -1914,7 +1246,6 @@ public class ChatPageController { bubble.getChildren().add(ph); } else { - // ناشناخته Label ph = new Label("[" + t + "]"); ph.setWrapText(true); String bg = outgoing ? mine : theirs; @@ -1944,7 +1275,10 @@ public class ChatPageController { 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()); @@ -1992,12 +1326,9 @@ public class ChatPageController { } - - 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)); @@ -2012,7 +1343,9 @@ public class ChatPageController { 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.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) st.close(); + }); st.show(); } @@ -2021,27 +1354,21 @@ public class ChatPageController { 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"); @@ -2056,13 +1383,11 @@ public class ChatPageController { menu.getItems().add(reactionsItem); menu.getItems().add(new SeparatorMenuItem()); - // --- 2.2 گزینه‌های مشترک --- MenuItem reply = new MenuItem("Reply"); reply.setOnAction(ae -> startReply(messageId)); MenuItem forward = new MenuItem("Forward"); forward.setOnAction(ae -> startForward(messageId)); - // (اختیاری) کپی متن فقط برای TEXT if ("TEXT".equalsIgnoreCase(nz(type)) && hasVal(content)) { MenuItem copy = new MenuItem("Copy"); copy.setOnAction(ae -> { @@ -2075,7 +1400,6 @@ public class ChatPageController { } menu.getItems().addAll(reply, forward); - // --- 2.3 فقط برای پیام‌های خودم: Edit/Delete --- if (isMine) { MenuItem edit = new MenuItem("Edit"); edit.setOnAction(ae -> startEdit(messageId, content)); @@ -2093,26 +1417,30 @@ public class ChatPageController { } - private String bracketLabel(String t) { String tt = (t == null) ? "" : t.trim().toUpperCase(); switch (tt) { - case "IMAGE": return "[Image]"; - case "AUDIO": return "[Audio]"; - case "VIDEO": return "[Video]"; - case "FILE": return "[File]"; - default: return "[Message]"; + case "IMAGE": + return "[Image]"; + case "AUDIO": + return "[Audio]"; + case "VIDEO": + return "[Video]"; + case "FILE": + return "[File]"; + default: + return "[Message]"; } } private javafx.scene.Node buildForwardHeader(String forwardedFrom, String forwardedBy) { String from = hasVal(forwardedFrom) ? forwardedFrom.trim() : null; - String by = hasVal(forwardedBy) ? forwardedBy.trim() : null; + String by = hasVal(forwardedBy) ? forwardedBy.trim() : null; String txt = (from != null && by != null) ? ("Forwarded from " + from + " by " + by) : (from != null) ? ("Forwarded from " + from) - : (by != null) ? ("Forwarded by " + by) + : (by != null) ? ("Forwarded by " + by) : "Forwarded"; Label l = new Label(txt); @@ -2127,9 +1455,9 @@ public class ChatPageController { String preview; if (r != null) { - String rType = r.optString("message_type", "TEXT"); + String rType = r.optString("message_type", "TEXT"); String rContent = r.optString("content", ""); - String rSender = r.optString("sender_name", ""); + String rSender = r.optString("sender_name", ""); if (hasVal(rSender)) from = rSender; preview = "TEXT".equalsIgnoreCase(rType) ? rContent : bracketLabel(rType); @@ -2158,11 +1486,11 @@ public class ChatPageController { 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); + String emo = r.optString("emoji", "👍"); + int cnt = r.optInt("count", 1); Label chip = new Label(emo + " " + cnt); - chip.getStyleClass().add("emoji-label"); // 👈 کلاس CSS برای ایموجی + chip.getStyleClass().add("emoji-label"); chip.setStyle("-fx-background-color:" + (dark ? "#39424a" : "#e9eef3") + "; -fx-padding:3 8; -fx-background-radius:12;"); @@ -2172,9 +1500,9 @@ public class ChatPageController { } - - - 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) { return currentChat != null @@ -2182,236 +1510,55 @@ public class ChatPageController { && currentChat.getType().equalsIgnoreCase(type); } -// public void onRealTimeNewMessage(JSONObject m) { -// try { -// String chatIdStr = str(m,"receiver_id"); -// String chatType = str(m,"receiver_type"); -// if (chatIdStr.isEmpty() || chatType.isEmpty()) return; -// -// UUID chatId = UUID.fromString(chatIdStr); -// if (!isSameChat(chatId, chatType)) { -// System.out.println("[UI] RT msg for another chat: " + chatId); -// return; -// } -// -// // id → message_id fallback -// if (!m.has("message_id") && m.has("id")) { -// m.put("message_id", m.getString("id")); -// } -// -// String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") -// : (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown"); -// -// String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT"; -// String content = str(m,"content"); -// String whenIso = str(m,"send_at"); -// String msgId = str(m,"message_id"); -// -// LocalDateTime ts = parseWhen(whenIso); -// if (ts == null) ts = LocalDateTime.now(); -// -// addBubble(false, senderName, type, content, ts, msgId, -// str(m,"forwarded_from"), str(m,"forwarded_by"), str(m,"reply_to_id"), -// bool(m,"is_edited"), arr(m,"reactions")); -// -// if (hasVal(msgId)) msgIndex.put(msgId, m); -// -// if (currentChat != null) markAsRead(currentChat); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } - - -// 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 { - // 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",""))); + 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); - // 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"); + String msgId = str(m, "message_id"); if (!hasVal(msgId)) return; - // تکراری نساز if (messageNodes.containsKey(msgId)) return; - // 3) sender/name - String senderId = str(m,"sender_id"); - String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name") + String senderId = str(m, "sender_id"); + String senderName = hasVal(str(m, "sender_name")) ? str(m, "sender_name") : (hasVal(senderId) ? shortId(senderId) : "Unknown"); - // 4) نوع پیام (سرور ممکنه lowercase بده) - String tRaw = nz(m.optString("message_type","TEXT")); + 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",""))); + String content = nz(m.optString("content", m.optString("text", ""))); - // 6) زمان - String whenIso = nz(m.optString("send_at", m.optString("created_at",""))); + 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","")); + 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") بخوان + if (!hasVal(fileUrl)) fileUrl = nz(media.optString("url", "")); + if (!hasVal(thumbUrl)) thumbUrl = nz(media.optString("thumbnail_url", "")); } - // 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"); - org.json.JSONArray reacts = arr(m,"reactions"); + 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"); + org.json.JSONArray reacts = arr(m, "reactions"); - // 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); - // 11) آپدیت پریویو لیست چت‌ها String previewText = switch (type) { case "IMAGE" -> (hasVal(content) ? "🖼️ Photo — " + content : "🖼️ Photo"); case "AUDIO" -> "🎵 Audio"; @@ -2419,7 +1566,6 @@ public class ChatPageController { }; updateChatListPreview(chatId, chatType, !outgoing, previewText, type); - // 12) اگر چت جاری است، حباب بساز if (!isCurrent) return; addBubble(outgoing, senderName, type, content, ts, msgId, @@ -2434,7 +1580,6 @@ public class ChatPageController { } - private void updateChatListPreview(UUID chatId, String type, boolean incoming, String content, String messageType) { var mc = MainController.getInstance(); if (mc == null) return; @@ -2443,8 +1588,9 @@ public class ChatPageController { case "IMAGE" -> preview = (incoming ? "" : "You: ") + "[Image]"; case "AUDIO" -> preview = (incoming ? "" : "You: ") + "[Audio]"; case "VIDEO" -> preview = (incoming ? "" : "You: ") + "[Video]"; - case "FILE" -> preview = (incoming ? "" : "You: ") + "[File]"; - default -> preview = (incoming ? "" : "You: ") + (content == null || content.isBlank() ? "[Message]" : content); + case "FILE" -> preview = (incoming ? "" : "You: ") + "[File]"; + default -> + preview = (incoming ? "" : "You: ") + (content == null || content.isBlank() ? "[Message]" : content); } mc.onChatUpdated(chatId, type, LocalDateTime.now(), incoming, preview); } @@ -2453,10 +1599,8 @@ public class ChatPageController { String msgId = str(ev, "message_id"); if (!hasVal(msgId)) return; - // 1) ایندکس را به‌روزرسانی کن JSONObject idx = msgIndex.getOrDefault(msgId, new JSONObject().put("message_id", msgId)); - // اگر «counts» اومد (map emoji→count)، به آرایه تبدیل کن JSONArray reactions = ev.optJSONArray("reactions"); if (reactions == null) { JSONObject counts = ev.optJSONObject("counts"); @@ -2470,7 +1614,7 @@ public class ChatPageController { } } else if (ev.has("emoji")) { reactions = new JSONArray().put(new JSONObject() - .put("emoji", ev.optString("emoji","👍")) + .put("emoji", ev.optString("emoji", "👍")) .put("count", ev.optInt("count", 1))); } } @@ -2479,7 +1623,6 @@ public class ChatPageController { msgIndex.put(msgId, idx); } - // 2) اگر حبابش روی صفحه هست، فقط نوار ری‌اکشن را عوض کن Node row = messageNodes.get(msgId); if (!(row instanceof HBox hbox)) return; @@ -2487,7 +1630,10 @@ public class ChatPageController { if (child instanceof VBox bubble && msgId.equals(bubble.getProperties().get("messageId"))) { Node oldBar = null; for (Node bch : bubble.getChildren()) { - if ("reactionsBar".equals(bch.getProperties().get("role"))) { oldBar = bch; break; } + if ("reactionsBar".equals(bch.getProperties().get("role"))) { + oldBar = bch; + break; + } } if (oldBar != null) bubble.getChildren().remove(oldBar); @@ -2509,13 +1655,11 @@ public class ChatPageController { String newContent = str(ev, "new_content"); - // ایندکس JSONObject idx = msgIndex.getOrDefault(msgId, new JSONObject().put("message_id", msgId)); if (hasVal(newContent)) idx.put("content", newContent); idx.put("is_edited", true); msgIndex.put(msgId, idx); - // UI Node row = messageNodes.get(msgId); if (!(row instanceof HBox hbox)) return; @@ -2547,29 +1691,26 @@ public class ChatPageController { } - private void fetchAndRenderHeader(ChatEntry entry) { JSONObject req = new JSONObject(); req.put("action", "get_header_info"); req.put("receiver_id", entry.getId().toString()); - req.put("receiver_type", entry.getType()); // باید "private" باشه + req.put("receiver_type", entry.getType()); - String viewer = Session.getUserUUID(); // internal_uuid کاربر فعلی + String viewer = Session.getUserUUID(); if (viewer != null && !viewer.isBlank()) { req.put("viewer_id", viewer); } -// // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) -// UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری -// if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { -// req.put("viewer_id", viewerId.toString()); -// } new Thread(() -> { JSONObject resp; try { resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); - } catch (Exception ex) { ex.printStackTrace(); return; } + } catch (Exception ex) { + ex.printStackTrace(); + return; + } if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { System.err.println("get_header_info failed: " + (resp != null ? resp.optString("message") : "null resp")); @@ -2586,38 +1727,12 @@ public class ChatPageController { String t = entry.getType() == null ? "" : entry.getType().toLowerCase(); switch (t) { case "private" -> updatePrivateHeader(entry, data); - case "group" -> updateGroupHeader(entry, data); + case "group" -> updateGroupHeader(entry, data); case "channel" -> updateChannelHeader(entry, data); - default -> chatStatus.setText(""); + default -> chatStatus.setText(""); } } -// private void updatePrivateHeader(ChatEntry entry, JSONObject data) { -// String name = nz(data.optString("profile_name", entry.getName())); -// chatTitle.setText(name); -// -// // other_user_id برای ریل‌تایم status -// String other = data.optString("other_user_id", ""); -// if (!other.isBlank()) { -// try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} -// } -// -// // تصویر -// String img = data.optString("image_url", ""); -// if (hasVal(img)) { -// try { -// Image im = AvatarLocalResolver.load(img); // ⬅️ -// if (im != null) userAvatar.setImage(im); -// userAvatar.setClip(new Circle(20, 20, 20)); -// } catch (Exception ignore) {} -// } -// -// chatStatus.setText(userStatusText( -// data.optBoolean("online", false), -// data.optString("last_seen", null) -// )); -// -// } private void updatePrivateHeader(ChatEntry entry, JSONObject data) { String name = nz(data.optString("profile_name", entry.getName())); @@ -2625,7 +1740,10 @@ public class ChatPageController { String other = data.optString("other_user_id", ""); if (!other.isBlank()) { - try { entry.setOtherUserId(UUID.fromString(other)); } catch (Exception ignore) {} + try { + entry.setOtherUserId(UUID.fromString(other)); + } catch (Exception ignore) { + } } String img = data.optString("image_url", ""); @@ -2634,7 +1752,8 @@ public class ChatPageController { Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } } chatStatus.setText(userStatusText( @@ -2642,10 +1761,8 @@ public class ChatPageController { data.optString("last_seen", null) )); - // ❌ هیچ applyMode اینجا نزن! - // حتی اگر blocked آمد، تصمیم مود از بیرون می‌آید. - } + } private void updateGroupHeader(ChatEntry entry, JSONObject data) { @@ -2657,33 +1774,17 @@ public class ChatPageController { Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } } int members = data.optInt("member_count", 0); - int online = data.optInt("online_count", -1); + int online = data.optInt("online_count", -1); chatStatus.setText(online >= 0 ? (members + " members, " + online + " online") : (members + " members")); - // ❌ هیچ applyMode اینجا نزن! - } -// private void updateChannelHeader(ChatEntry entry, JSONObject data) { -// chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); -// -// String img = data.optString("image_url", ""); -// if (hasVal(img)) { -// try { -// Image im = AvatarLocalResolver.load(img); // ⬅️ -// if (im != null) userAvatar.setImage(im); -// userAvatar.setClip(new Circle(20, 20, 20)); -// } catch (Exception ignore) {} -// } -// -// -// int subs = data.optInt("member_count", 0); -// chatStatus.setText(subs + " subscribers"); -// } + } private void updateChannelHeader(ChatEntry entry, JSONObject data) { @@ -2695,16 +1796,15 @@ public class ChatPageController { Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } } int subs = data.optInt("member_count", 0); chatStatus.setText(subs + " subscribers"); - // ❌ هیچ applyMode اینجا نزن! - // حتی اگر can_post را بده، به مود دست نزن. - } + } public void onUserStatusChanged(String userUuid, String status, String lastSeenIso) { @@ -2725,7 +1825,6 @@ public class ChatPageController { } - public String userStatusText(boolean online, String lastSeenIso) { if (online) return "online"; @@ -2746,52 +1845,17 @@ public class ChatPageController { return "last seen recently"; } - private void setDefaultHeaderAvatarByType(String type){ + private void setDefaultHeaderAvatarByType(String type) { String path = switch (type == null ? "" : type.toLowerCase()) { case "channel" -> "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; - case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; - default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; + default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; }; userAvatar.setImage(new Image( java.util.Objects.requireNonNull(getClass().getResourceAsStream(path)) )); } -// @FXML -// private void onJoinClicked() { -// if (currentChat == null) return; -// -// // 1) internal_uuid کاربر فعلی (UUID) -// String myInternalUuid = Session.currentUser != null -// ? Session.currentUser.optString("internal_uuid", "") -// : ""; -// if (myInternalUuid.isBlank()) { -// addSystemMessage("Join failed: missing current user internal_uuid."); -// return; -// } -// -// // 2) internal_uuid مقصد (گروه/کانال) -// String targetId = currentChat.getId().toString(); -// -// // 3) نوع و نام اکشن -// String t = currentChat.getType(); -// String action = "group".equalsIgnoreCase(t) ? "join_group" : "join_channel"; -// -// // 4) درخواست طبق قرارداد سرور (کلیدها: user_id = UUID کاربر، id = UUID مقصد) -// JSONObject req = new JSONObject() -// .put("action", action) -// .put("user_id", myInternalUuid) // ← UUID -// .put("id", targetId); // ← UUID گروه/کانال -// -// JSONObject res = ActionHandler.sendWithResponse(req); -// if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { -// MainController.getInstance().onJoinedOrAdded(currentChat); -// applyMode(ChatViewMode.NORMAL); -// Platform.runLater(() -> messageInput.requestFocus()); -// } else { -// addSystemMessage("Join failed: " + (res != null ? res.optString("message","") : "no response")); -// } -// } @FXML private void onJoinClicked() { @@ -2811,8 +1875,8 @@ public class ChatPageController { JSONObject req = new JSONObject() .put("action", action) - .put("user_id", myInternalUuid) // UUID من - .put("id", targetId); // UUID مقصد + .put("user_id", myInternalUuid) + .put("id", targetId); new Thread(() -> { JSONObject res = ActionHandler.sendWithResponse(req); @@ -2820,98 +1884,33 @@ public class ChatPageController { Platform.runLater(() -> { if (!ok) { - addSystemMessage("Join failed: " + (res != null ? res.optString("message","") : "no response")); + addSystemMessage("Join failed: " + (res != null ? res.optString("message", "") : "no response")); return; } - // برای جلوگیری از داون‌گِرید مود توسط هِدرِ بعدی: justJoinedThisChat = true; - // اگر MainController نال نبود، لیست چت‌ها را آپدیت کن var mc = MainController.getInstance(); if (mc != null) { mc.onJoinedOrAdded(currentChat); } - // ⚠️ منطق: گروه → همیشه Composer باز شود. - // کانال → اگر اجازه‌ی پست داری، Composer؛ وگرنه READ_ONLY با پیام. -// if ("group".equalsIgnoreCase(t)) { -// applyMode(ChatViewMode.NORMAL); -// messageInput.requestFocus(); -// } else if ("channel".equalsIgnoreCase(t)) { -// // اگر می‌خواهی «همان‌جا» Composer فعال شود، باید اجازه‌ی پست را -// // یا از سرور بگیری یا لوکال ست کنی (طبق بیزینس‌لاک‌ت). -// // این‌جا منطقی‌تر: فقط اگر واقعاً اجازه داری. -// boolean canPost = -// (currentChat.isOwner() || currentChat.isAdmin()) || -// (currentChat.getPermissions()!=null && currentChat.getPermissions().optBoolean("can_post", false)); -// if (canPost) { -// applyMode(ChatViewMode.NORMAL); -// messageInput.requestFocus(); -// } else { -// if (readOnlyLabel != null) -// readOnlyLabel.setText("YOU CAN’T SEND MESSAGES IN THIS CHANNEL"); -// applyMode(ChatViewMode.READ_ONLY); -// } -// } else { -// applyMode(ChatViewMode.NORMAL); -// messageInput.requestFocus(); -// } if ("group".equalsIgnoreCase(t)) { applyMode(ChatViewMode.NORMAL); } else if ("channel".equalsIgnoreCase(t)) { - // تصمیم بیزینسی: ثبت کن. - applyMode(ChatViewMode.READ_ONLY); // یا NORMAL اگر همین را می‌خواهی + applyMode(ChatViewMode.READ_ONLY); } - // هدر را دوباره بگیر (ولی نگذار مود را خراب کند) fetchAndRenderHeader(currentChat); }); }).start(); } -// -// @FXML -// private void onAddContactClicked() { -// if (currentChat == null) return; -// -// String myUserId = Session.currentUser != null -// ? Session.currentUser.optString("user_id", "") -// : ""; -// -// // 2) internal_uuid طرف مقابل -// UUID other = currentChat.getOtherUserId(); -// if (other == null) { -// // اگر otherUserId هنوز نگرفته‌ای، بهتره قبلش از هدر/پروفایل بیاری. -// addSystemMessage("Cannot add: other user UUID is missing."); -// return; -// } -// -// // 3) درخواست طبق قرارداد سرور -// JSONObject req = new JSONObject() -// .put("action", "add_contact") -// .put("user_id", myUserId) // ← stringِ user_id (غیر UUID) -// .put("contact_id", other.toString()); // ← UUID طرف مقابل -// -// // 4) ارسال -// JSONObject res = ActionHandler.sendWithResponse(req); -// if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { -// // به لیست چت‌ها اضافه و سوییچ به حالت نرمال -// MainController.getInstance().onJoinedOrAdded(currentChat); -// applyMode(ChatViewMode.NORMAL); -// Platform.runLater(() -> messageInput.requestFocus()); -// } else { -// addSystemMessage("Add contact failed: " + (res != null ? res.optString("message","") : "no response")); -// } -// } - - @FXML private void onAddContactClicked() { if (currentChat == null) return; - // user_id من (همان string که سرور انتظار دارد) String myUserId = Session.currentUser != null ? Session.currentUser.optString("user_id", "") : ""; @@ -2920,14 +1919,12 @@ public class ChatPageController { return; } - // UUID طرف مقابل (از هدر آمده) UUID other = currentChat.getOtherUserId(); if (other == null) { addSystemMessage("Cannot add: other user UUID is missing."); return; } - // درخواست به سرور JSONObject req = new JSONObject() .put("action", "add_contact") .put("user_id", myUserId) @@ -2939,21 +1936,19 @@ public class ChatPageController { Platform.runLater(() -> { if (!ok) { - addSystemMessage("Add contact failed: " + (res != null ? res.optString("message","") : "no response")); + addSystemMessage("Add contact failed: " + (res != null ? res.optString("message", "") : "no response")); return; } - // ✅ فقط به کانتکت‌های سشن اضافه کن (لوکالی) try { - // اگر مدل ContactEntry داری از همان استفاده کن - // این یک نمونه‌ی امن برای پر کردن حداقل فیلدهاست + org.to.telegramfinalproject.Models.ContactEntry ce = new org.to.telegramfinalproject.Models.ContactEntry( other, // contact_id (UUID) currentChat.getDisplayId(), // contact_display_id / user_id دیدنی - currentChat.getDisplayId(), // هر دو اگر یکی داری - nz(chatTitle.getText()), // نام نمایشی - currentChat.getImageUrl(), // آواتار (اگر هست) + currentChat.getDisplayId(), + nz(chatTitle.getText()), + currentChat.getImageUrl(), false, // is_blocked null // last_seen ); @@ -2964,7 +1959,8 @@ public class ChatPageController { boolean exists = Session.contactEntries.stream() .anyMatch(c -> other.equals(c.getContactId())); if (!exists) Session.contactEntries.add(ce); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } showOpenFromContactsHint(); @@ -2973,102 +1969,23 @@ public class ChatPageController { } -// private void applyMode(ChatViewMode mode) { -// currentMode = mode; -// -// boolean normal = (mode == ChatViewMode.NORMAL); -// boolean needsJoin = (mode == ChatViewMode.NEEDS_JOIN); -// boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); -// -// composerPane.setVisible(normal); -// composerPane.setManaged(normal); -// -// joinPane.setVisible(needsJoin); -// joinPane.setManaged(needsJoin); -// -// addContactPane.setVisible(needsAdd); -// addContactPane.setManaged(needsAdd); -// -// if (needsJoin && joinButton != null && currentChat != null) { -// String what = "channel".equalsIgnoreCase(currentChat.getType()) ? "CHANNEL" : "GROUP"; -// joinButton.setText(("Join " + what).toUpperCase()); // => JOIN CHANNEL / JOIN GROUP -// } -// if (needsAdd && addContactButton != null) { -// addContactButton.setText("ADD CONTACT"); -// } -// } - - - -// private void applyMode(ChatViewMode mode) { -// currentMode = mode; -// -// boolean normal = (mode == ChatViewMode.NORMAL); -// boolean needsJoin = (mode == ChatViewMode.NEEDS_JOIN); -// boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); -// boolean readOnly = (mode == ChatViewMode.READ_ONLY); -// boolean blocked = (mode == ChatViewMode.BLOCKED); -// -// // Composer فقط در حالت نرمال -// composerPane.setVisible(normal); -// composerPane.setManaged(normal); -// -// // Join / Add -// joinPane.setVisible(needsJoin); -// joinPane.setManaged(needsJoin); -// addContactPane.setVisible(needsAdd); -// addContactPane.setManaged(needsAdd); -// -// // پنل پایین برای READ_ONLY/BLOCKED -// boolean showRO = readOnly || blocked; -// if (readOnlyPane != null) { -// readOnlyPane.setVisible(showRO); -// readOnlyPane.setManaged(showRO); -// } -// -// // متن آبی برای READ_ONLY -// if (readOnlyLabel != null) { -// readOnlyLabel.setVisible(readOnly); -// readOnlyLabel.setManaged(readOnly); -// } -// -// // دکمهٔ قرمز UNBLOCK فقط در BLOCKED -// if (unblockBtn != null) { -// unblockBtn.setVisible(blocked); -// unblockBtn.setManaged(blocked); -// } -// -// // متن دکمه‌های Join/Add -// if (needsJoin && joinButton != null && currentChat != null) { -// String what = "channel".equalsIgnoreCase(currentChat.getType()) ? "CHANNEL" : "GROUP"; -// joinButton.setText(("Join " + what).toUpperCase()); -// } -// if (needsAdd && addContactButton != null) { -// addContactButton.setText("ADD CONTACT"); -// } -// } - - private void applyMode(ChatViewMode mode) { currentMode = mode; - boolean normal = (mode == ChatViewMode.NORMAL); + boolean normal = (mode == ChatViewMode.NORMAL); boolean needsJoin = (mode == ChatViewMode.NEEDS_JOIN); - boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); - boolean readOnly = (mode == ChatViewMode.READ_ONLY); - boolean blocked = (mode == ChatViewMode.BLOCKED); + boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); + boolean readOnly = (mode == ChatViewMode.READ_ONLY); + boolean blocked = (mode == ChatViewMode.BLOCKED); - // فقط در حالت نرمال: کامپوزر composerPane.setVisible(normal); composerPane.setManaged(normal); - // پنل‌های Join / Add joinPane.setVisible(needsJoin); joinPane.setManaged(needsJoin); addContactPane.setVisible(needsAdd); addContactPane.setManaged(needsAdd); - // پنل‌های پایین if (readOnlyPane != null) { readOnlyPane.setVisible(readOnly); readOnlyPane.setManaged(readOnly); @@ -3078,7 +1995,6 @@ public class ChatPageController { blockedPane.setManaged(blocked); } - // متن دکمه‌ها if (needsJoin && joinButton != null && currentChat != null) { String what = "channel".equalsIgnoreCase(currentChat.getType()) ? "CHANNEL" : "GROUP"; joinButton.setText(("Join " + what).toUpperCase()); @@ -3099,9 +2015,7 @@ public class ChatPageController { return; } - // شبکه روی بک‌گراند new Thread(() -> { - // 1) اگر otherUserId نداشتیم، از سرور بگیر java.util.UUID other = currentChat.getOtherUserId(); if (other == null) { other = resolvePeerUuidFromServer(currentChat); @@ -3111,7 +2025,6 @@ public class ChatPageController { return; } - // 2) حالا درخواست آن‌بلاک org.json.JSONObject req = new org.json.JSONObject() .put("action", "toggle_block") .put("user_id", viewerUuid) // internal_uuid خودت @@ -3125,24 +2038,20 @@ public class ChatPageController { applyMode(ChatViewMode.NORMAL); messageInput.requestFocus(); } else { - addSystemMessage("Unblock failed: " + (res != null ? res.optString("message","") : "")); + addSystemMessage("Unblock failed: " + (res != null ? res.optString("message", "") : "")); } }); }).start(); } - private boolean canPostToChannel(ChatEntry entry, JSONObject headerData) { - // 1) اگر سرور صراحتاً can_post داد، همان را بگیر if (headerData != null && headerData.has("can_post")) { return headerData.optBoolean("can_post", false); } - // 2) یا اگر is_owner / is_admin را داد if (headerData != null && (headerData.has("is_owner") || headerData.has("is_admin"))) { return headerData.optBoolean("is_owner", false) || headerData.optBoolean("is_admin", false); } - // 3) فال‌بک به اطلاعات لوکال: ChatEntry + permissions محلی if (entry != null) { if (entry.isOwner() || entry.isAdmin()) return true; if (entry.getPermissions() != null && entry.getPermissions().optBoolean("can_post", false)) { @@ -3153,7 +2062,6 @@ public class ChatPageController { } - private void reactToMessage(String msgId, String emoji) { JSONObject req = new JSONObject() .put("action", "react_to_message") @@ -3166,7 +2074,6 @@ public class ChatPageController { if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { addSystemMessage("Failed to react."); } else { - // ساده‌ترین کار: رفرش loadMessages(currentChat); } }); @@ -3175,7 +2082,6 @@ public class ChatPageController { private void startReply(String msgId) { pendingReplyToId = msgId; - // یک پریویو کوچیک بالای TextArea نشان بده (می‌تونی از buildReplyBoxFromIndex استفاده کنی) var preview = buildReplyBoxFromIndex(msgId); if (!composerPane.getChildren().contains(preview)) { composerPane.getChildren().add(0, preview); @@ -3190,7 +2096,6 @@ public class ChatPageController { private void openForwardPickerFromSession(String originalMsgId) { java.util.List targets = fetchForwardTargetsFromSession(); - // اگر نخواستی به همین چت فعلی هم اجازه بدی، حذفش کن: if (currentChat != null) { targets.removeIf(t -> t.id.equals(currentChat.getId()) && @@ -3208,14 +2113,15 @@ public class ChatPageController { ButtonType btnCancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); dialog.getDialogPane().getButtonTypes().setAll(btnSend, btnCancel); - // آیکن/گرافیک (اگر آیکن forward داری) try { var iv = new ImageView(new Image( getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/ic_forward.png") )); - iv.setFitWidth(18); iv.setFitHeight(18); + iv.setFitWidth(18); + iv.setFitHeight(18); dialog.getDialogPane().setGraphic(iv); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } TextField search = new TextField(); search.setPromptText("Search chats…"); @@ -3249,7 +2155,8 @@ public class ChatPageController { root.getChildren().addAll(avatar, texts); } - @Override protected void updateItem(ForwardTarget item, boolean empty) { + @Override + protected void updateItem(ForwardTarget item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setGraphic(null); @@ -3261,8 +2168,8 @@ public class ChatPageController { if (img == null) { String fallback = switch (item.type.toLowerCase()) { case "channel" -> "/org/to/telegramfinalproject/Avatars/default_channel_profile.png"; - case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; - default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png"; + default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; }; img = new Image(getClass().getResourceAsStream(fallback)); } @@ -3306,9 +2213,6 @@ public class ChatPageController { private void forwardToTarget(String originalMsgId, ForwardTarget target) { if (target == null) return; - // (اختیاری) قبل از ارسال، محدودیت‌ها را چک کن - // مثلا کانال‌هایی که اجازه‌ی پست نداری: - // if ("channel".equalsIgnoreCase(target.type) && !/*canPost*/ false) { addSystemMessage("You can’t post to this channel."); return; } JSONObject req = new JSONObject() .put("action", "forward_message") @@ -3320,9 +2224,8 @@ public class ChatPageController { JSONObject res = ActionHandler.sendWithResponse(req); Platform.runLater(() -> { if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { - addSystemMessage("Forward failed: " + (res == null ? "" : res.optString("message",""))); + addSystemMessage("Forward failed: " + (res == null ? "" : res.optString("message", ""))); } else { - // اگر مقصد همین چت بود، لیست پیام‌ها را رفرش کن if (currentChat != null && currentChat.getId().equals(target.id) && currentChat.getType().equalsIgnoreCase(target.type)) { @@ -3342,6 +2245,7 @@ public class ChatPageController { messageInput.requestFocus(); messageInput.positionCaret(messageInput.getText().length()); } + private void confirmDelete(String msgId) { Alert a = new Alert(Alert.AlertType.CONFIRMATION); a.setHeaderText("Delete message?"); @@ -3349,10 +2253,9 @@ public class ChatPageController { ButtonType everyone = new ButtonType("Delete for everyone"); ButtonType cancel = ButtonType.CANCEL; - // نمایش «Delete for everyone» فقط اگر منطقی به‌نظر می‌رسد - boolean showGlobal = true; // ساده: بذار سرور رد کند اگر مجاز نیست + boolean showGlobal = true; if (showGlobal) a.getButtonTypes().setAll(onlyMe, everyone, cancel); - else a.getButtonTypes().setAll(onlyMe, cancel); + else a.getButtonTypes().setAll(onlyMe, cancel); a.showAndWait().ifPresent(btn -> { if (btn == onlyMe) deleteMessage(msgId, "one-sided"); @@ -3370,15 +2273,13 @@ public class ChatPageController { JSONObject res = ActionHandler.sendWithResponse(req); Platform.runLater(() -> { if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { - addSystemMessage("Delete failed: " + (res==null?"":res.optString("message"))); + addSystemMessage("Delete failed: " + (res == null ? "" : res.optString("message"))); return; } - // one-sided: سریعاً از UI حذف کن if ("one-sided".equals(deleteType)) { Node n = messageNodes.remove(msgId); if (n != null) messageContainer.getChildren().remove(n); } else { - // global: سرور RT می‌فرستد، اما برای UX می‌توانی رفرش کنی loadMessages(currentChat); } }); @@ -3386,7 +2287,6 @@ public class ChatPageController { } - // پیام مالِ من است؟ private boolean isOutgoingMessage(String messageId) { if (Session.currentUser == null || !Session.currentUser.has("internal_uuid")) return false; String meId = Session.currentUser.optString("internal_uuid", ""); @@ -3395,17 +2295,14 @@ public class ChatPageController { return meId.equalsIgnoreCase(m.optString("sender_id", "")); } - // در کانال می‌تونم global حذف کنم؟ private boolean canDeleteInChannel() { if (currentChat == null) return false; if (currentChat.isOwner() || currentChat.isAdmin()) return true; - return currentChat.getPermissions()!=null && + return currentChat.getPermissions() != null && currentChat.getPermissions().optBoolean("can_delete", false); } - - private void confirmDeleteDialog(String messageId) { boolean outgoing = isOutgoingMessage(messageId); String t = currentChat != null ? currentChat.getType() : ""; @@ -3414,24 +2311,21 @@ public class ChatPageController { : "channel".equalsIgnoreCase(t) ? canDeleteInChannel() : false; - String peerName = (currentChat != null && currentChat.getName()!=null) + String peerName = (currentChat != null && currentChat.getName() != null) ? currentChat.getName() : "everyone"; Dialog dialog = new Dialog<>(); dialog.setTitle("Delete message"); - // مالک دیالوگ (اختیاری ولی بهتر) if (messageContainer != null && messageContainer.getScene() != null) { dialog.initOwner(messageContainer.getScene().getWindow()); } - // دکمه‌ها ButtonType btnCancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); ButtonType btnDelete = new ButtonType("Delete", ButtonBar.ButtonData.OK_DONE); - dialog.getDialogPane().getButtonTypes().setAll(btnDelete, btnCancel); // Delete اول بیاید + dialog.getDialogPane().getButtonTypes().setAll(btnDelete, btnCancel); - // عنوان و چک‌باکس Label title = new Label("Do you want to delete this message?"); title.setStyle("-fx-font-size: 14; -fx-font-weight: bold; -fx-text-fill: -fx-text-base-color;"); @@ -3452,7 +2346,6 @@ public class ChatPageController { trashIv.setFitHeight(18); dialog.getDialogPane().setGraphic(trashIv); - // آیکن خود پنجره (بالا-چپ فریم) dialog.getDialogPane().sceneProperty().addListener((obs, oldScene, newScene) -> { if (newScene != null) { Stage stage = (Stage) newScene.getWindow(); @@ -3460,12 +2353,11 @@ public class ChatPageController { } }); - // کمی استایل dialog.getDialogPane().setStyle(""" - -fx-background-radius: 12; - -fx-background-insets: 0; - -fx-padding: 8; - """); + -fx-background-radius: 12; + -fx-background-insets: 0; + -fx-padding: 8; + """); dialog.setOnShown(ev -> { @@ -3479,7 +2371,6 @@ public class ChatPageController { } }); - // نمایش و تصمیم var res = dialog.showAndWait(); if (res.isPresent() && res.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) { String deleteType = (alsoDelete.isSelected() && canGlobal) ? "global" : "one-sided"; @@ -3487,41 +2378,36 @@ public class ChatPageController { } } - // ChatPageController.java public void onChatAvatarUpdated(UUID chatId, String newUrl) { - // اگه چت فعلی چیز دیگریه، کاری نکن if (currentChat == null || chatId == null || newUrl == null || newUrl.isBlank()) return; if (!currentChat.getId().equals(chatId)) return; - // اگر از نخ غیر JavaFX صدا زده شد، امنش کنیم if (!Platform.isFxApplicationThread()) { Platform.runLater(() -> onChatAvatarUpdated(chatId, newUrl)); return; } - // State داخلی entry را هم آپدیت کن currentChat.setImageUrl(newUrl); - // آواتار هدر را ست کن (با resolver خودت) try { Image im = org.to.telegramfinalproject.Client.AvatarLocalResolver.load(newUrl); if (im != null) { userAvatar.setImage(im); } else { - // fallback اگر لود نشد setDefaultHeaderAvatarByType(currentChat.getType()); } } catch (Exception ignore) { setDefaultHeaderAvatarByType(currentChat.getType()); } - // مطمئن شو همچنان دایره‌ایه - try { AvatarFX.circleClip(userAvatar, 36); } catch (Throwable ignored) {} + try { + AvatarFX.circleClip(userAvatar, 36); + } catch (Throwable ignored) { + } } - private static final class ForwardTarget { final UUID id; // internal_id final String type; // private | group | channel @@ -3535,7 +2421,8 @@ public class ChatPageController { this.imageUrl = imageUrl == null ? "" : imageUrl; } - @Override public String toString() { + @Override + public String toString() { return name + " (" + type + ")"; } } @@ -3559,23 +2446,24 @@ public class ChatPageController { org.json.JSONObject o = arr.optJSONObject(i); if (o == null) continue; String internalId = o.optString("internal_id", ""); - String type = o.optString("type", ""); - String name = o.optString("name", ""); - String imageUrl = o.optString("image_url", ""); + String type = o.optString("type", ""); + String name = o.optString("name", ""); + String imageUrl = o.optString("image_url", ""); if (internalId.isBlank() || type.isBlank()) continue; UUID id; - try { id = java.util.UUID.fromString(internalId); } - catch (Exception ignore) { continue; } + try { + id = java.util.UUID.fromString(internalId); + } catch (Exception ignore) { + continue; + } ForwardTarget ft = new ForwardTarget(id, type, name, imageUrl); - // کلید یکتا: id + type map.put(id.toString() + "|" + type.toLowerCase(), ft); } } - // (اختیاری) Saved Messages اگر داری می‌خوای اضافه کنی، اینجا اضافه کن. return new java.util.ArrayList<>(map.values()); } @@ -3584,17 +2472,14 @@ public class ChatPageController { private void showOpenFromContactsHint() { if (addContactPane == null) return; - // پنل را نگه دار، فقط محتوا را عوض کن addContactPane.getChildren().clear(); Label hint = new Label("you should open chat from contact list for first time"); - hint.getStyleClass().add("footer-link-btn"); // همان کلاس CSS دکمه‌ی پایین - // اگر می‌خواهی شبیه لینک آبی شود و کلیک‌پذیر نباشد: + hint.getStyleClass().add("footer-link-btn"); hint.setUnderline(true); addContactPane.getChildren().add(hint); - // مطمئن شو فقط همین پانل دیده شود (کامپوزر/بقیه بسته بمانند) composerPane.setVisible(false); composerPane.setManaged(false); joinPane.setVisible(false); @@ -3609,11 +2494,11 @@ public class ChatPageController { String suffix = dark ? "_light.png" : "_dark.png"; if (attachmentIcon != null) attachmentIcon.setImage(loadIcon("attachment" + suffix)); - if (sendIcon != null) sendIcon.setImage(loadIcon("send_cyan2.png")); // always cyan - if (searchIcon != null) searchIcon.setImage(loadIcon("search" + suffix)); - if (moreIcon != null) moreIcon.setImage(loadIcon("more" + suffix)); + if (sendIcon != null) sendIcon.setImage(loadIcon("send_cyan2.png")); // always cyan + if (searchIcon != null) searchIcon.setImage(loadIcon("search" + suffix)); + if (moreIcon != null) moreIcon.setImage(loadIcon("more" + suffix)); - if (chatTitle != null) chatTitle.setStyle(dark ? "-fx-text-fill:#e8f1f8;" : "-fx-text-fill:#0f141a;"); + if (chatTitle != null) chatTitle.setStyle(dark ? "-fx-text-fill:#e8f1f8;" : "-fx-text-fill:#0f141a;"); if (chatStatus != null) chatStatus.setStyle(dark ? "-fx-text-fill:#8ea1b2;" : "-fx-text-fill:#7e8a97;"); // === Context menu icons === @@ -3641,18 +2526,16 @@ public class ChatPageController { private UUID resolvePeerUuidFromServer(ChatEntry chat) { if (chat == null || !"private".equalsIgnoreCase(chat.getType())) return null; - // اگر از قبل ست شده بود از همون استفاده کن try { UUID cached = chat.getOtherUserId(); if (cached != null) return cached; - } catch (Exception ignore) {} + } catch (Exception ignore) { + } - // درخواست به سرور برای گرفتن target_id org.json.JSONObject req = new org.json.JSONObject() .put("action", "get_private_chat_target") .put("chat_id", chat.getId().toString()); - // اگر سمت سرور لازم دارد، می‌توانی viewer را هم بفرستی: - // .put("viewer_id", Session.getUserUUID()); + org.json.JSONObject res = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) return null; @@ -3665,7 +2548,7 @@ public class ChatPageController { try { java.util.UUID target = java.util.UUID.fromString(tid); - chat.setOtherUserId(target); // کش محلی کن که دفعات بعد لازم نشه + chat.setOtherUserId(target); return target; } catch (Exception ignore) { return null; @@ -3676,13 +2559,13 @@ 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; + l.setText(text); + return; } } } @@ -3691,4 +2574,288 @@ public class ChatPageController { } + // Put this inside your controller class (or as a top-level small class) + private static final class AdminVM { + final UUID internalId; // internal_uuid (preferred for server calls) + final String userId; // display user_id (fallback if server still expects it) + final String profileName; + + AdminVM(UUID internalId, String userId, String profileName) { + this.internalId = internalId; + this.userId = userId; + this.profileName = profileName; + } + + @Override + public String toString() { + // This is what shows in the ChoiceDialog list: + return profileName + (userId != null && !userId.isBlank() ? " [" + userId + "]" : ""); + } + } +// private void onLeaveGroupMenuClicked(ChatEntry entry) { +// if (entry == null || !"group".equalsIgnoreCase(entry.getType())) { +// alert(Alert.AlertType.INFORMATION, "This action is only available for groups."); +// return; +// } +// +// String myUuidStr = Session.getUserUUID(); +// if (myUuidStr == null || myUuidStr.isBlank()) { +// alert(Alert.AlertType.ERROR, "Cannot determine your identity."); +// return; +// } +// if (entry.isOwner()) { +// // 1. گرفتن لیست ادمین‌ها +// JSONObject req = new JSONObject() +// .put("action", "view_group_admins") +// .put("group_id", entry.getId().toString()); +// +// JSONObject res = ActionHandler.sendWithResponse(req); +// if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { +// alert(Alert.AlertType.ERROR, "Failed to fetch admins."); +// return; +// } +// +// JSONArray admins = res.getJSONObject("data").optJSONArray("admins"); +// if (admins == null || admins.isEmpty()) { +// alert(Alert.AlertType.WARNING, "No other admins available. Promote someone first."); +// return; +// } +// +// String myUuid = Session.getUserUUID(); +// List candidates = new ArrayList<>(); +// for (int i = 0; i < admins.length(); i++) { +// JSONObject a = admins.getJSONObject(i); +// String uuid = a.optString("internal_uuid"); +// if (uuid != null && uuid.equals(myUuid)) continue; // حذف Owner +// candidates.add(a); +// } +// +// if (candidates.isEmpty()) { +// alert(Alert.AlertType.WARNING, "No other admins available."); +// return; +// } +// +// // 2. نمایش لیست در یک Dialog ساده +// Dialog dialog = new Dialog<>(); +// dialog.setTitle("Transfer Ownership"); +// dialog.setHeaderText("Select a new owner before leaving"); +// dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); +// +// ListView listView = new ListView<>(); +// listView.getItems().addAll(candidates); +// listView.setCellFactory(v -> new ListCell<>() { +// @Override protected void updateItem(JSONObject item, boolean empty) { +// super.updateItem(item, empty); +// if (empty || item == null) { +// setText(null); +// } else { +// setText(item.optString("profile_name", "Unknown") +// + " (" + item.optString("user_id") + ")"); +// } +// } +// }); +// +// dialog.getDialogPane().setContent(listView); +// Node okBtn = dialog.getDialogPane().lookupButton(ButtonType.OK); +// okBtn.setDisable(true); +// +// listView.getSelectionModel().selectedItemProperty().addListener((obs, old, sel) -> { +// okBtn.setDisable(sel == null); +// }); +// +// dialog.setResultConverter(bt -> +// bt == ButtonType.OK ? listView.getSelectionModel().getSelectedItem() : null); +// +// Optional pick = dialog.showAndWait(); +// if (pick.isEmpty()) return; +// +// JSONObject selected = pick.get(); +// String newOwnerId = selected.optString("internal_uuid"); +// +// JSONObject promoteReq = new JSONObject() +// .put("action", "transfer_group_ownership") +// .put("group_id", entry.getId().toString()) +// .put("new_owner_user_id", newOwnerId); +// +// JSONObject promoteRes = ActionHandler.sendWithResponse(promoteReq); +// if (promoteRes == null || !"success".equalsIgnoreCase(promoteRes.optString("status"))) { +// alert(Alert.AlertType.ERROR, "Ownership transfer failed."); +// return; +// } +// +// // 4. بعد از انتقال، خروج از گروه +// JSONObject leaveReq = new JSONObject() +// .put("action", "leave_chat") +// .put("user_id", myUuid) +// .put("chat_id", entry.getId().toString()) +// .put("chat_type", "group"); +// +// JSONObject leaveRes = ActionHandler.sendWithResponse(leaveReq); +// if (leaveRes != null && "success".equalsIgnoreCase(leaveRes.optString("status"))) { +// alert(Alert.AlertType.INFORMATION, "Ownership transferred and you left the group."); +// MainController.getInstance().refreshChatListUI(); +// AppRouter.showMain(); +// } else { +// alert(Alert.AlertType.ERROR, "Leave failed."); +// } +// return; +// } +// +// +// +// if (confirm("Leave Group", "Are you sure you want to leave this group?")) { +// sendLeaveRequest(entry.getId(), myUuidStr); +// } +// } + + private void onLeaveGroupMenuClicked(ChatEntry entry) { + if (entry == null || !"group".equalsIgnoreCase(entry.getType())) { + alert(Alert.AlertType.INFORMATION, "This action is only available for groups."); + return; + } + + String myUuidStr = Session.getUserUUID(); + if (myUuidStr == null || myUuidStr.isBlank()) { + alert(Alert.AlertType.ERROR, "Cannot determine your identity."); + return; + } + + if (!entry.isOwner()) { + if (confirm("Leave Group", "Are you sure you want to leave this group?")) { + sendLeaveRequest(entry.getId(), myUuidStr); + } + return; + } + + // ========== Owner case ========== + // گرفتن لیست ادمین‌ها + JSONObject req = new JSONObject() + .put("action", "view_group_admins") + .put("group_id", entry.getId().toString()); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { + alert(Alert.AlertType.ERROR, "Failed to fetch admins."); + return; + } + + JSONArray admins = res.getJSONObject("data").optJSONArray("admins"); + if (admins == null || admins.isEmpty()) { + alert(Alert.AlertType.WARNING, "No other admins available. Promote someone first."); + return; + } + + List candidates = new ArrayList<>(); + for (int i = 0; i < admins.length(); i++) { + JSONObject a = admins.getJSONObject(i); + if (a == null) continue; + String uuid = a.optString("internal_uuid"); + if (uuid != null && uuid.equals(Session.getUserUUID())) continue; + candidates.add(a); + } + + if (candidates.isEmpty()) { + alert(Alert.AlertType.WARNING, "No other admins available."); + return; + } + + Dialog dialog = new Dialog<>(); + dialog.setTitle("Transfer Ownership"); + dialog.setHeaderText("Select a new owner before leaving"); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + ListView listView = new ListView<>(); + listView.getItems().addAll(candidates); + listView.setCellFactory(v -> new ListCell<>() { + @Override protected void updateItem(JSONObject item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.optString("profile_name", "Unknown") + + " (" + item.optString("user_id") + ")"); + } + } + }); + + dialog.getDialogPane().setContent(listView); + Node okBtn = dialog.getDialogPane().lookupButton(ButtonType.OK); + okBtn.setDisable(true); + + listView.getSelectionModel().selectedItemProperty().addListener((obs, old, sel) -> { + okBtn.setDisable(sel == null); + }); + + dialog.setResultConverter(bt -> + bt == ButtonType.OK ? listView.getSelectionModel().getSelectedItem() : null); + + Optional pick = dialog.showAndWait(); + if (pick.isEmpty()) return; + + JSONObject selected = pick.get(); + + String newOwnerUuid = selected.optString("internal_uuid", "").trim(); + String newOwnerUserId = selected.optString("user_id", "").trim(); + + if (newOwnerUuid.isEmpty() && newOwnerUserId.isEmpty()) { + alert(Alert.AlertType.ERROR, "Selected admin has no valid id."); + return; + } + + // ارسال درخواست انتقال مالکیت + JSONObject promoteReq = new JSONObject() + .put("action", "transfer_group_ownership") + .put("group_id", entry.getId().toString()); + + if (!newOwnerUuid.isEmpty()) { + promoteReq.put("new_owner_id", newOwnerUuid); + } + if (!newOwnerUserId.isEmpty()) { + promoteReq.put("new_owner_user_id", newOwnerUserId); + } + + JSONObject promoteRes = ActionHandler.sendWithResponse(promoteReq); + if (promoteRes == null || !"success".equalsIgnoreCase(promoteRes.optString("status"))) { + alert(Alert.AlertType.ERROR, "Ownership transfer failed."); + return; + } + + // بعد از انتقال، لفت بده + sendLeaveRequest(entry.getId(), myUuidStr); + } + + private void sendLeaveRequest(UUID groupId, String myUuidStr) { + JSONObject req = new JSONObject() + .put("action", "leave_chat") + .put("user_id", myUuidStr) + .put("chat_id", groupId.toString()) + .put("chat_type", "group"); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + alert(Alert.AlertType.INFORMATION, "You left the group."); + MainController.getInstance().refreshChatListUI(); + AppRouter.showMain(); + } else { + String msg = (res != null) ? res.optString("message", "Leave failed.") : "null response"; + alert(Alert.AlertType.ERROR, msg); + } + } + + + + private boolean confirm(String title, String msg) { + Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL); + a.setTitle(title); + Optional r = a.showAndWait(); + return r.isPresent() && r.get() == ButtonType.OK; + } + + private void alert(Alert.AlertType type, String msg) { + new Alert(type, msg, ButtonType.OK).show(); + } + + + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java b/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java index fcf9d60..237f535 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java @@ -289,10 +289,45 @@ public class GroupInfoController { } private void handleDeleteGroup() { - System.out.println("Deleting group..."); - // TODO: implement backend call + ChatEntry entry = Session.currentChatEntry; + +// if (entry == null || !"group".equalsIgnoreCase(entry.getType())) { +// alert(Alert.AlertType.INFORMATION, "This action is only available for groups."); +// return; +// } + + if (!confirm("Delete Group", + "Are you sure you want to DELETE this group?\nThis action cannot be undone.")) { + return; + } + + JSONObject req = new JSONObject() + .put("action", "delete_group") + .put("group_id", entry.getId().toString()); + + JSONObject res = ActionHandler.sendWithResponse(req); + + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + alert(Alert.AlertType.INFORMATION, "✅ Group deleted successfully."); + MainController.getInstance().refreshChatListUI(); + AppRouter.showMain(); + } else { + String msg = (res != null) ? res.optString("message", "Failed to delete group.") : "null response"; + alert(Alert.AlertType.ERROR, "❌ " + msg); + } } + private boolean confirm(String title, String msg) { + Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL); + a.setTitle(title); + return a.showAndWait().filter(btn -> btn == ButtonType.OK).isPresent(); + } + private void alert(Alert.AlertType type, String msg) { + new Alert(type, msg, ButtonType.OK).show(); + } + + + private void updateIcons(boolean dark) { String suffix = dark ? "_light.png" : "_dark.png"; diff --git a/src/main/java/org/to/telegramfinalproject/UI/LeaveGroupFlow.java b/src/main/java/org/to/telegramfinalproject/UI/LeaveGroupFlow.java new file mode 100644 index 0000000..d29f31e --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/LeaveGroupFlow.java @@ -0,0 +1,359 @@ +package org.to.telegramfinalproject.UI; + +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Models.ChatEntry; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.UUID; + +public final class LeaveGroupFlow { + + private LeaveGroupFlow() {} + + // ==== Public API: call this from your controller when menu item clicked + public static void start(ChatEntry entry) { + if (entry == null || !"group".equalsIgnoreCase(entry.getType())) { + info("This action is only available for groups."); + return; + } + final String myUuidStr = Session.getUserUUID(); + if (myUuidStr == null || myUuidStr.isBlank()) { + error("Cannot determine your identity (internal UUID missing)."); + return; + } + + if (!entry.isOwner()) { + // Non-owner: confirm and leave directly + if (!confirm("Leave Group", "Are you sure you want to leave this group?")) return; + leaveGroupAsync(entry.getId(), myUuidStr, () -> info("You left the group.")); + return; + } + + // Owner: choose new owner (server returns list WITHOUT self) + openOwnerPickerAndLeave(entry.getId(), myUuidStr); + } + + // ==== Owner flow ==== + private static void openOwnerPickerAndLeave(UUID groupId, String myUuidStr) { + ProgressIndicator pi = showLoading("Loading admins..."); + + Task> task = new Task<>() { + @Override protected List call() { + JSONObject req = new JSONObject() + .put("action", "view_group_admins") + .put("group_id", groupId.toString()) + .put("exclude_self", true) // 👈 سرور خودش Owner را حذف کند + .put("viewer_id", myUuidStr); // 👈 برای تشخیص self + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { + throw new RuntimeException(res != null ? res.optString("message","Failed to fetch admins.") + : "null response"); + } + + JSONArray arr = res.getJSONObject("data").optJSONArray("admins"); + List list = new ArrayList<>(); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + JSONObject a = arr.getJSONObject(i); + list.add(new AdminRow( + a.optString("internal_uuid",""), + a.optString("user_id",""), + a.optString("profile_name","Unknown"), + a.optString("avatar_url","") + )); + } + } + return list; + } + }; + + task.setOnSucceeded(ev -> { + hideLoading(pi); + + List candidates = task.getValue(); + if (candidates == null || candidates.isEmpty()) { + warn("No other admins available. Promote an admin first, then try again."); + return; + } + + // اگر فقط یکی بود، سریع‌تر پیش برو + if (candidates.size() == 1) { + AdminRow target = candidates.get(0); + if (!confirm("Confirm Ownership Transfer", + "Transfer ownership to " + target.display() + "?\nYou will leave the group afterward.")) return; + transferThenLeaveAtomic(groupId, target, myUuidStr); + return; + } + + // دیالوگ شیک با سرچ + لیست + Optional pick = AdminPickerDialog.show("Transfer Ownership", "Select the new owner", candidates); + if (pick.isEmpty()) return; + AdminRow target = pick.get(); + + if (!confirm("Confirm Ownership Transfer", + "Transfer ownership to " + target.display() + "?\nYou will leave the group afterward.")) return; + + transferThenLeaveAtomic(groupId, target, myUuidStr); + }); + + task.setOnFailed(ev -> { + hideLoading(pi); + error("Failed to load admins: " + safeMsg(task.getException())); + }); + + new Thread(task, "load-admins").start(); + } + + // ==== Prefer ATOMIC request; fallback to classic ==== + private static void transferThenLeaveAtomic(UUID groupId, AdminRow target, String myUuidStr) { + ProgressIndicator pi = showLoading("Transferring & leaving..."); + + Task task = new Task<>() { + @Override protected Void call() { + // 1) Try atomic endpoint + JSONObject atomic = new JSONObject() + .put("action", "transfer_and_leave_group") // 👈 اکشن اتمیک + .put("group_id", groupId.toString()) + .put("viewer_id", myUuidStr) + .put("new_owner_id", target.requireUuid().toString()); // UUID ضروری + + JSONObject res = ActionHandler.sendWithResponse(atomic); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + return null; // done + } + + // 2) Fallback: classic two-step + JSONObject tReq = new JSONObject() + .put("action", "transfer_group_ownership") + .put("group_id", groupId.toString()) + .put("new_owner_id", target.requireUuid().toString()); + + JSONObject tRes = ActionHandler.sendWithResponse(tReq); + if (tRes == null || !"success".equalsIgnoreCase(tRes.optString("status"))) { + throw new RuntimeException(tRes != null ? tRes.optString("message","Ownership transfer failed.") + : "null response"); + } + + JSONObject lReq = new JSONObject() + .put("action", "leave_chat") + .put("user_id", myUuidStr) + .put("chat_id", groupId.toString()) + .put("chat_type", "group"); + + JSONObject lRes = ActionHandler.sendWithResponse(lReq); + if (lRes == null || !"success".equalsIgnoreCase(lRes.optString("status"))) { + throw new RuntimeException(lRes != null ? lRes.optString("message","Leave failed.") + : "null response"); + } + return null; + } + }; + + task.setOnSucceeded(ev -> { + hideLoading(pi); + info("Done. You left the group."); + // TODO: refresh chat list / close current view + }); + + task.setOnFailed(ev -> { + hideLoading(pi); + error("Operation failed: " + safeMsg(task.getException())); + }); + + new Thread(task, "transfer-leave").start(); + } + + // ==== direct leave for non-owner ==== + private static void leaveGroupAsync(UUID groupId, String myUuidStr, Runnable onOk) { + ProgressIndicator pi = showLoading("Leaving group..."); + + Task task = new Task<>() { + @Override protected Void call() { + JSONObject req = new JSONObject() + .put("action", "leave_chat") + .put("user_id", myUuidStr) + .put("chat_id", groupId.toString()) + .put("chat_type", "group"); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { + throw new RuntimeException(res != null ? res.optString("message","Leave failed.") : "null response"); + } + return null; + } + }; + + task.setOnSucceeded(ev -> { + hideLoading(pi); + if (onOk != null) onOk.run(); + }); + task.setOnFailed(ev -> { + hideLoading(pi); + error("Leave failed: " + safeMsg(task.getException())); + }); + + new Thread(task, "leave-group").start(); + } + + // ====== Admin Picker Dialog (with search + avatars) ====== + private static final class AdminPickerDialog { + + static Optional show(String title, String header, List admins) { + Dialog dialog = new Dialog<>(); + dialog.setTitle(title); + dialog.setHeaderText(header); + + ButtonType okType = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll(okType, ButtonType.CANCEL); + + TextField search = new TextField(); + search.setPromptText("Search admin by name or id..."); + search.setMinHeight(36); + + ListView list = new ListView<>(); + list.getItems().setAll(admins); + list.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + list.setPrefWidth(480); + list.setPrefHeight(360); + + list.setCellFactory(v -> new ListCell<>() { + private final HBox row = new HBox(10); + private final ImageView avatar = new ImageView(); + private final Label name = new Label(); + private final Label id = new Label(); + { + row.setAlignment(Pos.CENTER_LEFT); + avatar.setFitWidth(36); + avatar.setFitHeight(36); + avatar.setPreserveRatio(true); + name.getStyleClass().add("admin-name"); + id.getStyleClass().add("admin-id"); + HBox text = new HBox(8, name, id); + row.getChildren().addAll(avatar, text); + } + @Override protected void updateItem(AdminRow it, boolean empty) { + super.updateItem(it, empty); + if (empty || it == null) { + setGraphic(null); setText(null); + } else { + name.setText(it.name); + id.setText("• " + it.userId); + // آواتار (اختیاری): اگر URL هست نمایش بده + if (it.avatarUrl != null && !it.avatarUrl.isBlank()) { + try { avatar.setImage(new Image(it.avatarUrl, true)); } + catch (Exception e) { avatar.setImage(null); } + } else avatar.setImage(null); + setGraphic(row); setText(null); + } + } + }); + + // جستجو + search.textProperty().addListener((obs, o, n) -> { + String q = (n == null) ? "" : n.trim().toLowerCase(Locale.ROOT); + List filtered = admins.stream() + .filter(a -> a.name.toLowerCase(Locale.ROOT).contains(q) + || normalizeDisplayId(a.userId).contains(q)) + .collect(Collectors.toList()); + list.getItems().setAll(filtered); + }); + + // دابل کلیک = OK + list.setOnMouseClicked(e -> { + if (e.getClickCount() == 2 && list.getSelectionModel().getSelectedItem() != null) { + dialog.setResult(list.getSelectionModel().getSelectedItem()); + dialog.close(); + } + }); + + Node okBtn = dialog.getDialogPane().lookupButton(okType); + okBtn.setDisable(true); + list.getSelectionModel().selectedItemProperty().addListener((obs, old, sel) -> okBtn.setDisable(sel == null)); + + VBox content = new VBox(12, search, list); + content.setPadding(new Insets(10)); + dialog.getDialogPane().setContent(content); + + dialog.setResultConverter(bt -> bt == okType ? list.getSelectionModel().getSelectedItem() : null); + return dialog.showAndWait(); + } + } + + // ===== Models & helpers ===== + private static final class AdminRow { + final UUID internalUuid; + final String userId; + final String name; + final String avatarUrl; + + AdminRow(String internalUuidStr, String userId, String name, String avatarUrl) { + this.internalUuid = parseUuidOrNull(internalUuidStr); + this.userId = userId == null ? "" : userId; + this.name = name == null ? "Unknown" : name; + this.avatarUrl = avatarUrl; + } + + UUID requireUuid() { + if (internalUuid == null) + throw new IllegalStateException("Admin has no internal_uuid; server must provide it."); + return internalUuid; + } + + String display() { return name + " (" + userId + ")"; } + + @Override public String toString() { + return "AdminRow{name='"+name+"', userId='"+userId+"', uuid="+internalUuid+"}"; + } + } + + private static String normalizeDisplayId(String s) { + if (s == null) return ""; + String t = s.trim(); + if (t.startsWith("@")) t = t.substring(1); + return t.toLowerCase(Locale.ROOT); + } + + private static UUID parseUuidOrNull(String s) { + try { return (s == null || s.isBlank()) ? null : UUID.fromString(s); } + catch (Exception e) { return null; } + } + + private static ProgressIndicator showLoading(String title) { + Alert a = new Alert(Alert.AlertType.NONE); + a.setTitle(title); + ProgressIndicator pi = new ProgressIndicator(); + a.getDialogPane().setContent(pi); + a.getButtonTypes().clear(); + a.show(); + pi.getProperties().put("alert", a); + return pi; + } + private static void hideLoading(ProgressIndicator pi) { + Object a = pi.getProperties().get("alert"); + if (a instanceof Alert) ((Alert) a).close(); + } + static boolean confirm(String title, String msg) { + Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL); + a.setTitle(title); + Optional r = a.showAndWait(); + return r.isPresent() && r.get() == ButtonType.OK; + } + private static void info(String msg) { new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK).show(); } + private static void warn(String msg) { new Alert(Alert.AlertType.WARNING, msg, ButtonType.OK).show(); } + private static void error(String msg) { new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK).show(); } + private static String safeMsg(Throwable t) { return (t == null || t.getMessage() == null) ? "Unknown error" : t.getMessage(); } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index c970969..932d8a2 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -1289,7 +1289,6 @@ public class MainController { public void addChatAndSelect(org.to.telegramfinalproject.Models.ChatEntry entry) { - // در Session نگه‌داری if (org.to.telegramfinalproject.Client.Session.chatList.stream() .noneMatch(c -> c.getId().equals(entry.getId()))) { org.to.telegramfinalproject.Client.Session.chatList.add(0, entry); diff --git a/src/main/java/org/to/telegramfinalproject/UI/OwnerTransferDialogController.java b/src/main/java/org/to/telegramfinalproject/UI/OwnerTransferDialogController.java new file mode 100644 index 0000000..f5caf2c --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/OwnerTransferDialogController.java @@ -0,0 +1,271 @@ +package org.to.telegramfinalproject.UI; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.transformation.FilteredList; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; + +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.UUID; + +import static org.to.telegramfinalproject.UI.LeaveGroupFlow.confirm; + +public class OwnerTransferDialogController { + + @FXML private TextField searchField; + @FXML private ListView adminList; + @FXML private Button okBtn; + @FXML private Button cancelBtn; + + private UUID groupId; + private String viewerUuid; // internal UUID string + private Runnable onSuccess; // callback برای refresh UI + + private FilteredList filtered; + + // ===== API ===== + public void init(UUID groupId, String viewerUuid, Runnable onSuccess) { + this.groupId = groupId; + this.viewerUuid = viewerUuid; + this.onSuccess = onSuccess; + setupUI(); + loadAdmins(); + } + + private void setupUI() { + okBtn.setDisable(true); + adminList.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null)); + cancelBtn.setOnAction(e -> cancel()); + + // Double-click = OK + adminList.setOnMouseClicked(e -> { + if (e.getClickCount() == 2 && adminList.getSelectionModel().getSelectedItem() != null) { + onOkClick(); + } + }); + + // Cell factory (Avatar + name + id) + adminList.setCellFactory(v -> new ListCell<>() { + private final HBox row = new HBox(10); + private final ImageView avatar = new ImageView(); + private final Label name = new Label(); + private final Label id = new Label(); + { + avatar.setFitWidth(32); avatar.setFitHeight(32); avatar.setPreserveRatio(true); + row.getChildren().addAll(avatar, name, id); + } + @Override protected void updateItem(AdminRow it, boolean empty) { + super.updateItem(it, empty); + if (empty || it == null) { + setGraphic(null); setText(null); + } else { + name.setText(it.name); + id.setText("• " + it.userId); + if (it.avatarUrl != null && !it.avatarUrl.isBlank()) { + try { avatar.setImage(new Image(it.avatarUrl, true)); } catch (Exception ex) { avatar.setImage(null); } + } else avatar.setImage(null); + setGraphic(row); setText(null); + } + } + }); + + // search + searchField.textProperty().addListener((obs, o, n) -> { + final String q = (n == null) ? "" : n.trim().toLowerCase(Locale.ROOT); + filtered.setPredicate(makePredicate(q)); + }); + + okBtn.setOnAction(e -> onOkClick()); + } + + private Predicate makePredicate(String q) { + if (q.isBlank()) return r -> true; + return r -> r.name.toLowerCase(Locale.ROOT).contains(q) + || normalizeDisplayId(r.userId).contains(q); + } + + private void loadAdmins() { + ProgressIndicator pi = modalSpinner("Loading admins..."); + + Task> t = new Task<>() { + @Override protected List call() { + JSONObject req = new JSONObject() + .put("action", "view_group_admins") + .put("group_id", groupId.toString()) + .put("exclude_self", true) + .put("viewer_id", viewerUuid); + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equalsIgnoreCase(res.optString("status"))) { + throw new RuntimeException(res != null ? res.optString("message","Failed to fetch admins.") + : "null response"); + } + JSONArray arr = res.getJSONObject("data").optJSONArray("admins"); + List list = new ArrayList<>(); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + JSONObject a = arr.getJSONObject(i); + list.add(new AdminRow( + a.optString("internal_uuid",""), + a.optString("user_id",""), + a.optString("profile_name","Unknown"), + a.optString("avatar_url","") + )); + } + } + return list; + } + }; + + t.setOnSucceeded(ev -> { + closeSpinner(pi); + List admins = t.getValue(); + if (admins == null) admins = List.of(); + if (admins.isEmpty()) { + alert(Alert.AlertType.WARNING, "No other admins available. Promote an admin first, then try again."); + } + filtered = new FilteredList<>(FXCollections.observableArrayList(admins), r -> true); + adminList.setItems(filtered); + }); + + t.setOnFailed(ev -> { + closeSpinner(pi); + alert(Alert.AlertType.ERROR, "Failed to load admins: " + safeMsg(t.getException())); + }); + + new Thread(t, "load-admins").start(); + } + + private void onOkClick() { + AdminRow sel = adminList.getSelectionModel().getSelectedItem(); + if (sel == null) return; + + if (!confirm("Confirm Ownership Transfer", + "Transfer ownership to " + sel.display() + "?\nYou will leave the group afterward.")) return; + + ProgressIndicator pi = modalSpinner("Transferring & leaving..."); + + Task t = new Task<>() { + @Override protected Void call() { + // 1) تلاش برای اکشن اتمیک + JSONObject atomic = new JSONObject() + .put("action", "transfer_and_leave_group") + .put("group_id", groupId.toString()) + .put("viewer_id", viewerUuid) + .put("new_owner_id", sel.requireUuid().toString()); + + JSONObject res = ActionHandler.sendWithResponse(atomic); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + return null; + } + + // 2) Fallback: دو مرحله‌ای + JSONObject tReq = new JSONObject() + .put("action", "transfer_group_ownership") + .put("group_id", groupId.toString()) + .put("new_owner_id", sel.requireUuid().toString()); + JSONObject tRes = ActionHandler.sendWithResponse(tReq); + if (tRes == null || !"success".equalsIgnoreCase(tRes.optString("status"))) { + throw new RuntimeException(tRes != null ? tRes.optString("message","Ownership transfer failed.") + : "null response"); + } + + JSONObject lReq = new JSONObject() + .put("action", "leave_chat") + .put("user_id", viewerUuid) + .put("chat_id", groupId.toString()) + .put("chat_type", "group"); + JSONObject lRes = ActionHandler.sendWithResponse(lReq); + if (lRes == null || !"success".equalsIgnoreCase(lRes.optString("status"))) { + throw new RuntimeException(lRes != null ? lRes.optString("message","Leave failed.") + : "null response"); + } + return null; + } + }; + + t.setOnSucceeded(ev -> { + closeSpinner(pi); + alert(Alert.AlertType.INFORMATION, "Done. You left the group."); + closeDialog(); + if (onSuccess != null) onSuccess.run(); + }); + + t.setOnFailed(ev -> { + closeSpinner(pi); + alert(Alert.AlertType.ERROR, "Operation failed: " + safeMsg(t.getException())); + }); + + new Thread(t, "transfer-leave").start(); + } + + private void cancel() { closeDialog(); } + + // ===== Utilities ===== + private ProgressIndicator modalSpinner(String title) { + Alert a = new Alert(Alert.AlertType.NONE); + a.setTitle(title); + ProgressIndicator pi = new ProgressIndicator(); + a.getDialogPane().setContent(pi); + a.getButtonTypes().clear(); + a.show(); + pi.getProperties().put("alert", a); + return pi; + } + private void closeSpinner(ProgressIndicator pi) { + Object a = pi.getProperties().get("alert"); + if (a instanceof Alert) ((Alert)a).close(); + } + + private void closeDialog() { + // بستن پنجره میزبان کنترلر (DialogStage): + adminList.getScene().getWindow().hide(); + } + + private static String normalizeDisplayId(String s) { + if (s == null) return ""; + String t = s.trim(); + if (t.startsWith("@")) t = t.substring(1); + return t.toLowerCase(Locale.ROOT); + } + private static String safeMsg(Throwable t) { return (t == null || t.getMessage() == null) ? "Unknown error" : t.getMessage(); } + + // ===== Row model ===== + public static final class AdminRow { + public final UUID internalUuid; + public final String userId; + public final String name; + public final String avatarUrl; + + public AdminRow(String internalUuidStr, String userId, String name, String avatarUrl) { + this.internalUuid = parseUuidOrNull(internalUuidStr); + this.userId = userId == null ? "" : userId; + this.name = name == null ? "Unknown" : name; + this.avatarUrl = avatarUrl; + } + public UUID requireUuid() { + if (internalUuid == null) throw new IllegalStateException("admin.internal_uuid missing"); + return internalUuid; + } + public String display() { return name + " (" + userId + ")"; } + + private static UUID parseUuidOrNull(String s) { + try { return (s == null || s.isBlank()) ? null : UUID.fromString(s); } + catch (Exception e) { return null; } + } + } + + private static void alert(Alert.AlertType type, String msg) { + new Alert(type, msg, ButtonType.OK).show(); + } +} diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/owner_transfer_dialog.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/owner_transfer_dialog.fxml new file mode 100644 index 0000000..90764c3 --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/owner_transfer_dialog.fxml @@ -0,0 +1,44 @@ + + + + + + + + + + + +

+ + + + + + + + +
+ +
+ + + +