diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index a4a510a..5ce10d6 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -30,6 +30,11 @@ public class ActionHandler { //use for UI private volatile String lastStatus = "error"; // success | error private volatile String lastMessage = ""; + // ===== UI hook for search results ===== + + + + public String getLastStatus() { return lastStatus; } public String getLastMessage() { return lastMessage; } @@ -37,7 +42,6 @@ public class ActionHandler { - private void handleRealTime(JSONObject json) throws IOException { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); @@ -141,6 +145,18 @@ public class ActionHandler { send(model.toJson()); } + public void searchUI(String keyword) { + + if (Session.currentUser == null || !Session.currentUser.has("user_id")) { + System.out.println("You must be logged in to search."); + return; + } + + String userId = Session.currentUser.getString("user_id"); + SearchRequestModel model = new SearchRequestModel("search", keyword, userId); + send(model.toJson()); + } + public void searchInUsers(){ System.out.println("Enter keyword to search: "); @@ -664,6 +680,8 @@ public class ActionHandler { } break; case "search" : + + JSONArray results = response.getJSONObject("data").getJSONArray("results"); if (results.isEmpty()) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index db68b76..132b5bc 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -14,7 +14,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; import org.to.telegramfinalproject.Models.ChatEntry; - +import org.to.telegramfinalproject.Client.ActionHandler; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; @@ -118,23 +118,41 @@ public class MainController { mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider()); } - // Search in chat field listener - searchBar.textProperty().addListener((obs, oldV, newV) -> { - if (!chatSearchPane.isVisible()) return; // only react if in search mode - if (newV.trim().isEmpty()) { - chatSearchResults.setVisible(false); - chatSearchResults.setManaged(false); - } else { - chatSearchResults.setVisible(true); - chatSearchResults.setManaged(true); - chatSearchResults.getItems().setAll( - "Result 1: " + newV, - "Result 2: " + newV, - "Result 3: " + newV - ); + // Search in chat field listener +// searchBar.textProperty().addListener((obs, oldV, newV) -> { +// if (!chatSearchPane.isVisible()) return; // only react if in search mode +// +// if (newV.trim().isEmpty()) { +// chatSearchResults.setVisible(false); +// chatSearchResults.setManaged(false); +// } else { +// chatSearchResults.setVisible(true); +// chatSearchResults.setManaged(true); +// chatSearchResults.getItems().setAll( +// "Result 1: " + newV, +// "Result 2: " + newV, +// "Result 3: " + newV +// ); +// } +// }); + + // وقتی کاربر Enter زد روی سرچ‌بار، درخواست سرچ بفرست + searchBar.setOnAction(e -> performGlobalSearch(searchBar.getText().trim())); + + // باز/بسته کردن پنل نتایج با تایپ (دلخواه) + searchBar.textProperty().addListener((obs,o,n)->{ + if (n!=null && !n.isBlank()) showSearchPanel(); + }); + + // کلیک روی نتیجه + chatSearchResults.setOnMouseClicked(e -> { + int idx = chatSearchResults.getSelectionModel().getSelectedIndex(); + if (idx >= 0 && idx < searchBacking.size()) { + openSearchResult(searchBacking.get(idx)); } }); + } // Called from ChatPageController when user clicks search button @@ -207,11 +225,31 @@ public class MainController { if (list == null || list.isEmpty()) return; + // جدا کردن Saved از بقیه + ChatEntry saved = null; + java.util.List others = new java.util.ArrayList<>(); for (ChatEntry c : list) { + if (saved == null && isSavedMessages(c)) { + saved = c; + } else { + others.add(c); + } + } + + // اگر “Archived Chats” یا هدر دیگری داری، قبلش اضافه کن (اختیاری) + // addArchivedHeaderIfYouHaveOne(); + + // 1) همیشه Saved اول بیاد (اگر وجود داشت) + if (saved != null) { + addChatNode(saved); + } + + for (ChatEntry c : others) { addChatNode(c); } } + // private void addChatNode(ChatEntry chat ) { // try { // FXMLLoader fx = new FXMLLoader(getClass().getResource( @@ -417,4 +455,307 @@ public class MainController { private void updateLabelsForLightMode() { //myLabel.setStyle("-fx-text-fill: black;"); } + + + + + + // ===== Search UI state ===== + private final java.util.List searchBacking = new java.util.ArrayList<>(); + + private enum SRType { USER, GROUP, CHANNEL, MESSAGE } + + private static class SearchResult { + final SRType type; + final String title; // name / sender_name / ... + final String subtitle; // @"id" / context string / time + final String receiverType; // برای MESSAGE/GROUP/CHANNEL: private|group|channel, برای USER: null + final java.util.UUID uuid; // uuid آن موجودیت (user/group/channel/chat holder) یا receiver_id پیام + final String displayId; // id قابل نمایش (مثل username یا group_id/channel_id) + final String messageId; // فقط برای MESSAGE + final String time; // نمایش + + SearchResult(SRType t, String title, String subtitle, String receiverType, + java.util.UUID uuid, String displayId, String messageId, String time) { + this.type = t; this.title = title; this.subtitle = subtitle; + this.receiverType = receiverType; this.uuid = uuid; + this.displayId = displayId; this.messageId = messageId; this.time = time; + } + + String toDisplay() { + // رشته‌ای که داخل ListView نشان می‌دهیم + switch (type) { + case MESSAGE: + String left = (title == null || title.isBlank()) ? "Message" : title; + String right = (time == null ? "" : (" • " + time)); + return "🗨 " + left + right + (subtitle==null?"":(" — " + subtitle)); + case USER: return "👤 " + title + (subtitle==null?"":(" — " + subtitle)); + case GROUP: return "👥 " + title + (subtitle==null?"":(" — " + subtitle)); + case CHANNEL: return "📣 " + title + (subtitle==null?"":(" — " + subtitle)); + } + return title; + } + } + + + public void performGlobalSearch(String keyword) { + if (keyword == null || keyword.isBlank()) { + chatSearchResults.getItems().clear(); + searchBacking.clear(); + return; + } + if (org.to.telegramfinalproject.Client.Session.currentUser == null || + !org.to.telegramfinalproject.Client.Session.currentUser.has("user_id")) { + System.out.println("You must be logged in to search."); + return; + } + + showSearchPanel(); + + // درخواست به سرور (مثل کنسول) + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "search"); + req.put("keyword", keyword); + req.put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")); + + new Thread(() -> { + org.json.JSONObject resp; + try { + resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + } catch (Exception ex) { + ex.printStackTrace(); + return; + } + if (resp == null || !"success".equals(resp.optString("status"))) return; + + org.json.JSONArray arr = resp.optJSONObject("data").optJSONArray("results"); + if (arr == null) arr = new org.json.JSONArray(); + + final java.util.List tmp = new java.util.ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject it = arr.getJSONObject(i); + String type = it.optString("type",""); + + switch (type) { + case "user": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.USER, name, + id.isBlank()? null : "@"+id, + null, uuid, id, null, null + )); + break; + } + case "group": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown group"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.GROUP, name, + id.isBlank()? null : id, + "group", uuid, id, null, null + )); + break; + } + case "channel": { + java.util.UUID uuid = java.util.UUID.fromString(it.getString("uuid")); + String name = it.optString("name","Unknown channel"); + String id = it.optString("id", ""); + tmp.add(new SearchResult( + SRType.CHANNEL, name, + id.isBlank()? null : id, + "channel", uuid, id, null, null + )); + break; + } + case "message": { + String senderName = it.optString("sender_name", it.optString("sender","Unknown")); + String time = it.optString("time", ""); + String content = it.optString("content","[No content]"); + String rType = it.optString("receiver_type",""); // private|group|channel + java.util.UUID rUuid = java.util.UUID.fromString(it.getString("receiver_id")); + String ctx = ""; + if ("group".equals(rType)) ctx = it.optString("group_name",""); + if ("channel".equals(rType)) ctx = it.optString("channel_name",""); + String subtitle = (ctx.isBlank()? "" : ctx) + (content.isBlank()? "" : (subtitleSep(ctx)+content)); + tmp.add(new SearchResult( + SRType.MESSAGE, senderName, subtitle, rType, rUuid, + it.optString("receiver_display_id", ""), // اگر داشتی + it.optString("message_id", null), + time + )); + break; + } + } + } + + Platform.runLater(() -> renderSearchResults(tmp)); + }).start(); + } + + private String subtitleSep(String s){ return s==null || s.isBlank()? "" : " — "; } + + private void renderSearchResults(java.util.List results) { + searchBacking.clear(); + searchBacking.addAll(results); + + javafx.collections.ObservableList view = javafx.collections.FXCollections.observableArrayList(); + for (SearchResult r : results) view.add(r.toDisplay()); + chatSearchResults.setItems(view); + chatSearchResults.setVisible(true); + chatSearchResults.setManaged(true); + } + + + private void openSearchResult(SearchResult r) { + switch (r.type) { + case USER: { + // مثل کنسول: اگر چت private با این یوزر داریم بازش کن؛ + // وگرنه chat_id را از سرور بگیر/بساز، بعد باز کن. + java.util.UUID chatId = findExistingPrivateChatId(r.uuid); + if (chatId == null) { + chatId = fetchOrCreatePrivateChat(r.uuid); // نیاز به API سمت سرور + if (chatId == null) { + System.out.println("❌ Failed to create/find private chat."); + return; + } + } + + org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); + ce.setId(chatId.toString()); // ⬅️ internal chat_id + ce.setDisplayId(r.displayId); // username + ce.setName(r.title); // profile_name + ce.setType("private"); + + openChat(ce); // همون متد فعلی MainController + break; + } + + case GROUP: + case CHANNEL: { + // اگر تو لیست هست بازش کن، وگرنه با همون uuid باز کن + org.to.telegramfinalproject.Models.ChatEntry existing = + findExistingChat(r.uuid, r.receiverType); + if (existing != null) { + openChat(existing); + } else { + org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); + ce.setId(r.uuid.toString()); // internal_uuid group/channel + ce.setDisplayId(r.displayId); // group_id/channel_id + ce.setName(r.title); + ce.setType(r.receiverType); + openChat(ce); + } + break; + } + + case MESSAGE: { + // مثل کنسول: چتِ پیام را باز کن (اسکرول به پیام را بعداً اضافه کن) + org.to.telegramfinalproject.Models.ChatEntry existing = + findExistingChat(r.uuid, r.receiverType); + if (existing != null) { + openChat(existing); + } else { + org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); + ce.setId(r.uuid.toString()); // receiver internal_uuid + ce.setType(r.receiverType); + ce.setName(guessNameForReceiver(r)); // اگر خواستی از subtitle استفاده کن + ce.setDisplayId(r.displayId); + openChat(ce); + } + break; + } + } + } + + private org.to.telegramfinalproject.Models.ChatEntry findExistingChat(java.util.UUID internalId, String type) { + var list = (org.to.telegramfinalproject.Client.Session.chatList==null) + ? java.util.Collections.emptyList() + : org.to.telegramfinalproject.Client.Session.chatList; + for (var c : list) { + if (internalId.toString().equals(c.getId().toString()) + && type.equalsIgnoreCase(c.getType())) { + return c; + } + } + return null; + } + + private java.util.UUID findExistingPrivateChatId(java.util.UUID otherUserUuid) { + // اگر در لیست چت‌ها private با همین طرف داری و internal_id همان chat_id است، برش گردان + var list = (org.to.telegramfinalproject.Client.Session.chatList==null) + ? java.util.Collections.emptyList() + : org.to.telegramfinalproject.Client.Session.chatList; + + for (var c : list) { + if ("private".equalsIgnoreCase(c.getType())) { + // اگر مدل‌ات otherUserId در ChatEntry دارد، از آن استفاده کن + if (otherUserUuid.equals(c.getOtherUserId())) { + try { return java.util.UUID.fromString(c.getId().toString()); } catch (Exception ignored) {} + } + } + } + return null; + } + + private java.util.UUID fetchOrCreatePrivateChat(java.util.UUID otherUserUuid) { + try { + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "get_or_create_private_chat"); + req.put("user1", org.to.telegramfinalproject.Client.Session.currentUser.getString("internal_uuid")); + req.put("user2", otherUserUuid.toString()); + org.json.JSONObject resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + if (resp != null && "success".equals(resp.optString("status"))) { + String chatId = resp.getJSONObject("data").getString("chat_id"); + return java.util.UUID.fromString(chatId); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private String guessNameForReceiver(SearchResult r) { + // فقط برای زمانی که موجودیت در لیست نبود + if (r.title != null && !r.title.isBlank()) return r.title; + if (r.receiverType != null) { + switch (r.receiverType) { + case "group": return "Group"; + case "channel": return "Channel"; + case "private": return "Private Chat"; + } + } + return "Chat"; + } + + private boolean isSavedMessages(ChatEntry c) { + if (c == null) return false; + + // اگر تایپ اختصاصی داری + if ("saved".equalsIgnoreCase(c.getType())) return true; + + // اگر با نام مشخص ذخیره می‌کنی + String n = c.getName(); + if (n != null && n.equalsIgnoreCase("Saved Messages")) return true; + + // حالت پرایوت با خودِ کاربر + String me = (org.to.telegramfinalproject.Client.Session.currentUser != null) + ? org.to.telegramfinalproject.Client.Session.currentUser.optString("internal_uuid", "") + : ""; + try { + UUID other = c.getOtherUserId(); // اگر این فیلد را داری + if ("private".equalsIgnoreCase(c.getType()) && + other != null && other.toString().equalsIgnoreCase(me)) { + return true; + } + } catch (Exception ignore) {} + + return false; + } + + + + } \ No newline at end of file