Handle contact on UI

This commit is contained in:
2025-09-04 23:19:20 +03:30
parent fcb23f6d43
commit 9dbdf89831
3 changed files with 324 additions and 89 deletions
@@ -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 @FXML
private void onAddContactClicked() { private void onAddContactClicked() {
if (currentChat == null) return; if (currentChat == null) return;
// user_id من (همان string که سرور انتظار دارد)
String myUserId = Session.currentUser != null String myUserId = Session.currentUser != null
? Session.currentUser.optString("user_id", "") ? Session.currentUser.optString("user_id", "")
: ""; : "";
if (myUserId.isBlank()) {
addSystemMessage("Add contact failed: missing user_id.");
return;
}
// 2) internal_uuid طرف مقابل // UUID طرف مقابل (از هدر آمده)
UUID other = currentChat.getOtherUserId(); UUID other = currentChat.getOtherUserId();
if (other == null) { if (other == null) {
// اگر otherUserId هنوز نگرفته‌ای، بهتره قبلش از هدر/پروفایل بیاری.
addSystemMessage("Cannot add: other user UUID is missing."); addSystemMessage("Cannot add: other user UUID is missing.");
return; return;
} }
// 3) درخواست طبق قرارداد سرور // درخواست به سرور
JSONObject req = new JSONObject() JSONObject req = new JSONObject()
.put("action", "add_contact") .put("action", "add_contact")
.put("user_id", myUserId) // ← stringِ user_id (غیر UUID) .put("user_id", myUserId)
.put("contact_id", other.toString()); // ← UUID طرف مقابل .put("contact_id", other.toString());
// 4) ارسال new Thread(() -> {
JSONObject res = ActionHandler.sendWithResponse(req); JSONObject res = ActionHandler.sendWithResponse(req);
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { boolean ok = (res != null && "success".equalsIgnoreCase(res.optString("status")));
// به لیست چت‌ها اضافه و سوییچ به حالت نرمال
MainController.getInstance().onJoinedOrAdded(currentChat); Platform.runLater(() -> {
applyMode(ChatViewMode.NORMAL); if (!ok) {
Platform.runLater(() -> messageInput.requestFocus());
} else {
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(), // آواتار (اگر هست)
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);
}
} }
@@ -3,12 +3,21 @@ package org.to.telegramfinalproject.UI;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.*; 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.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ContactsController { public class ContactsController {
@@ -21,123 +30,171 @@ public class ContactsController {
@FXML private ScrollPane contactsScroll; @FXML private ScrollPane contactsScroll;
@FXML private Button searchIcon; @FXML private Button searchIcon;
// Sample data for testing (later fetch from DB/server) /** منبع داده UI — با کانتکت‌های واقعی پر می‌شود */
private final List<Contact> allContacts = Arrays.asList( private final List<ContactVM> allContacts = new ArrayList<>();
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")
);
@FXML @FXML
public void initialize() { public void initialize() {
// Sort contacts alphabetically on load // 1) لود اولیه‌ی کانتکت‌ها از سشن/سرور
List<Contact> sorted = allContacts.stream() loadContactsAndRender();
.sorted(Comparator.comparing(Contact::getName))
.collect(Collectors.toList());
renderContacts(sorted);
// Search filter // 2) سرچ محلی روی لیست
searchField.textProperty().addListener((obs, oldVal, newVal) -> { searchField.textProperty().addListener((obs, ov, nv) -> {
String filter = newVal.toLowerCase(); String f = nv == null ? "" : nv.trim().toLowerCase();
List<Contact> filtered = allContacts.stream() List<ContactVM> filtered = allContacts.stream()
.filter(c -> c.getName().toLowerCase().contains(filter)) .filter(c -> c.profileName.toLowerCase().contains(f)
.sorted(Comparator.comparing(Contact::getName)) || (c.userId != null && c.userId.toLowerCase().contains(f)))
.sorted(Comparator.comparing(c -> c.profileName.toLowerCase()))
.collect(Collectors.toList()); .collect(Collectors.toList());
renderContacts(filtered); renderContacts(filtered);
}); });
// Auto_focus search bar when overlay opens // 3) فوکوس خودکار روی سرچ
Platform.runLater(() -> searchField.requestFocus()); Platform.runLater(() -> searchField.requestFocus());
// Close with footer button // 4) بستن اورلی
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent())); closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
// Close when clicking outside card // 5) اسکرول نرم
overlayBackground.setOnMouseClicked(e -> {
MainController.getInstance().closeOverlay(contactsCard.getParent());
});
// Smooth scroll feel for contacts list
contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
contactsScroll.setPannable(true); contactsScroll.setPannable(true);
contactsScroll.setFitToWidth(true); contactsScroll.setFitToWidth(true);
contactsScroll.setFitToHeight(false); contactsScroll.setFitToHeight(false);
contactsScroll.getContent().setOnScroll(event -> { contactsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003; // smaller = smoother double deltaY = event.getDeltaY() * 0.003;
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY); contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
}); });
// Register scene for ThemeManager → stylesheet swap will handle colors/icons // 6) ثبت صحنه برای ThemeManager
Platform.runLater(() -> { Platform.runLater(() -> {
if (contactsCard.getScene() != null) { if (contactsCard.getScene() != null) {
ThemeManager.getInstance().registerScene(contactsCard.getScene()); ThemeManager.getInstance().registerScene(contactsCard.getScene());
} }
}); });
// Listener for theme change // 7) واکنش به تغییر تم
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> { ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal));
updateSearchIcon(newVal);
});
// Set initial state
updateSearchIcon(ThemeManager.getInstance().isDarkMode()); updateSearchIcon(ThemeManager.getInstance().isDarkMode());
} }
private void renderContacts(List<Contact> 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<ContactEntry> 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.<ContactEntry>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<ContactVM> contacts) {
contactsList.getChildren().clear(); contactsList.getChildren().clear();
if (contacts.isEmpty()) { if (contacts.isEmpty()) {
contactsList.getChildren().clear();
StackPane emptyPane = new StackPane(); StackPane emptyPane = new StackPane();
emptyPane.setPrefHeight(300); // << pushes it lower emptyPane.setPrefHeight(300);
emptyPane.setAlignment(Pos.CENTER); emptyPane.setAlignment(Pos.CENTER);
Label emptyLabel = new Label("No contacts found"); Label emptyLabel = new Label("No contacts found");
emptyLabel.getStyleClass().add("no-contacts-label"); emptyLabel.getStyleClass().add("no-contacts-label");
emptyPane.getChildren().add(emptyLabel); emptyPane.getChildren().add(emptyLabel);
// Do NOT give it prefHeight or Vgrow → prevents scroll bar
contactsList.getChildren().add(emptyPane); contactsList.getChildren().add(emptyPane);
return; return;
} }
for (Contact c : contacts) { for (ContactVM c : contacts) {
HBox item = new HBox(10); HBox item = new HBox(10);
item.getStyleClass().add("contact-item"); item.getStyleClass().add("contact-item");
item.setCursor(Cursor.HAND);
// Avatar // آواتار
ImageView avatar = new ImageView(new Image( ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl));
Objects.requireNonNull(getClass().getResourceAsStream(c.getImageUrl()))
));
avatar.setFitWidth(58); avatar.setFitWidth(58);
avatar.setFitHeight(58); avatar.setFitHeight(58);
avatar.setPreserveRatio(true); avatar.setPreserveRatio(true);
VBox details = new VBox(2); VBox details = new VBox(2);
Label nameLabel = new Label(c.getName()); Label nameLabel = new Label(c.profileName);
nameLabel.getStyleClass().add("contact-name"); 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.getChildren().addAll(avatar, details);
item.setOnMouseClicked(e -> openOrStartPrivateChat(c));
contactsList.getChildren().add(item); 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) { private void updateSearchIcon(boolean darkMode) {
String iconPath = darkMode String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/search_light.png" ? "/org/to/telegramfinalproject/Icons/search_light.png"
@@ -149,19 +206,103 @@ public class ContactsController {
searchIcon.setGraphic(icon); 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) { private void openOrStartPrivateChat(ContactVM contact) {
this.name = name; ChatEntry existing = findExistingPrivateChatWith(contact.contactId);
this.status = status; if (existing != null) {
this.imageUrl = imageUrl; MainController.getInstance().openChat(existing);
MainController.getInstance().closeOverlay(contactsCard.getParent());
return;
} }
public String getName() { return name; }
public String getStatus() { return status; } CompletableFuture
public String getImageUrl() { return imageUrl; } .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;
});
}
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";
}
} }
} }
@@ -40,6 +40,7 @@ public class MainController {
private enum SearchMode { private enum SearchMode {
GLOBAL, GLOBAL,
CHAT CHAT
@@ -429,7 +430,7 @@ public class MainController {
} }
} }
private void openChat(ChatEntry chat) { void openChat(ChatEntry chat) {
try { try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml"));
Node chatPage = loader.load(); Node chatPage = loader.load();
@@ -1149,4 +1150,8 @@ public class MainController {
} }
} }
} }