From 106cfbbf3d5567289c13b5e0828e8f079e4e4a61 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 5 Sep 2025 13:12:22 +0330 Subject: [PATCH 1/2] add to group or channel real time --- src/main/java/module-info.java | 13 +- .../Client/IncomingMessageListener.java | 145 +++- .../telegramfinalproject/Client/Session.java | 1 - .../UI/ChatPageController.java | 662 +++++++++++------- .../UI/MainController.java | 90 ++- .../org/to/telegramfinalproject/CSS/chat.css | 25 +- .../telegramfinalproject/Fxml/chat_page.fxml | 8 +- .../0ae6648c-9b72-43db-8229-206d8134c0f3.jpg | Bin 0 -> 3804 bytes .../3336cf86-cb4e-47d6-85c2-44431a1ed07f.jpg | Bin 0 -> 53255 bytes .../5b195266-4c7b-4b66-8303-c469dd49b5ed.jpg | Bin 0 -> 5457 bytes .../91103024-fa95-4ee9-aa82-7b6e50ac960f.jpg | Bin 0 -> 53255 bytes .../a60e9051-ca4c-4eaa-95b8-417044af8569.jpg | Bin 0 -> 7851 bytes .../ca9f35f2-8ecb-450f-8b56-b6b67c87f256.jpg | Bin 0 -> 6920 bytes 13 files changed, 644 insertions(+), 300 deletions(-) create mode 100644 uploads/avatars/2025-09-05/0ae6648c-9b72-43db-8229-206d8134c0f3.jpg create mode 100644 uploads/avatars/2025-09-05/3336cf86-cb4e-47d6-85c2-44431a1ed07f.jpg create mode 100644 uploads/avatars/2025-09-05/5b195266-4c7b-4b66-8303-c469dd49b5ed.jpg create mode 100644 uploads/avatars/2025-09-05/91103024-fa95-4ee9-aa82-7b6e50ac960f.jpg create mode 100644 uploads/avatars/2025-09-05/a60e9051-ca4c-4eaa-95b8-417044af8569.jpg create mode 100644 uploads/avatars/2025-09-05/ca9f35f2-8ecb-450f-8b56-b6b67c87f256.jpg diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 274c4f7..9741c93 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -15,8 +15,13 @@ module org.to.telegramfinalproject { requires spark.core; requires javax.servlet.api; requires mp3agic; - opens org.to.telegramfinalproject to javafx.fxml; - exports org.to.telegramfinalproject; - exports org.to.telegramfinalproject.Client; + + // FXML کنترلرها در این پکیج‌اند: + opens org.to.telegramfinalproject.UI to javafx.fxml; + // اگر FXML از کلاس‌های Client هم استفاده می‌کند: opens org.to.telegramfinalproject.Client to javafx.fxml; -} \ No newline at end of file + + // فقط اگر لازم است از بیرون به این API‌ها کامپایل شود: + exports org.to.telegramfinalproject.Client; + // ⚠️ عمداً NOT exporting/opens ریشه‌ی پکیج، چون کلاس ندارد. +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 803fc78..6c7be2a 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -138,22 +138,22 @@ public class IncomingMessageListener implements Runnable { JSONObject finalMsg = msg; JSONObject finalMsg1 = msg; switch (action) { - case "added_to_group", "added_to_channel", - "removed_from_group", "removed_from_channel", - "chat_deleted", "created_private_chat" -> { - // این قسمت مستقل از UI/کنسول است - System.out.println("🔄 Chat list changed. Updating..."); - Session.forceRefreshChatList = true; - - String chatId = msg.getString("chat_id"); - String chatType = msg.getString("chat_type"); - ActionHandler.requestChatInfo(chatId, chatType); - - if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) { - System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting..."); - ActionHandler.forceExitChat = true; - } - } +// case "added_to_group", "added_to_channel", +// "removed_from_group", "removed_from_channel", +// "chat_deleted", "created_private_chat" -> { +// // این قسمت مستقل از UI/کنسول است +// System.out.println("🔄 Chat list changed. Updating..."); +// Session.forceRefreshChatList = true; +// +// String chatId = msg.getString("chat_id"); +// String chatType = msg.getString("chat_type"); +// ActionHandler.requestChatInfo(chatId, chatType); +// +// if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) { +// System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting..."); +// ActionHandler.forceExitChat = true; +// } +// } // case "chat_updated" -> { // if (uiMode == UIMode.UI) { @@ -163,7 +163,89 @@ public class IncomingMessageListener implements Runnable { // } // } - case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> { + case "added_to_group": + case "added_to_channel": + case "created_private_chat": { + // داده‌ها + UUID chatId = UUID.fromString(msg.getString("chat_id")); + String type = msg.getString("chat_type"); // "group" | "channel" | "private" + String name = msg.optString("name", ""); + String imgUrl = msg.optString("image_url", ""); + + // یک ChatEntry مینیمال بساز (تا UI سریع واکنش بده) + ChatEntry ce = new ChatEntry(); + ce.setId(chatId.toString()); + ce.setType(type); + ce.setName(name); + ce.setImageUrl(imgUrl); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc == null) return; + + // به لیست‌ها اضافه و UI را رفرش می‌کند (متد خودت) + mc.onJoinedOrAdded(ce); + + // اگر همین چت الان بازه، مود مناسب را اعمال کن + var cpc = mc.getChatPageController(); + if (cpc != null && cpc.isSameChat(chatId, type)) { + // ❗ اگر applyMode در ChatPageController private است، + // یا publicش کن یا این دو خط را حذف کن. + // گروه → NORMAL ، کانال → READ_ONLY (مگر اینکه اجازه پست داشته باشی) + // cpc.applyMode("group".equalsIgnoreCase(type) ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); + // cpc.fetchAndRenderHeader(ce); // اختیاری: هدر را تازه کن + } + }); + break; + } + + case "removed_from_group": + case "removed_from_channel": { + UUID chatId = UUID.fromString(msg.getString("chat_id")); + String type = msg.getString("chat_type"); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc == null) return; + + removeFromAllLists(chatId); + mc.refreshChatListUI(); + + // اگر همین چت باز است → به حالت نیاز به Join برگرد + var cpc = mc.getChatPageController(); + if (cpc != null && cpc.isSameChat(chatId, type)) { + // اگر applyMode private است، این خط را کامنت کن یا publicش کن + // cpc.applyMode(ChatViewMode.NEEDS_JOIN); + } + }); + break; + } + + case "chat_deleted": { + UUID chatId = UUID.fromString(msg.getString("chat_id")); + String type = msg.getString("chat_type"); + + Platform.runLater(() -> { + var mc = MainController.getInstance(); + if (mc == null) return; + + removeFromAllLists(chatId); + mc.refreshChatListUI(); + + var cpc = mc.getChatPageController(); + if (cpc != null && cpc.isSameChat(chatId, type)) { + // حداقل ورودی را ببندیم/غیرفعال کنیم + // اگر applyMode private است، این خط را کامنت کن یا publicش کن + // cpc.applyMode(ChatViewMode.READ_ONLY); + // و یک پیام سیستمی هم نشان بده + cpc.addSystemMessage("This chat was deleted."); + } + }); + break; + } + + + case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" : { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { try { @@ -174,7 +256,7 @@ public class IncomingMessageListener implements Runnable { }).start(); } - case "new_message" -> { + case "new_message" :{ JSONObject data = response.optJSONObject("data"); if (data == null) break; @@ -198,7 +280,7 @@ public class IncomingMessageListener implements Runnable { }); } - case "message_edited" -> { + case "message_edited": { JSONObject ui = normalizeMessageId(msg); // (اختیاری) اگر ایونت زمان و چت را هم می‌دهد، می‌توانی چت‌لیست را آپدیت کنی Platform.runLater(() -> { @@ -208,7 +290,7 @@ public class IncomingMessageListener implements Runnable { }); } - case "message_deleted_global", "message_deleted_one_sided", "message_deleted" -> { + case "message_deleted_global", "message_deleted_one_sided", "message_deleted" : { JSONObject ui = normalizeMessageId(msg); Platform.runLater(() -> { var mc = MainController.getInstance(); @@ -217,7 +299,7 @@ public class IncomingMessageListener implements Runnable { }); } - case "message_reacted", "message_unreacted" -> { + case "message_reacted", "message_unreacted" : { JSONObject ui = normalizeMessageId(msg); Platform.runLater(() -> { var mc = MainController.getInstance(); @@ -228,12 +310,12 @@ public class IncomingMessageListener implements Runnable { - case "chat_updated" -> { + case "chat_updated": { var data = response.getJSONObject("data"); bumpChatListFromUpdate(data); } - case "user_status_changed" -> { + case "user_status_changed" : { displayRealTimeMessage(action, msg); Platform.runLater(() -> { @@ -250,11 +332,11 @@ public class IncomingMessageListener implements Runnable { - case "blocked_by_user", "unblocked_by_user", "message_seen" -> { + case "blocked_by_user", "unblocked_by_user", "message_seen" : { displayRealTimeMessage(action, msg); } - default -> { + default :{ System.out.println("\n❓ Unknown real-time action: " + action); System.out.println(msg.toString(2)); } @@ -550,4 +632,17 @@ public class IncomingMessageListener implements Runnable { } + private void removeFromAllLists(UUID chatId) { + if (Session.chatList != null) { + Session.chatList.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId()))); + } + if (Session.activeChats != null) { + Session.activeChats.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId()))); + } + if (Session.archivedChats != null) { + Session.archivedChats.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId()))); + } + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index b6fcb77..a555949 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -133,7 +133,6 @@ public class Session { resortAndRefresh(); } - /** سورت بر اساس lastMessageTime (نزولی) و آپدیت active/archived */ public static void resortAndRefresh() { chatList.sort((c1, c2) -> { if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index b4eabd5..4a37b8c 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -64,6 +64,8 @@ public class ChatPageController { @FXML private ImageView searchIcon; + @FXML private VBox blockedPane; + @FXML private Button moreButton; // 3-dots button @FXML @@ -114,6 +116,8 @@ public class ChatPageController { private boolean blockedByMeFlag = false; private boolean blockedMeFlag = false; + private volatile boolean justJoinedThisChat = false; + // ===== state ===== @@ -372,7 +376,6 @@ public class ChatPageController { // if (!text.isEmpty()) { // addMessage("You", text); // messageInput.clear(); -// // TODO: send to server // } // } @@ -691,7 +694,7 @@ public class ChatPageController { // messageContainer.getChildren().add(msg); // } - private void addSystemMessage(String content) { + public void addSystemMessage(String content) { Label sys = new Label(content); sys.setStyle("-fx-text-fill: gray; -fx-font-size: 11;"); messageContainer.getChildren().add(sys); @@ -699,38 +702,38 @@ 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)); - } +// /** +// * 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); @@ -759,7 +762,7 @@ public class ChatPageController { // } else { // setDefaultHeaderAvatarByType(entry.getType()); // } -//// userAvatar.setClip(new Circle(20, 20, 20)); + //// userAvatar.setClip(new Circle(20, 20, 20)); // AvatarFX.circleClip(userAvatar, 36); // // @@ -800,37 +803,37 @@ public class ChatPageController { // } -public void showChat(ChatEntry entry) { - this.currentChat = entry; - this.chatName = entry.getName(); + 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); + + + // حالت اولیه (بدون انتظار هدر) + if ("channel".equalsIgnoreCase(entry.getType())) { + boolean canPostLocal = entry.isOwner() || entry.isAdmin() + || (entry.getPermissions()!=null && entry.getPermissions().optBoolean("can_post", false)); + applyMode(canPostLocal ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); + } else { + applyMode(ChatViewMode.NORMAL); + } + + messageContainer.getChildren().clear(); + loadMessages(entry); + markAsRead(entry); + + // حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم + fetchAndRenderHeader(entry); - 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); - - - // حالت اولیه (بدون انتظار هدر) - if ("channel".equalsIgnoreCase(entry.getType())) { - boolean canPostLocal = entry.isOwner() || entry.isAdmin() - || (entry.getPermissions()!=null && entry.getPermissions().optBoolean("can_post", false)); - applyMode(canPostLocal ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); - } else { - applyMode(ChatViewMode.NORMAL); - } - - messageContainer.getChildren().clear(); - loadMessages(entry); - markAsRead(entry); - - // حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم - fetchAndRenderHeader(entry); - -} @@ -1175,7 +1178,7 @@ public void showChat(ChatEntry entry) { // 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()); @@ -1186,110 +1189,110 @@ public void showChat(ChatEntry entry) { -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"); + 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); + // === متن/نوع پیام === + 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"); + 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; + // === رنگ بابل‌ها + 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;" - ); + 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); + // === بدنه‌ی بابل === + 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()); + // برچسب‌گذاری بابل برای پیدا کردنش در آپدیت‌های 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 ContextMenu buildMessageMenu(boolean isMine, String messageId, String type, String content) { ContextMenu menu = new ContextMenu(); @@ -1748,37 +1751,30 @@ private void addBubble( data.optString("last_seen", null) )); - // ⭐️ بلاک؟ - boolean blocked = data.optBoolean("blocked", false) - || data.optBoolean("is_blocked", false) - || data.optBoolean("blocked_by_me", false); - - if (blocked) { - if (readOnlyLabel != null) readOnlyLabel.setText(""); // فقط UNBLOCK را نشان بده - applyMode(ChatViewMode.BLOCKED); - } else { - applyMode(ChatViewMode.NORMAL); - } + // ❌ هیچ applyMode اینجا نزن! + // حتی اگر blocked آمد، تصمیم مود از بیرون می‌آید. } + private void updateGroupHeader(ChatEntry entry, JSONObject data) { chatTitle.setText(nz(data.optString("group_name", entry.getName()))); String img = data.optString("image_url", ""); if (hasVal(img)) { try { - Image im = AvatarLocalResolver.load(img); // ⬅️ + Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} } - int members = data.optInt("member_count", 0); int online = data.optInt("online_count", -1); chatStatus.setText(online >= 0 ? (members + " members, " + online + " online") : (members + " members")); + + // ❌ هیچ applyMode اینجا نزن! } // private void updateChannelHeader(ChatEntry entry, JSONObject data) { @@ -1814,14 +1810,8 @@ private void addBubble( int subs = data.optInt("member_count", 0); chatStatus.setText(subs + " subscribers"); - boolean canPost = canPostToChannel(entry, data); - if (canPost) { - applyMode(ChatViewMode.NORMAL); - Platform.runLater(() -> messageInput.requestFocus()); - } else { - if (readOnlyLabel != null) readOnlyLabel.setText("YOU CAN’T SEND MESSAGES IN THIS CHANNEL"); - applyMode(ChatViewMode.READ_ONLY); - } + // ❌ هیچ applyMode اینجا نزن! + // حتی اگر can_post را بده، به مود دست نزن. } @@ -1876,11 +1866,46 @@ private void addBubble( )); } +// @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() { if (currentChat == null) return; - // 1) internal_uuid کاربر فعلی (UUID) String myInternalUuid = Session.currentUser != null ? Session.currentUser.optString("internal_uuid", "") : ""; @@ -1889,27 +1914,71 @@ private void addBubble( return; } - // 2) internal_uuid مقصد (گروه/کانال) - String targetId = currentChat.getId().toString(); + final String targetId = currentChat.getId().toString(); + final String t = currentChat.getType(); + final String action = "group".equalsIgnoreCase(t) ? "join_group" : "join_channel"; - // 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 گروه/کانال + .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")); - } + // ⛔ روی ترد FX کال شبکه نزن! + new Thread(() -> { + JSONObject res = ActionHandler.sendWithResponse(req); + boolean ok = (res != null && "success".equalsIgnoreCase(res.optString("status"))); + + Platform.runLater(() -> { + if (!ok) { + 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 اگر همین را می‌خواهی + } + + // هدر را دوباره بگیر (ولی نگذار مود را خراب کند) + fetchAndRenderHeader(currentChat); + }); + }).start(); } // @@ -2041,6 +2110,55 @@ private void addBubble( +// 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; @@ -2050,36 +2168,27 @@ private void addBubble( boolean readOnly = (mode == ChatViewMode.READ_ONLY); boolean blocked = (mode == ChatViewMode.BLOCKED); - // Composer فقط در حالت نرمال + // فقط در حالت نرمال: کامپوزر composerPane.setVisible(normal); composerPane.setManaged(normal); - // Join / Add + // پنل‌های 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); + readOnlyPane.setVisible(readOnly); + readOnlyPane.setManaged(readOnly); + } + if (blockedPane != null) { + blockedPane.setVisible(blocked); + blockedPane.setManaged(blocked); } - // متن آبی برای 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()); @@ -2090,32 +2199,53 @@ private void addBubble( } + + @FXML private void onUnblockClicked() { if (currentChat == null) return; - UUID other = currentChat.getOtherUserId(); - if (other == null && currentChat.getDisplayId() == null) return; - - org.json.JSONObject req = new org.json.JSONObject() - .put("action", "toggle_block") - .put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")) - .put("target_id", (other != null) ? other.toString() : currentChat.getDisplayId()); - - org.json.JSONObject res = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); - boolean ok = (res != null) && ("ok".equalsIgnoreCase(res.optString("status")) - || "success".equalsIgnoreCase(res.optString("status"))); - - if (ok) { - applyMode(ChatViewMode.NORMAL); - Platform.runLater(() -> messageInput.requestFocus()); - } else { - addSystemMessage("Unblock failed."); + final String viewerUuid = org.to.telegramfinalproject.Client.Session.getUserUUID(); // internal_uuid + if (viewerUuid == null || viewerUuid.isBlank()) { + addSystemMessage("Missing viewer UUID"); + return; } + + // شبکه روی بک‌گراند + new Thread(() -> { + // 1) اگر otherUserId نداشتیم، از سرور بگیر + java.util.UUID other = currentChat.getOtherUserId(); + if (other == null) { + other = resolvePeerUuidFromServer(currentChat); + } + if (other == null) { + Platform.runLater(() -> addSystemMessage("Could not resolve peer UUID.")); + return; + } + + // 2) حالا درخواست آن‌بلاک + org.json.JSONObject req = new org.json.JSONObject() + .put("action", "toggle_block") + .put("user_id", viewerUuid) // internal_uuid خودت + .put("target_id", other.toString()); // internal_uuid طرف مقابل + + org.json.JSONObject res = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + boolean ok = res != null && "success".equalsIgnoreCase(res.optString("status")); + + Platform.runLater(() -> { + if (ok) { + applyMode(ChatViewMode.NORMAL); + messageInput.requestFocus(); + } else { + 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")) { @@ -2587,7 +2717,63 @@ private void addBubble( } + private void syncIconsWithTheme() { + boolean dark = themeManager.isDarkMode(); + String suffix = dark ? "_light.png" : "_dark.png"; + + if (attachmentIcon != null) attachmentIcon.setImage(loadIcon("attachment" + suffix)); + if (sendIcon != null) sendIcon.setImage(loadIcon("send_cyan2.png")); + 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 (chatStatus != null) chatStatus.setStyle(dark ? "-fx-text-fill:#8ea1b2;" : "-fx-text-fill:#7e8a97;"); + + // 👇 این خط قبلاً ممکن بود NPE بده + if (viewProfileItem != null && viewProfileItem.getGraphic() instanceof ImageView iv) { + iv.setImage(loadIcon("view_profile" + suffix)); + } + } -} + // 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) {} + + // درخواست به سرور برای گرفتن 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; + + org.json.JSONObject data = res.optJSONObject("data"); + if (data == null) return null; + + String tid = data.optString("target_id", ""); + if (tid == null || tid.isBlank()) return null; + + try { + java.util.UUID target = java.util.UUID.fromString(tid); + chat.setOtherUserId(target); // کش محلی کن که دفعات بعد لازم نشه + return target; + } catch (Exception ignore) { + return null; + } + } + + + + +} \ 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 cdcab0d..7a5b08e 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -156,7 +156,7 @@ public class MainController { } - void refreshChatListUI() { + public void refreshChatListUI() { Platform.runLater(() -> { chatListContainer.getChildren().clear(); itemControllers.clear(); @@ -433,31 +433,81 @@ public class MainController { } } +// void openChat(ChatEntry chat) { +// try { +// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); +// Node chatPage = loader.load(); +// +// +// +// +// +// +// +// ChatPageController controller = loader.getController(); +// controller.showChat(chat); +// +// this.chatPageController = controller; +// Session.currentChatId = chat.getId().toString(); +// +// chatDisplayArea.getChildren().setAll(chatPage); +// chat.setUnreadCount(0); +// ChatItemController item = itemControllers.get(chat.getId()); +// if (item != null) item.setUnread(0); +// +// } catch (IOException ex) { +// ex.printStackTrace(); +// } +// } + void openChat(ChatEntry chat) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); - Node chatPage = loader.load(); + // فقط برای پرایوت: اول بلاک/بلاک‌شدن را چک کن، بعد مود را تعیین کن + if ("private".equalsIgnoreCase(chat.getType())) { + final String viewerId = org.to.telegramfinalproject.Client.Session.getUserUUID(); // internal_uuid کاربر فعلی + if (viewerId != null && !viewerId.isBlank()) { + new Thread(() -> { + org.json.JSONObject req = new org.json.JSONObject() + .put("action", "check_block_status_by_chat") + .put("viewer_id", viewerId) + .put("chat_id", chat.getId().toString()); + org.json.JSONObject res = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + boolean blockedByMe = false; + boolean blockedMe = false; + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + org.json.JSONObject data = res.optJSONObject("data"); + if (data != null) { + // اسم فیلدها را با سرور خودت یکی کن + blockedByMe = data.optBoolean("blocked_by_me", false) || data.optBoolean("is_blocked", false); + blockedMe = data.optBoolean("blocked_me", false); + } + } + final ChatViewMode mode = blockedByMe + ? ChatViewMode.BLOCKED + : (blockedMe ? ChatViewMode.READ_ONLY : ChatViewMode.NORMAL); - - - - ChatPageController controller = loader.getController(); - controller.showChat(chat); - - this.chatPageController = controller; - Session.currentChatId = chat.getId().toString(); - - chatDisplayArea.getChildren().setAll(chatPage); - chat.setUnreadCount(0); - ChatItemController item = itemControllers.get(chat.getId()); - if (item != null) item.setUnread(0); - - } catch (IOException ex) { - ex.printStackTrace(); + Platform.runLater(() -> openChatWithMode(chat, mode)); + }).start(); + return; // نذار پایین دوباره باز شود + } } + + if ("channel".equalsIgnoreCase(chat.getType())) { + boolean canPost = + chat.isOwner() || chat.isAdmin() || + (chat.getPermissions() != null && + chat.getPermissions().optBoolean("can_post", false)); + // اگر فقط owner/admin ملاک توست، خط بالا را به این تغییر بده: + // boolean canPost = chat.isOwner() || chat.isAdmin(); + + openChatWithMode(chat, canPost ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); + return; + } + + // غیرپرایوت یا اگر viewerId نبود → نرمال + openChatWithMode(chat, ChatViewMode.NORMAL); } diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/chat.css b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css index 97d0747..bc3a46b 100644 --- a/src/main/resources/org/to/telegramfinalproject/CSS/chat.css +++ b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css @@ -56,20 +56,25 @@ -fx-background-color: transparent; } -/* لینک‌استایل دکمه‌ها در بنر */ -.chat-footer-banner .footer-link-btn { + + +.button.footer-link-btn { -fx-background-color: transparent; - -fx-text-fill: #1E88E5; /* آبی پیش‌فرض */ - -fx-font-weight: 700; - -fx-padding: 6 12; -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-padding: 6 12; + -fx-font-weight: 700; -fx-cursor: hand; + -fx-effect: null; + -fx-text-fill: #1E88E5; /* پیش‌فرض آبی */ } -.chat-footer-banner .footer-link-btn:hover { +.button.footer-link-btn:hover { -fx-underline: true; } -/* نسخه قرمز برای UNBLOCK */ -.chat-footer-banner .footer-link-btn.danger { - -fx-text-fill: #D32F2F; /* قرمز */ -} +/* نسخه‌ی قرمز برای UNBLOCK */ +.button.footer-link-btn.danger { + -fx-text-fill: #D32F2F; +} \ No newline at end of file diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml index 2660490..d2f88fc 100644 --- a/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml @@ -147,10 +147,14 @@ visible="false" managed="false" styleClass="chat-footer-banner"> -