From 9dbdf89831dad05e273539f276363c1a41a0e704 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 4 Sep 2025 23:19:20 +0330 Subject: [PATCH] Handle contact on UI --- .../UI/ChatPageController.java | 119 +++++++- .../UI/ContactsController.java | 287 +++++++++++++----- .../UI/MainController.java | 7 +- 3 files changed, 324 insertions(+), 89 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index dc0b29e..1cc170f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -1861,39 +1861,105 @@ private void addBubble( } } +// +// @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", "") : ""; + if (myUserId.isBlank()) { + addSystemMessage("Add contact failed: missing user_id."); + return; + } - // 2) internal_uuid طرف مقابل + // 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 طرف مقابل + .put("user_id", myUserId) + .put("contact_id", other.toString()); - // 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")); - } + new Thread(() -> { + JSONObject res = ActionHandler.sendWithResponse(req); + boolean ok = (res != null && "success".equalsIgnoreCase(res.optString("status"))); + + Platform.runLater(() -> { + if (!ok) { + 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(), // آواتار (اگر هست) + false, // is_blocked + null // last_seen + ); + + if (Session.contactEntries == null) + Session.contactEntries = new java.util.ArrayList<>(); + + boolean exists = Session.contactEntries.stream() + .anyMatch(c -> other.equals(c.getContactId())); + if (!exists) Session.contactEntries.add(ce); + } catch (Exception ignore) {} + + + showOpenFromContactsHint(); + }); + }).start(); } @@ -2450,5 +2516,28 @@ private void addBubble( } + 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.setUnderline(true); + + addContactPane.getChildren().add(hint); + + // مطمئن شو فقط همین پانل دیده شود (کامپوزر/بقیه بسته بمانند) + composerPane.setVisible(false); + composerPane.setManaged(false); + joinPane.setVisible(false); + joinPane.setManaged(false); + addContactPane.setVisible(true); + addContactPane.setManaged(true); + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java b/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java index 7277033..21b017f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ContactsController.java @@ -3,12 +3,21 @@ package org.to.telegramfinalproject.UI; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.geometry.Pos; +import javafx.scene.Cursor; 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 org.to.telegramfinalproject.Models.ContactEntry; +import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; public class ContactsController { @@ -21,123 +30,171 @@ public class ContactsController { @FXML private ScrollPane contactsScroll; @FXML private Button searchIcon; - // Sample data for testing (later fetch from DB/server) - private final List allContacts = Arrays.asList( - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") - ); + /** منبع داده UI — با کانتکت‌های واقعی پر می‌شود */ + private final List allContacts = new ArrayList<>(); @FXML public void initialize() { - // Sort contacts alphabetically on load - List sorted = allContacts.stream() - .sorted(Comparator.comparing(Contact::getName)) - .collect(Collectors.toList()); - renderContacts(sorted); + // 1) لود اولیه‌ی کانتکت‌ها از سشن/سرور + loadContactsAndRender(); - // Search filter - searchField.textProperty().addListener((obs, oldVal, newVal) -> { - String filter = newVal.toLowerCase(); - List filtered = allContacts.stream() - .filter(c -> c.getName().toLowerCase().contains(filter)) - .sorted(Comparator.comparing(Contact::getName)) + // 2) سرچ محلی روی لیست + searchField.textProperty().addListener((obs, ov, nv) -> { + String f = nv == null ? "" : nv.trim().toLowerCase(); + List filtered = allContacts.stream() + .filter(c -> c.profileName.toLowerCase().contains(f) + || (c.userId != null && c.userId.toLowerCase().contains(f))) + .sorted(Comparator.comparing(c -> c.profileName.toLowerCase())) .collect(Collectors.toList()); renderContacts(filtered); }); - // Auto_focus search bar when overlay opens + // 3) فوکوس خودکار روی سرچ Platform.runLater(() -> searchField.requestFocus()); - // Close with footer button + // 4) بستن اورلی closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent())); + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(contactsCard.getParent())); - // Close when clicking outside card - overlayBackground.setOnMouseClicked(e -> { - MainController.getInstance().closeOverlay(contactsCard.getParent()); - }); - - // Smooth scroll feel for contacts list + // 5) اسکرول نرم contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); contactsScroll.setPannable(true); contactsScroll.setFitToWidth(true); contactsScroll.setFitToHeight(false); contactsScroll.getContent().setOnScroll(event -> { - double deltaY = event.getDeltaY() * 0.003; // smaller = smoother + double deltaY = event.getDeltaY() * 0.003; contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY); }); - // Register scene for ThemeManager → stylesheet swap will handle colors/icons + // 6) ثبت صحنه برای ThemeManager Platform.runLater(() -> { if (contactsCard.getScene() != null) { ThemeManager.getInstance().registerScene(contactsCard.getScene()); } }); - // Listener for theme change - ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> { - updateSearchIcon(newVal); - }); - - // Set initial state + // 7) واکنش به تغییر تم + ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal)); updateSearchIcon(ThemeManager.getInstance().isDarkMode()); } - private void renderContacts(List contacts) { + // ----------------------- + // لود داده و رندر لیست + // ----------------------- + private void loadContactsAndRender() { + // اگر Session.contactEntries از قبل لود شده، از همان استفاده می‌کنیم. + CompletableFuture + .supplyAsync(() -> { + try { + if (Session.contactEntries != null && !Session.contactEntries.isEmpty()) { + return new ArrayList<>(Session.contactEntries); + } + + // در غیر این صورت از سرور می‌گیریم (اختیاری) + // اگر API «view_contacts» داری، اینجا بفرست: + JSONObject req = new JSONObject() + .put("action", "view_contacts") // 🔧 اگر نام اکشن‌ات فرق دارد، تغییر بده + .put("user_id", Session.getUserUUID()); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equals(res.optString("status"))) { + return Collections.emptyList(); + } + JSONObject data = res.optJSONObject("data"); + JSONArray arr = data != null ? data.optJSONArray("contacts") : null; + if (arr == null) return Collections.emptyList(); + + List fetched = new ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + JSONObject c = arr.getJSONObject(i); + UUID contactId = UUID.fromString(c.getString("contact_id")); + String userId = c.optString("user_id", null); + String contactDisplay = c.optString("contact_display_id", userId); + String profileName = c.optString("profile_name", contactDisplay); + String imageUrl = c.optString("image_url", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"); + boolean isBlocked = c.optBoolean("is_blocked", false); + String lastSeenStr = c.optString("last_seen", null); + + LocalDateTime lastSeen = null; + if (lastSeenStr != null && !lastSeenStr.isEmpty()) { + try { lastSeen = LocalDateTime.parse(lastSeenStr); } catch (Exception ignore) {} + } + + 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); + return fetched; + } catch (Exception e) { + e.printStackTrace(); + return Collections.emptyList(); + } + }) + .thenAccept(entries -> Platform.runLater(() -> { + allContacts.clear(); + for (Object ce : entries) { + allContacts.add(ContactVM.from((ContactEntry) ce)); + } + allContacts.sort(Comparator.comparing(vm -> vm.profileName.toLowerCase())); + renderContacts(allContacts); + })); + } + + private void renderContacts(List contacts) { contactsList.getChildren().clear(); if (contacts.isEmpty()) { - contactsList.getChildren().clear(); - StackPane emptyPane = new StackPane(); - emptyPane.setPrefHeight(300); // << pushes it lower + emptyPane.setPrefHeight(300); emptyPane.setAlignment(Pos.CENTER); - Label emptyLabel = new Label("No contacts found"); emptyLabel.getStyleClass().add("no-contacts-label"); - emptyPane.getChildren().add(emptyLabel); - - // Do NOT give it prefHeight or Vgrow → prevents scroll bar contactsList.getChildren().add(emptyPane); return; } - for (Contact c : contacts) { + for (ContactVM c : contacts) { HBox item = new HBox(10); item.getStyleClass().add("contact-item"); + item.setCursor(Cursor.HAND); - // Avatar - ImageView avatar = new ImageView(new Image( - Objects.requireNonNull(getClass().getResourceAsStream(c.getImageUrl())) - )); + // آواتار + ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl)); avatar.setFitWidth(58); avatar.setFitHeight(58); avatar.setPreserveRatio(true); VBox details = new VBox(2); - Label nameLabel = new Label(c.getName()); + Label nameLabel = new Label(c.profileName); nameLabel.getStyleClass().add("contact-name"); - Label statusLabel = new Label(c.getStatus()); - statusLabel.getStyleClass().add("contact-status"); - - details.getChildren().addAll(nameLabel, statusLabel); + details.getChildren().addAll(nameLabel); item.getChildren().addAll(avatar, details); + + item.setOnMouseClicked(e -> openOrStartPrivateChat(c)); + contactsList.getChildren().add(item); } } + private Image loadAvatarSafe(String urlOrResource) { + try { + if (urlOrResource != null && urlOrResource.startsWith("/")) { + return new Image(Objects.requireNonNull(getClass().getResourceAsStream(urlOrResource))); + } + // اگر URL وب هم داری، می‌تونی مستقیم Image(url) بسازی + return new Image(Objects.requireNonNull( + getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png"))); + } catch (Exception ignore) { + return new Image(Objects.requireNonNull( + getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png"))); + } + } + private void updateSearchIcon(boolean darkMode) { String iconPath = darkMode ? "/org/to/telegramfinalproject/Icons/search_light.png" @@ -149,19 +206,103 @@ public class ContactsController { searchIcon.setGraphic(icon); } - // Inner class for contact data - public static class Contact { - private final String name; - private final String status; - private final String imageUrl; - public Contact(String name, String status, String imageUrl) { - this.name = name; - this.status = status; - this.imageUrl = imageUrl; + private void openOrStartPrivateChat(ContactVM contact) { + ChatEntry existing = findExistingPrivateChatWith(contact.contactId); + if (existing != null) { + MainController.getInstance().openChat(existing); + MainController.getInstance().closeOverlay(contactsCard.getParent()); + return; } - public String getName() { return name; } - public String getStatus() { return status; } - public String getImageUrl() { return imageUrl; } + + CompletableFuture + .supplyAsync(() -> { + try { + UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); + JSONObject req = new JSONObject() + .put("action", "get_or_create_private_chat") + .put("user1", myId.toString()) + .put("user2", contact.contactId.toString()); + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res == null || !"success".equals(res.optString("status"))) { + throw new RuntimeException(res != null ? res.optString("message", "Unknown error") + : "null response"); + } + JSONObject data = res.getJSONObject("data"); + UUID chatId = UUID.fromString(data.getString("chat_id")); + + ChatEntry entry = new ChatEntry( + chatId, + contact.userId, + contact.profileName, + contact.imageUrl, + "private", + null, + false, + false + ); + entry.setOtherUserId(contact.contactId); + + return entry; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }) + .thenAccept(entry -> Platform.runLater(() -> { + MainController.getInstance().onJoinedOrAdded(entry); + MainController.getInstance().openChat(entry); + MainController.getInstance().closeOverlay(contactsCard.getParent()); + })) + .exceptionally(err -> { + Platform.runLater(() -> { + Alert a = new Alert(Alert.AlertType.ERROR, "Failed to start chat: " + err.getMessage(), ButtonType.OK); + a.showAndWait(); + }); + return null; + }); } -} \ No newline at end of file + + + private ChatEntry findExistingPrivateChatWith(UUID otherUserUuid) { + if (Session.chatList != null) { + for (ChatEntry ce : Session.chatList) { + if (!"private".equalsIgnoreCase(ce.getType())) continue; + UUID stored = ce.getOtherUserId(); + if (stored != null && stored.equals(otherUserUuid)) return ce; + } + } + if (Session.activeChats != null) { + for (ChatEntry ce : Session.activeChats) { + if (!"private".equalsIgnoreCase(ce.getType())) continue; + UUID stored = ce.getOtherUserId(); + if (stored != null && stored.equals(otherUserUuid)) return ce; + } + } + return null; + } + + + // ----------------------- + // ViewModel ساده‌ی کانتکت + // ----------------------- + private static class ContactVM { + final UUID contactId; // internal UUID (واقعی) + final String userId; // @id نمایش (اختیاری) + final String profileName; + final String imageUrl; + + static ContactVM from(ContactEntry ce) { + return new ContactVM(ce.getContactId(), ce.getUserId(), ce.getProfileName(), ce.getImageUrl()); + } + + ContactVM(UUID contactId, String userId, String profileName, String imageUrl) { + this.contactId = contactId; + this.userId = userId; + this.profileName = profileName != null ? profileName : (userId != null ? userId : "Unknown"); + this.imageUrl = imageUrl != null ? imageUrl : "/org/to/telegramfinalproject/Avatars/default_user_profile.png"; + } + + + } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index de8886d..a1189f5 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -40,6 +40,7 @@ public class MainController { + private enum SearchMode { GLOBAL, CHAT @@ -429,7 +430,7 @@ public class MainController { } } - private void openChat(ChatEntry chat) { + void openChat(ChatEntry chat) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); Node chatPage = loader.load(); @@ -1149,4 +1150,8 @@ public class MainController { } } + + + + } \ No newline at end of file