1161 lines
44 KiB
Java
1161 lines
44 KiB
Java
package org.to.telegramfinalproject.UI;
|
|
|
|
import javafx.animation.PauseTransition;
|
|
import javafx.animation.TranslateTransition;
|
|
import javafx.application.Platform;
|
|
import javafx.fxml.FXML;
|
|
import javafx.fxml.FXMLLoader;
|
|
import javafx.geometry.Pos;
|
|
import javafx.scene.Node;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.image.Image;
|
|
import javafx.scene.image.ImageView;
|
|
import javafx.scene.layout.AnchorPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.Pane;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.scene.shape.Circle;
|
|
import javafx.util.Duration;
|
|
import org.to.telegramfinalproject.Client.Session;
|
|
import org.to.telegramfinalproject.Models.ChatEntry;
|
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
|
import java.io.IOException;
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
|
|
public class MainController {
|
|
|
|
// === LEFT PANE ===
|
|
@FXML private VBox chatListContainer; // inside the scrollPane
|
|
@FXML private ScrollPane scrollPane; // chat list scroll
|
|
|
|
// === Global Search ===
|
|
@FXML private TextField searchBar;
|
|
@FXML private VBox globalSearchPane;
|
|
@FXML private VBox noResultsBox;
|
|
@FXML private ImageView noResultIcon;
|
|
@FXML private ScrollPane globalSearchScroll;
|
|
@FXML private VBox globalSearchResultsContainer;
|
|
|
|
private boolean blockedByMeFlag = false;
|
|
private boolean blockedMeFlag = false;
|
|
|
|
|
|
|
|
|
|
private enum SearchMode {
|
|
GLOBAL,
|
|
CHAT
|
|
}
|
|
private ChatViewMode currentMode = ChatViewMode.NORMAL; // حالت فعلی: NORMAL/NEEDS_JOIN/NEEDS_ADD_CONTACT
|
|
private SearchMode currentSearchMode = SearchMode.GLOBAL;
|
|
private UUID currentChatId; // if in CHAT mode, which chat to search in
|
|
|
|
// === Search In Chat ===
|
|
@FXML private VBox chatSearchPane; // search results panel
|
|
@FXML private ListView<String> chatSearchResults;
|
|
|
|
// === RIGHT PANE ===
|
|
@FXML private VBox leftPane;
|
|
@FXML private javafx.scene.layout.StackPane chatDisplayArea;
|
|
@FXML private Label placeholderLabel;
|
|
|
|
// === Main root ===
|
|
@FXML private StackPane mainRoot;
|
|
@FXML private SplitPane mainSplitPane;
|
|
|
|
// === Sidebar ===
|
|
@FXML private Pane overlay;
|
|
@FXML private ImageView menuIcon;
|
|
private SidebarMenuController sidebarController; //save sidebar controller
|
|
|
|
// === Overlay ===
|
|
@FXML private StackPane overlayLayer;
|
|
|
|
// === Time formatter for messages ===
|
|
private static final java.time.format.DateTimeFormatter FMT_HHMM =
|
|
java.time.format.DateTimeFormatter.ofPattern("HH:mm");
|
|
private static final java.time.format.DateTimeFormatter FMT_DATE_TIME =
|
|
java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
|
|
private static final String YESTERDAY_LABEL = "Yesterday";
|
|
|
|
// === STATE ===
|
|
private static MainController instance;
|
|
private Node sidebarRoot;
|
|
private boolean isSidebarOpen = false;
|
|
|
|
// Add ThemeManager handle
|
|
private final ThemeManager themeManager = ThemeManager.getInstance();
|
|
|
|
public MainController() {
|
|
instance = this;
|
|
}
|
|
|
|
public static MainController getInstance() {
|
|
return instance;
|
|
}
|
|
|
|
// Keep track of the scene user comes from
|
|
private final Deque<Node> navigationStack = new ArrayDeque<>();
|
|
private final Map<UUID, ChatItemController> itemControllers = new HashMap<>();
|
|
|
|
|
|
//For realtime handling
|
|
public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs,
|
|
boolean isIncoming, String lastPreview) {
|
|
|
|
boolean exists =
|
|
(Session.chatList != null && Session.chatList.stream().anyMatch(c -> chatId.equals(c.getId()))) ||
|
|
(Session.activeChats != null && Session.activeChats.stream().anyMatch(c -> chatId.equals(c.getId()))) ||
|
|
(Session.archivedChats != null && Session.archivedChats.stream().anyMatch(c -> chatId.equals(c.getId())));
|
|
if (!exists) return;
|
|
|
|
boolean isOpen = Session.currentChatId != null &&
|
|
Session.currentChatId.equals(chatId.toString());
|
|
|
|
forEachChat(chatId, chat -> {
|
|
chat.setLastMessageTime(lastTs);
|
|
if (lastPreview != null) chat.setLastMessagePreview(lastPreview);
|
|
if (isIncoming && !isOpen) chat.setUnreadCount(chat.getUnreadCount() + 1); // ✅ یکدست با unreadCount
|
|
});
|
|
|
|
java.util.Comparator<ChatEntry> byTimeDesc = (c1, c2) -> {
|
|
var t1 = c1.getLastMessageTime();
|
|
var t2 = c2.getLastMessageTime();
|
|
if (t1 == null && t2 == null) return 0;
|
|
if (t1 == null) return 1;
|
|
if (t2 == null) return -1;
|
|
return t2.compareTo(t1);
|
|
};
|
|
if (Session.chatList != null) Session.chatList.sort(byTimeDesc);
|
|
if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc);
|
|
if (Session.archivedChats != null) Session.archivedChats.sort(byTimeDesc);
|
|
|
|
refreshChatListUI();
|
|
refreshBadgesIfAny();
|
|
}
|
|
|
|
|
|
|
|
|
|
// MainController
|
|
private void forEachChat(UUID chatId, java.util.function.Consumer<ChatEntry> fn) {
|
|
java.util.List<java.util.List<ChatEntry>> lists = java.util.List.of(
|
|
Session.chatList != null ? Session.chatList : java.util.List.of(),
|
|
Session.activeChats != null ? Session.activeChats : java.util.List.of(),
|
|
Session.archivedChats != null ? Session.archivedChats : java.util.List.of()
|
|
);
|
|
for (var lst : lists) {
|
|
for (var c : lst) {
|
|
if (chatId.equals(c.getId())) {
|
|
fn.accept(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void refreshChatListUI() {
|
|
Platform.runLater(() -> {
|
|
chatListContainer.getChildren().clear();
|
|
itemControllers.clear();
|
|
populateChatListFromSession(); // همون متدی که نودها رو میسازه و میچسبونه
|
|
});
|
|
}
|
|
|
|
|
|
private void refreshBadgesIfAny() {
|
|
// آپدیت نشانها اگر لازم است
|
|
}
|
|
|
|
|
|
@FXML
|
|
public void initialize() {
|
|
// Populate chat list
|
|
populateChatListFromSession();
|
|
|
|
// Register the scene for automatic CSS updates
|
|
Platform.runLater(() -> {
|
|
ThemeManager.getInstance().registerScene(mainRoot.getScene());
|
|
});
|
|
|
|
// Listen for theme changes to update icons & labels manually
|
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
|
|
if (newVal) {
|
|
updateIconsForDarkMode();
|
|
updateLabelsForDarkMode();
|
|
} else {
|
|
updateIconsForLightMode();
|
|
updateLabelsForLightMode();
|
|
}
|
|
});
|
|
|
|
// Smooth scroll feel for chat list
|
|
scrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
|
|
scrollPane.setPannable(true);
|
|
scrollPane.setFitToWidth(true);
|
|
scrollPane.setFitToHeight(false);
|
|
scrollPane.getContent().setOnScroll(event -> {
|
|
double deltaY = event.getDeltaY() * 0.003; // smaller = smoother
|
|
scrollPane.setVvalue(scrollPane.getVvalue() - deltaY);
|
|
});
|
|
|
|
// ---------- SplitPane limits so panes can't fully collapse ----------
|
|
leftPane.setMinWidth(72); // avatars-only collapse
|
|
chatDisplayArea.setMinWidth(360); // chat area never fully collapses
|
|
|
|
mainSplitPane.widthProperty().addListener((o, ov, nv) -> clampDivider());
|
|
if (!mainSplitPane.getDividers().isEmpty()) {
|
|
mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider());
|
|
}
|
|
|
|
// Search bar enter action
|
|
searchBar.setOnAction(e -> {
|
|
String keyword = searchBar.getText().trim();
|
|
if (keyword.isEmpty()) return;
|
|
|
|
if (currentSearchMode == SearchMode.GLOBAL) {
|
|
performGlobalSearch(keyword);
|
|
} else if (currentSearchMode == SearchMode.CHAT && currentChatId != null) {
|
|
performChatSearch(keyword, currentChatId);
|
|
}
|
|
});
|
|
|
|
// Typing in search bar shows global search panel
|
|
searchBar.textProperty().addListener((obs, o, n) -> {
|
|
if (n != null && !n.isBlank()) {
|
|
if (currentSearchMode == SearchMode.GLOBAL) {
|
|
showGlobalSearchPanel();
|
|
} else if (currentSearchMode == SearchMode.CHAT) {
|
|
showSearchPanel();
|
|
}
|
|
}
|
|
});
|
|
|
|
chatSearchResults.setOnMouseClicked(e -> {
|
|
int idx = chatSearchResults.getSelectionModel().getSelectedIndex();
|
|
if (idx >= 0 && idx < searchBacking.size()) {
|
|
openSearchResult(searchBacking.get(idx));
|
|
}
|
|
});
|
|
|
|
// Smooth scroll feel for global search results
|
|
globalSearchScroll.getStylesheets().add(
|
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
|
);
|
|
globalSearchScroll.setPannable(true);
|
|
globalSearchScroll.setFitToWidth(true);
|
|
globalSearchScroll.setFitToHeight(false);
|
|
globalSearchScroll.getContent().setOnScroll(event -> {
|
|
double deltaY = event.getDeltaY() * 0.003; // match chat list smoothness
|
|
globalSearchScroll.setVvalue(globalSearchScroll.getVvalue() - deltaY);
|
|
});
|
|
}
|
|
|
|
// Called from ChatPageController when user clicks search button
|
|
public void showSearchPanel() {
|
|
scrollPane.setVisible(false);
|
|
scrollPane.setManaged(false);
|
|
|
|
currentSearchMode = SearchMode.CHAT;
|
|
|
|
chatSearchPane.setVisible(true);
|
|
chatSearchPane.setManaged(true);
|
|
|
|
searchBar.requestFocus();
|
|
}
|
|
|
|
@FXML
|
|
public void closeSearchPanel() {
|
|
chatSearchPane.setVisible(false);
|
|
chatSearchPane.setManaged(false);
|
|
|
|
currentSearchMode = SearchMode.GLOBAL;
|
|
currentChatId = null;
|
|
|
|
scrollPane.setVisible(true);
|
|
scrollPane.setManaged(true);
|
|
|
|
searchBar.clear();
|
|
}
|
|
|
|
public void showGlobalSearchPanel() {
|
|
scrollPane.setVisible(false);
|
|
scrollPane.setManaged(false);
|
|
|
|
chatSearchPane.setVisible(false);
|
|
chatSearchPane.setManaged(false);
|
|
|
|
globalSearchPane.setVisible(true);
|
|
globalSearchPane.setManaged(true);
|
|
}
|
|
|
|
@FXML
|
|
public void closeGlobalSearch() {
|
|
globalSearchPane.setVisible(false);
|
|
globalSearchPane.setManaged(false);
|
|
|
|
scrollPane.setVisible(true);
|
|
scrollPane.setManaged(true);
|
|
|
|
searchBar.clear();
|
|
}
|
|
|
|
private String formatChatTime(java.time.LocalDateTime ts) {
|
|
if (ts == null) return "";
|
|
var today = java.time.LocalDate.now();
|
|
var d = ts.toLocalDate();
|
|
|
|
if (d.isEqual(today)) {
|
|
//today
|
|
return FMT_HHMM.format(ts);
|
|
//yesterday
|
|
} else if (d.isEqual(today.minusDays(1))) {
|
|
return YESTERDAY_LABEL;
|
|
} else {
|
|
return FMT_DATE_TIME.format(ts);
|
|
}
|
|
}
|
|
|
|
private void populateChatListFromSession() {
|
|
chatListContainer.getChildren().clear();
|
|
|
|
var list = (org.to.telegramfinalproject.Client.Session.activeChats != null
|
|
&& !org.to.telegramfinalproject.Client.Session.activeChats.isEmpty())
|
|
? org.to.telegramfinalproject.Client.Session.activeChats
|
|
: org.to.telegramfinalproject.Client.Session.chatList;
|
|
|
|
if (list == null || list.isEmpty()) return;
|
|
|
|
ChatEntry saved = null;
|
|
java.util.List<ChatEntry> 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(
|
|
// "/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
|
// Node item = fx.load();
|
|
// ChatItemController cc = fx.getController();
|
|
//
|
|
// String lastPreview = "";
|
|
// String time = (chat.getLastMessageTime() != null)
|
|
// ? timeFmt.format(chat.getLastMessageTime()) : "";
|
|
//
|
|
// cc.setChatData(chat.getName(), lastPreview, time, 0);
|
|
//
|
|
// item.setOnMouseClicked(e -> openChat(String.valueOf(chat)));
|
|
// chatListContainer.getChildren().add(item);
|
|
// } catch (Exception e) {
|
|
// e.printStackTrace();
|
|
// }
|
|
// }
|
|
|
|
// private void addChatNode(ChatEntry chat) {
|
|
// try {
|
|
// FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
|
// Node item = fx.load();
|
|
// ChatItemController cc = fx.getController();
|
|
//
|
|
// String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
|
|
// String timeText = formatChatTime(chat.getLastMessageTime());
|
|
// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
|
// Node chatItem = loader.load();
|
|
// ChatItemController controller = loader.getController();
|
|
//
|
|
// cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png");
|
|
// item.setOnMouseClicked(e -> openChat(chat));
|
|
// chatListContainer.getChildren().add(item);
|
|
//
|
|
// itemControllers.put(chat.getId(), cc);
|
|
//
|
|
// } catch (Exception ex) {
|
|
// ex.printStackTrace();
|
|
// }
|
|
// }
|
|
|
|
|
|
private void addChatNode(ChatEntry chat) {
|
|
try {
|
|
FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
|
Node item = fx.load();
|
|
ChatItemController cc = fx.getController();
|
|
|
|
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
|
|
|
|
String timeText = chat.getLastMessageTime() == null
|
|
? ""
|
|
: formatChatTime(chat.getLastMessageTime());
|
|
|
|
// If chat has a profile picture, pass it; otherwise null
|
|
String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty())
|
|
? chat.getImageUrl()
|
|
: null;
|
|
|
|
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType());
|
|
item.setOnMouseClicked(e -> openChat(chat));
|
|
chatListContainer.getChildren().add(item);
|
|
|
|
itemControllers.put(chat.getId(), cc);
|
|
} catch (Exception ex) {
|
|
ex.printStackTrace();
|
|
}
|
|
}
|
|
|
|
private String mapTypeToLabel(String t) {
|
|
switch (t.toUpperCase()) {
|
|
case "IMAGE": return "[Image]";
|
|
case "AUDIO": return "[Audio]";
|
|
case "VIDEO": return "[Video]";
|
|
case "FILE": return "[File]";
|
|
default: return "[Message]";
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
|
|
private void openChatWithMode(ChatEntry chat, ChatViewMode mode) {
|
|
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, mode); // ⬅️ متد جدید در ChatPageController
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
|
|
@FXML
|
|
private void toggleSidebar() {
|
|
if (isSidebarOpen) {
|
|
closeSidebar();
|
|
} else {
|
|
openSidebar();
|
|
}
|
|
}
|
|
|
|
// @FXML
|
|
// private void openSidebar() {
|
|
// try {
|
|
// if (sidebarRoot == null) {
|
|
// sidebarRoot = FXMLLoader.load(getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml"));
|
|
// StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT);
|
|
// sidebarRoot.setTranslateX(-getSidebarWidth());
|
|
// mainRoot.getChildren().add(sidebarRoot);
|
|
// }
|
|
//
|
|
// TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot);
|
|
// slideIn.setToX(0);
|
|
// slideIn.play();
|
|
//
|
|
// overlay.setVisible(true);
|
|
// overlay.setMouseTransparent(false);
|
|
//
|
|
// isSidebarOpen = true;
|
|
//
|
|
// } catch (IOException e) {
|
|
// e.printStackTrace();
|
|
// }
|
|
// }
|
|
|
|
@FXML
|
|
private void openSidebar() {
|
|
try {
|
|
if (sidebarRoot == null) {
|
|
FXMLLoader loader = new FXMLLoader(
|
|
getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml"));
|
|
sidebarRoot = loader.load();
|
|
sidebarController = loader.getController();
|
|
|
|
StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT);
|
|
sidebarRoot.setTranslateX(-getSidebarWidth());
|
|
mainRoot.getChildren().add(sidebarRoot);
|
|
}
|
|
|
|
if (sidebarController != null && org.to.telegramfinalproject.Client.Session.currentUser != null) {
|
|
sidebarController.setUserFromSession(
|
|
org.to.telegramfinalproject.Client.Session.currentUser);
|
|
}
|
|
|
|
TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot);
|
|
slideIn.setToX(0);
|
|
slideIn.play();
|
|
|
|
overlay.setVisible(true);
|
|
overlay.setMouseTransparent(false);
|
|
isSidebarOpen = true;
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
private void closeSidebar() {
|
|
if (sidebarRoot != null && isSidebarOpen) {
|
|
TranslateTransition slideOut = new TranslateTransition(Duration.millis(250), sidebarRoot);
|
|
slideOut.setToX(-getSidebarWidth());
|
|
slideOut.setOnFinished(e -> {
|
|
overlay.setVisible(false);
|
|
overlay.setMouseTransparent(true);
|
|
isSidebarOpen = false;
|
|
});
|
|
slideOut.play();
|
|
}
|
|
}
|
|
|
|
private double getSidebarWidth() {
|
|
return 250; // Sidebar width in px
|
|
}
|
|
|
|
private void clampDivider() {
|
|
if (mainSplitPane.getDividers().isEmpty()) return;
|
|
|
|
double totalW = mainSplitPane.getWidth();
|
|
if (totalW <= 0) return;
|
|
|
|
double leftMinRatio = leftPane.getMinWidth() / totalW;
|
|
double rightMinRatio = chatDisplayArea.getMinWidth() / totalW;
|
|
double maxLeftRatio = 1.0 - rightMinRatio;
|
|
|
|
SplitPane.Divider d = mainSplitPane.getDividers().get(0);
|
|
double p = d.getPosition();
|
|
if (p < leftMinRatio) {
|
|
d.setPosition(leftMinRatio);
|
|
} else if (p > maxLeftRatio) {
|
|
d.setPosition(maxLeftRatio);
|
|
}
|
|
}
|
|
|
|
private void updateIconsForDarkMode() {
|
|
// Example: switch images to white versions
|
|
menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_light.png")));
|
|
noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_light.png")));
|
|
}
|
|
|
|
private void updateIconsForLightMode() {
|
|
// Example: switch images to black versions
|
|
menuIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/menu_dark.png")));
|
|
noResultIcon.setImage(new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/no_result_dark.png")));
|
|
}
|
|
|
|
private void updateLabelsForDarkMode() {
|
|
//myLabel.setStyle("-fx-text-fill: white;");
|
|
}
|
|
|
|
private void updateLabelsForLightMode() {
|
|
//myLabel.setStyle("-fx-text-fill: black;");
|
|
}
|
|
|
|
// === Overlay Handling ===
|
|
public void showOverlay(Node overlayNode) {
|
|
if (!overlayLayer.getChildren().isEmpty()) {
|
|
Node current = overlayLayer.getChildren().get(overlayLayer.getChildren().size() - 1);
|
|
navigationStack.push(current);
|
|
overlayLayer.getChildren().remove(current);
|
|
}
|
|
overlayLayer.getChildren().add(overlayNode);
|
|
|
|
// Automatically close sidebar
|
|
closeSidebar();
|
|
}
|
|
|
|
public void closeOverlay(Node overlayNode) {
|
|
overlayLayer.getChildren().remove(overlayNode);
|
|
}
|
|
|
|
public void goBack(Pane currentOverlay) {
|
|
// Remove the current overlay
|
|
closeOverlay(currentOverlay);
|
|
|
|
if (!navigationStack.isEmpty()) {
|
|
Node previous = navigationStack.pop();
|
|
|
|
// Clear existing overlays before showing previous
|
|
overlayLayer.getChildren().clear();
|
|
overlayLayer.getChildren().add(previous);
|
|
}
|
|
}
|
|
|
|
// ===== Search UI state =====
|
|
private final java.util.List<SearchResult> searchBacking = new java.util.ArrayList<>();
|
|
|
|
@FXML private AnchorPane chatHost; // کانتینر مناسب در مین برای قرار دادن ChatPage
|
|
|
|
private ChatPageController chatPageController;
|
|
|
|
|
|
|
|
public ChatPageController getChatPageController() {
|
|
return chatPageController;
|
|
}
|
|
|
|
|
|
private enum SRType { USER, GROUP, CHANNEL, MESSAGE }
|
|
|
|
// Inner class
|
|
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() {
|
|
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;
|
|
}
|
|
}
|
|
|
|
private void performChatSearch(String keyword, UUID chatId) {
|
|
if (keyword == null || keyword.isBlank()) {
|
|
chatSearchResults.getItems().clear();
|
|
return;
|
|
}
|
|
|
|
showSearchPanel();
|
|
|
|
org.json.JSONObject req = new org.json.JSONObject();
|
|
req.put("action", "search_in_chat");
|
|
req.put("keyword", keyword);
|
|
req.put("chat_id", chatId.toString());
|
|
req.put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id"));
|
|
|
|
new Thread(() -> {
|
|
org.json.JSONObject resp;
|
|
try {
|
|
resp = 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();
|
|
|
|
java.util.List<String> tmp = new java.util.ArrayList<>();
|
|
for (int i = 0; i < arr.length(); i++) {
|
|
org.json.JSONObject it = arr.getJSONObject(i);
|
|
String time = it.optString("time", "");
|
|
String content = it.optString("content", "[No content]");
|
|
tmp.add("🗨 " + content + (time.isEmpty() ? "" : " • " + time));
|
|
}
|
|
|
|
Platform.runLater(() -> {
|
|
chatSearchResults.getItems().setAll(tmp);
|
|
chatSearchResults.setVisible(true);
|
|
chatSearchResults.setManaged(true);
|
|
});
|
|
}).start();
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
showGlobalSearchPanel();
|
|
|
|
// Request to server
|
|
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<SearchResult> 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<SearchResult> results) {
|
|
// searchBacking.clear();
|
|
// searchBacking.addAll(results);
|
|
//
|
|
// javafx.collections.ObservableList<SearchResult> view =
|
|
// javafx.collections.FXCollections.observableArrayList(results);
|
|
// globalSearchResults.setItems(view);
|
|
// globalSearchResults.setVisible(true);
|
|
// globalSearchResults.setManaged(true);
|
|
// }
|
|
|
|
private void renderSearchResults(List<SearchResult> results) {
|
|
globalSearchResultsContainer.getChildren().clear();
|
|
|
|
if (results.isEmpty()) {
|
|
noResultsBox.setVisible(true);
|
|
noResultsBox.setManaged(true);
|
|
globalSearchScroll.setVisible(false);
|
|
globalSearchScroll.setManaged(false);
|
|
return;
|
|
}
|
|
|
|
noResultsBox.setVisible(false);
|
|
noResultsBox.setManaged(false);
|
|
globalSearchScroll.setVisible(true);
|
|
globalSearchScroll.setManaged(true);
|
|
|
|
for (SearchResult r : results) {
|
|
HBox container = new HBox(10);
|
|
ImageView avatar = new ImageView();
|
|
avatar.setFitWidth(40);
|
|
avatar.setFitHeight(40);
|
|
avatar.setSmooth(true);
|
|
avatar.setPreserveRatio(true);
|
|
|
|
// 🔑 Clip it into a circle
|
|
Circle clip = new Circle(20, 20, 20); // x,y = center, radius = 20
|
|
avatar.setClip(clip);
|
|
|
|
Label title = new Label(r.title);
|
|
title.getStyleClass().add("global-search-title");
|
|
|
|
Label subtitle = new Label(
|
|
r.type == SRType.MESSAGE
|
|
? (r.subtitle != null ? r.subtitle : "")
|
|
: "Press to see messages"
|
|
);
|
|
subtitle.getStyleClass().add("global-search-subtitle");
|
|
|
|
VBox texts = new VBox(2, title, subtitle);
|
|
container.getChildren().addAll(avatar, texts);
|
|
|
|
// Default profile per type
|
|
switch (r.type) {
|
|
case USER -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
|
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))));
|
|
case GROUP -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
|
|
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))));
|
|
case CHANNEL -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
|
|
"/org/to/telegramfinalproject/Avatars/default_channel_profile.png"))));
|
|
case MESSAGE -> avatar.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream(
|
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))));
|
|
}
|
|
|
|
container.setOnMouseClicked(e -> openSearchResult(r));
|
|
globalSearchResultsContainer.getChildren().add(container);
|
|
}
|
|
}
|
|
|
|
private void openSearchResult(SearchResult r) {
|
|
switch (r.type) {
|
|
case USER: {
|
|
// 1) اگه قبلاً چت پرایوت با این یوزر داری، از همون استفاده کن
|
|
UUID existingChatId = findExistingPrivateChatId(r.uuid);
|
|
|
|
ChatEntry ce = new ChatEntry();
|
|
ce.setType("private");
|
|
ce.setName(r.title);
|
|
ce.setDisplayId(r.displayId);
|
|
try { ce.setOtherUserId(r.uuid); } catch (Exception ignore) {}
|
|
|
|
ChatViewMode mode;
|
|
if (existingChatId != null) {
|
|
ce.setId(existingChatId.toString());
|
|
mode = ChatViewMode.NORMAL;
|
|
} else {
|
|
// هنوز چتی وجود ندارد → Preview (بدون ساخت چت)
|
|
// برای Preview از uuid خودِ طرف مقابل بهعنوان id موقت استفاده میکنیم
|
|
ce.setId(r.uuid.toString());
|
|
mode = isContact(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_ADD_CONTACT;
|
|
}
|
|
|
|
openChatWithMode(ce, mode);
|
|
break;
|
|
}
|
|
|
|
|
|
case GROUP:
|
|
case CHANNEL: {
|
|
org.to.telegramfinalproject.Models.ChatEntry existing =
|
|
findExistingChat(r.uuid, r.receiverType);
|
|
if (existing != null) {
|
|
openChatWithMode(existing, ChatViewMode.NORMAL);
|
|
} 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);
|
|
ChatViewMode mode;
|
|
try {
|
|
mode = isInAnyChatList(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_JOIN;
|
|
} catch (Exception e) {
|
|
mode = ChatViewMode.NEEDS_JOIN;
|
|
}
|
|
openChatWithMode(ce, mode);
|
|
|
|
}
|
|
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));
|
|
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.<org.to.telegramfinalproject.Models.ChatEntry>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) {
|
|
var list = (org.to.telegramfinalproject.Client.Session.chatList==null)
|
|
? java.util.Collections.<org.to.telegramfinalproject.Models.ChatEntry>emptyList()
|
|
: org.to.telegramfinalproject.Client.Session.chatList;
|
|
|
|
for (var c : list) {
|
|
if ("private".equalsIgnoreCase(c.getType())) {
|
|
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;
|
|
}
|
|
|
|
|
|
//Search
|
|
private boolean isInAnyChatList(UUID chatId) {
|
|
var lists = List.of(
|
|
Session.chatList != null ? Session.chatList : List.<ChatEntry>of(),
|
|
Session.activeChats != null ? Session.activeChats : List.<ChatEntry>of(),
|
|
Session.archivedChats != null ? Session.archivedChats : List.<ChatEntry>of()
|
|
);
|
|
for (var lst : lists) {
|
|
for (var c : lst) {
|
|
try {
|
|
if (chatId.equals(UUID.fromString(c.getId().toString()))) return true;
|
|
} catch (Exception ignore) {}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
private boolean isContact(UUID userUuid) {
|
|
if (Session.contactEntries == null) return false;
|
|
try {
|
|
for (var c : Session.contactEntries) {
|
|
UUID id = c.getContactId();
|
|
if (userUuid.equals(id)) return true;
|
|
}
|
|
} catch (Exception ignore) {}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
private ChatViewMode computeMode(ChatEntry ce) {
|
|
try {
|
|
UUID id = UUID.fromString(ce.getId().toString());
|
|
if (isInAnyChatList(id)) return ChatViewMode.NORMAL;
|
|
} catch (Exception ignore) {}
|
|
|
|
String t = ce.getType();
|
|
if ("group".equalsIgnoreCase(t) || "channel".equalsIgnoreCase(t)) {
|
|
return ChatViewMode.NEEDS_JOIN;
|
|
}
|
|
if ("private".equalsIgnoreCase(t)) {
|
|
UUID other = null;
|
|
try { other = ce.getOtherUserId(); } catch (Exception ignore) {}
|
|
return (other != null && isContact(other))
|
|
? ChatViewMode.NORMAL
|
|
: ChatViewMode.NEEDS_ADD_CONTACT;
|
|
}
|
|
return ChatViewMode.NORMAL;
|
|
}
|
|
|
|
public void onJoinedOrAdded(ChatEntry ce) {
|
|
try {
|
|
UUID id = UUID.fromString(ce.getId().toString());
|
|
if (!isInAnyChatList(id)) {
|
|
if (Session.chatList == null) Session.chatList = new ArrayList<>();
|
|
Session.chatList.add(ce);
|
|
}
|
|
// اگر activeChats استفاده میکنی:
|
|
if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) {
|
|
Session.activeChats.add(ce);
|
|
}
|
|
} catch (Exception ignore) {}
|
|
|
|
// (اختیاری) مرتبسازی بر اساس زمان آخرین پیام
|
|
Comparator<ChatEntry> byTimeDesc = (a,b) -> {
|
|
LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime();
|
|
if (t1 == null && t2 == null) return 0;
|
|
if (t1 == null) return 1;
|
|
if (t2 == null) return -1;
|
|
return t2.compareTo(t1);
|
|
};
|
|
if (Session.chatList != null) Session.chatList.sort(byTimeDesc);
|
|
if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc);
|
|
|
|
refreshChatListUI();
|
|
}
|
|
|
|
|
|
public void addChatAndSelect(org.to.telegramfinalproject.Models.ChatEntry entry) {
|
|
// در Session نگهداری
|
|
if (org.to.telegramfinalproject.Client.Session.chatList.stream()
|
|
.noneMatch(c -> c.getId().equals(entry.getId()))) {
|
|
org.to.telegramfinalproject.Client.Session.chatList.add(0, entry);
|
|
org.to.telegramfinalproject.Client.Session.activeChats.add(0, entry);
|
|
}
|
|
// سایدبارت اگر متدی برای رفرش دارد صداش بزن (اسمش را با کلاس خودت هماهنگ کن)
|
|
try {
|
|
this.refreshChatListUI(); // اگر نداری، این خط را حذف کن
|
|
} catch (Exception ignore) {}
|
|
|
|
// نمایش پیج چت
|
|
if (getChatPageController() != null) {
|
|
getChatPageController().showChat(entry);
|
|
}
|
|
}
|
|
|
|
|
|
public void refreshSidebarUserFromSession() {
|
|
if (sidebarController != null && Session.currentUser != null) {
|
|
Platform.runLater(() -> sidebarController.setUserFromSession(Session.currentUser));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |