Private chat info UI + backend implemented.
This commit is contained in:
@@ -7,7 +7,7 @@ import java.sql.SQLException;
|
||||
public class ConnectionDb {
|
||||
private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/Telegram";
|
||||
private static final String USERNAME = "postgres";
|
||||
private static final String PASSWORD = "Partow@1384";
|
||||
private static final String PASSWORD = "124postpass";
|
||||
|
||||
public ConnectionDb() {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.to.telegramfinalproject.UI;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||
|
||||
public class ChannelInfoController {
|
||||
public void setChannelDataFromJson(ChatEntry entry, JSONObject data) {
|
||||
// read name, status, bio, image url from JSON
|
||||
// update labels/images accordingly
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.to.telegramfinalproject.UI;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Side;
|
||||
@@ -23,6 +24,7 @@ import org.to.telegramfinalproject.Client.Session;
|
||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
@@ -58,6 +60,8 @@ public class ChatPageController {
|
||||
private Label chatTitle; // contact/group title
|
||||
@FXML
|
||||
private Label chatStatus; // last seen / online
|
||||
@FXML
|
||||
private HBox chatHeader; // Header
|
||||
|
||||
@FXML
|
||||
private Button searchInChatButton; // magnifier button
|
||||
@@ -84,11 +88,11 @@ public class ChatPageController {
|
||||
@FXML private VBox joinPane;
|
||||
@FXML private VBox addContactPane;
|
||||
|
||||
//For search handeling
|
||||
// For search handling
|
||||
@FXML private Button joinButton;
|
||||
@FXML private Button addContactButton;
|
||||
|
||||
//Handle View chat
|
||||
// Handle View chat
|
||||
@FXML private Button unblockBtn;
|
||||
@FXML private VBox readOnlyPane;
|
||||
@FXML private Label readOnlyLabel;
|
||||
@@ -124,6 +128,7 @@ public class ChatPageController {
|
||||
|
||||
// Where your icons live
|
||||
private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/";
|
||||
private static ChatPageController instance;
|
||||
private ChatEntry currentChat;
|
||||
private UUID me;
|
||||
|
||||
@@ -148,6 +153,14 @@ public class ChatPageController {
|
||||
}
|
||||
}
|
||||
|
||||
public ChatPageController() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static ChatPageController getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static String str(org.json.JSONObject j, String k) {
|
||||
try { return (j.has(k) && !j.isNull(k)) ? j.getString(k) : ""; } catch (Exception e) { return ""; }
|
||||
}
|
||||
@@ -813,7 +826,6 @@ public void showChat(ChatEntry entry) {
|
||||
}
|
||||
AvatarFX.circleClip(userAvatar, 36);
|
||||
|
||||
|
||||
// حالت اولیه (بدون انتظار هدر)
|
||||
if ("channel".equalsIgnoreCase(entry.getType())) {
|
||||
boolean canPostLocal = entry.isOwner() || entry.isAdmin()
|
||||
@@ -830,6 +842,8 @@ public void showChat(ChatEntry entry) {
|
||||
// حالا هدر بیاد، دوباره نهاییاش میکنیم
|
||||
fetchAndRenderHeader(entry);
|
||||
|
||||
// === (3-dot menu + header click) ===
|
||||
configureHeaderActions(entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -883,16 +897,18 @@ public void showChat(ChatEntry entry) {
|
||||
public void showChat(ChatEntry entry, ChatViewMode mode) {
|
||||
this.currentChat = entry;
|
||||
|
||||
// --- Header ---
|
||||
// --- Header (fallback until server responds) ---
|
||||
chatTitle.setText(entry.getName());
|
||||
if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) {
|
||||
Image img = AvatarLocalResolver.load(entry.getImageUrl());
|
||||
if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType());
|
||||
if (img != null) userAvatar.setImage(img);
|
||||
else setDefaultHeaderAvatarByType(entry.getType());
|
||||
} else {
|
||||
setDefaultHeaderAvatarByType(entry.getType());
|
||||
}
|
||||
AvatarFX.circleClip(userAvatar, 36);
|
||||
|
||||
// Fetch live header info from server
|
||||
fetchAndRenderHeader(entry);
|
||||
|
||||
// --- Messages ---
|
||||
@@ -900,13 +916,222 @@ public void showChat(ChatEntry entry) {
|
||||
loadMessages(entry);
|
||||
markAsRead(entry);
|
||||
|
||||
// --- حالت UI (Composer / Join / Add Contact) ---
|
||||
// --- UI mode (Composer / Join / Add Contact) ---
|
||||
applyMode(mode);
|
||||
|
||||
// فوکوس اگر در حالت نرمال هستیم
|
||||
// Focus if normal mode
|
||||
if (mode == ChatViewMode.NORMAL) {
|
||||
Platform.runLater(() -> messageInput.requestFocus());
|
||||
}
|
||||
|
||||
// Wire up menu + header click
|
||||
configureHeaderActions(entry);
|
||||
}
|
||||
|
||||
private void configureHeaderActions(ChatEntry entry) {
|
||||
moreMenu.getItems().clear();
|
||||
|
||||
switch (entry.getType().toLowerCase(Locale.ROOT)) {
|
||||
case "private" -> {
|
||||
MenuItem viewProfile = new MenuItem("View profile");
|
||||
viewProfile.setOnAction(e -> openInfoScene(entry));
|
||||
|
||||
MenuItem deleteChat = new MenuItem("Delete chat");
|
||||
deleteChat.setStyle("-fx-text-fill: red;");
|
||||
deleteChat.setOnAction(e -> deleteChatButton(entry));
|
||||
|
||||
moreMenu.getItems().addAll(viewProfile, deleteChat);
|
||||
}
|
||||
case "group" -> {
|
||||
MenuItem viewGroup = new MenuItem("View group info");
|
||||
viewGroup.setOnAction(e -> openInfoScene(entry));
|
||||
|
||||
MenuItem leaveGroup = new MenuItem("Leave group");
|
||||
leaveGroup.setOnAction(e -> leaveGroupButton(entry));
|
||||
|
||||
moreMenu.getItems().addAll(viewGroup, leaveGroup);
|
||||
}
|
||||
case "channel" -> {
|
||||
MenuItem viewChannel = new MenuItem("View channel info");
|
||||
viewChannel.setOnAction(e -> openInfoScene(entry));
|
||||
|
||||
MenuItem leaveChannel = new MenuItem("Leave channel");
|
||||
leaveChannel.setOnAction(e -> leaveChannelButton(entry));
|
||||
|
||||
moreMenu.getItems().addAll(viewChannel, leaveChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking header (but not the 3-dot) → open info scene
|
||||
chatHeader.setOnMouseClicked(e -> {
|
||||
if (!moreButton.equals(e.getTarget())) {
|
||||
openInfoScene(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openInfoScene(ChatEntry entry) {
|
||||
JSONObject req = new JSONObject();
|
||||
|
||||
switch (entry.getType().toLowerCase()) {
|
||||
case "private" -> {
|
||||
// First ask server for the target_id of this private chat
|
||||
JSONObject targetReq = new JSONObject()
|
||||
.put("action", "get_private_chat_target")
|
||||
.put("chat_id", entry.getId().toString());
|
||||
|
||||
JSONObject targetResp = ActionHandler.sendWithResponse(targetReq);
|
||||
if (targetResp == null || !"success".equalsIgnoreCase(targetResp.optString("status"))) {
|
||||
Platform.runLater(() ->
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
targetResp != null ? targetResp.optString("message") : "Failed to fetch chat target.",
|
||||
Alert.AlertType.ERROR
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String targetId = targetResp.optJSONObject("data").optString("target_id", null);
|
||||
if (targetId == null || targetId.isBlank()) {
|
||||
Platform.runLater(() ->
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
"No target user found for this chat.",
|
||||
Alert.AlertType.ERROR
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.put("action", "view_profile")
|
||||
.put("target_id", targetId);
|
||||
}
|
||||
case "group" -> {
|
||||
req.put("action", "view_group")
|
||||
.put("group_id", entry.getId().toString());
|
||||
}
|
||||
case "channel" -> {
|
||||
req.put("action", "view_channel")
|
||||
.put("channel_id", entry.getId().toString());
|
||||
}
|
||||
default -> {
|
||||
Platform.runLater(() ->
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
"Unsupported chat type: " + entry.getType(),
|
||||
Alert.AlertType.ERROR
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
Platform.runLater(() ->
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
resp != null ? resp.optString("message") : "Server not responding.",
|
||||
Alert.AlertType.ERROR
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject data = resp.optJSONObject("data");
|
||||
if (data == null) {
|
||||
Platform.runLater(() ->
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
"Malformed server response.",
|
||||
Alert.AlertType.ERROR
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
FXMLLoader loader;
|
||||
Node overlay = null;
|
||||
|
||||
switch (entry.getType().toLowerCase()) {
|
||||
case "private" -> {
|
||||
loader = new FXMLLoader(getClass().getResource(
|
||||
"/org/to/telegramfinalproject/Fxml/user_info.fxml"));
|
||||
overlay = loader.load();
|
||||
UserInfoController c = loader.getController();
|
||||
c.setProfileDataFromJson(entry, data);
|
||||
}
|
||||
case "group" -> {
|
||||
loader = new FXMLLoader(getClass().getResource(
|
||||
"/org/to/telegramfinalproject/Fxml/group_info.fxml"));
|
||||
overlay = loader.load();
|
||||
GroupInfoController c = loader.getController();
|
||||
c.setGroupDataFromJson(entry, data);
|
||||
}
|
||||
case "channel" -> {
|
||||
loader = new FXMLLoader(getClass().getResource(
|
||||
"/org/to/telegramfinalproject/Fxml/channel_info.fxml"));
|
||||
overlay = loader.load();
|
||||
ChannelInfoController c = loader.getController();
|
||||
c.setChannelDataFromJson(entry, data);
|
||||
}
|
||||
}
|
||||
|
||||
if (overlay != null) {
|
||||
MainController.getInstance().showOverlay(overlay);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
MainController.getInstance().showAlert(
|
||||
"Error",
|
||||
"Could not load info scene.",
|
||||
Alert.AlertType.ERROR
|
||||
);
|
||||
}
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void deleteChatButton(ChatEntry entry) {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "delete_chat")
|
||||
.put("receiver_id", entry.getId().toString())
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
// TODO: close chat + refresh sidebar
|
||||
MainController.getInstance().closeCurrentChat();
|
||||
}
|
||||
}
|
||||
|
||||
private void leaveGroupButton(ChatEntry entry) {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "leave_group")
|
||||
.put("group_id", entry.getId().toString())
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
MainController.getInstance().closeCurrentChat();
|
||||
}
|
||||
}
|
||||
|
||||
private void leaveChannelButton(ChatEntry entry) {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "leave_channel")
|
||||
.put("channel_id", entry.getId().toString())
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
MainController.getInstance().closeCurrentChat();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMessages(ChatEntry entry) {
|
||||
@@ -1845,7 +2070,7 @@ private void addBubble(
|
||||
|
||||
|
||||
|
||||
private String userStatusText(boolean online, String lastSeenIso) {
|
||||
public String userStatusText(boolean online, String lastSeenIso) {
|
||||
if (online) return "online";
|
||||
|
||||
LocalDateTime ts = parseWhen(lastSeenIso);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.to.telegramfinalproject.UI;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||
|
||||
public class GroupInfoController {
|
||||
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
|
||||
// read name, status, bio, image url from JSON
|
||||
// update labels/images accordingly
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,6 @@ public class MainController {
|
||||
private boolean blockedByMeFlag = false;
|
||||
private boolean blockedMeFlag = false;
|
||||
|
||||
|
||||
|
||||
|
||||
private enum SearchMode {
|
||||
GLOBAL,
|
||||
CHAT
|
||||
@@ -635,6 +632,37 @@ public class MainController {
|
||||
}
|
||||
}
|
||||
|
||||
public void closeCurrentChat() {
|
||||
// 1. Clear the chat area
|
||||
chatDisplayArea.getChildren().clear();
|
||||
|
||||
// 2. Re-add the placeholder label
|
||||
placeholderLabel.setVisible(true);
|
||||
placeholderLabel.setManaged(true);
|
||||
placeholderLabel.setText("Select a chat to start messaging");
|
||||
|
||||
chatDisplayArea.getChildren().add(placeholderLabel);
|
||||
|
||||
// 3. Ask the chat list to refresh
|
||||
MainController.getInstance().refreshChatListUI();
|
||||
}
|
||||
|
||||
public void showAlert(String title, String message, Alert.AlertType type) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(type);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
|
||||
// Tie it to the main window so it centers nicely
|
||||
if (mainRoot.getScene() != null && mainRoot.getScene().getWindow() != null) {
|
||||
alert.initOwner(mainRoot.getScene().getWindow());
|
||||
}
|
||||
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Search UI state =====
|
||||
private final java.util.List<SearchResult> searchBacking = new java.util.ArrayList<>();
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.to.telegramfinalproject.UI;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||
import org.to.telegramfinalproject.Client.Session;
|
||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||
|
||||
public class UserInfoController {
|
||||
|
||||
@FXML private VBox profileCard;
|
||||
@FXML private Pane overlayBackground;
|
||||
@FXML private Button closeButton;
|
||||
|
||||
@FXML private ImageView profileImage;
|
||||
@FXML private Label profileName;
|
||||
@FXML private Label profileStatus;
|
||||
@FXML private VBox bioBlock;
|
||||
@FXML private Label userBio;
|
||||
@FXML private VBox usernameBlock;
|
||||
@FXML private Label userId;
|
||||
|
||||
@FXML private Button moreButton;
|
||||
@FXML private ContextMenu moreMenu;
|
||||
@FXML private MenuItem deleteChatItem;
|
||||
@FXML private MenuItem blockItem;
|
||||
@FXML private MenuItem unblockItem;
|
||||
|
||||
private String otherUserId; // internal_uuid of the other user
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
closeButton.setOnAction(e ->
|
||||
MainController.getInstance().closeOverlay(profileCard.getParent()));
|
||||
overlayBackground.setOnMouseClicked(e ->
|
||||
MainController.getInstance().closeOverlay(profileCard.getParent()));
|
||||
|
||||
// Show menu manually
|
||||
moreButton.setOnAction(e -> {
|
||||
if (moreMenu != null) {
|
||||
moreMenu.show(moreButton, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide menu when clicking outside
|
||||
Platform.runLater(() -> {
|
||||
if (moreButton.getScene() != null) {
|
||||
moreButton.getScene().addEventFilter(
|
||||
javafx.scene.input.MouseEvent.MOUSE_PRESSED, ev -> {
|
||||
if (moreMenu.isShowing()
|
||||
&& !moreButton.localToScreen(moreButton.getBoundsInLocal())
|
||||
.contains(ev.getScreenX(), ev.getScreenY())) {
|
||||
moreMenu.hide();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
deleteChatItem.setOnAction(e -> handleDeleteChat());
|
||||
blockItem.setOnAction(e -> handleBlock());
|
||||
unblockItem.setOnAction(e -> handleUnblock());
|
||||
}
|
||||
|
||||
/** Backend JSON → UI */
|
||||
public void setProfileDataFromJson(ChatEntry entry, JSONObject data) {
|
||||
// --- Profile name ---
|
||||
String name = data.optString("profile_name", entry.getName());
|
||||
profileName.setText(name);
|
||||
|
||||
// --- Status ---
|
||||
String status = ChatPageController.getInstance().userStatusText(
|
||||
data.optBoolean("is_online", false),
|
||||
data.optString("last_seen", null)
|
||||
);
|
||||
profileStatus.setText(status);
|
||||
|
||||
// --- Bio ---
|
||||
String bio = data.optString("bio", "");
|
||||
if (bio != null && !bio.isBlank()) {
|
||||
userBio.setText(bio);
|
||||
bioBlock.setVisible(true);
|
||||
bioBlock.setManaged(true);
|
||||
} else {
|
||||
bioBlock.setVisible(false);
|
||||
bioBlock.setManaged(false);
|
||||
}
|
||||
|
||||
// --- Username ---
|
||||
String username = data.optString("user_id", "");
|
||||
if (username != null && !username.isBlank()) {
|
||||
userId.setText("@" + username);
|
||||
usernameBlock.setVisible(true);
|
||||
usernameBlock.setManaged(true);
|
||||
} else {
|
||||
usernameBlock.setVisible(false);
|
||||
usernameBlock.setManaged(false);
|
||||
}
|
||||
|
||||
// --- Profile picture ---
|
||||
String imgUrl = data.optString("image_url", "");
|
||||
if (imgUrl != null && !imgUrl.isBlank()) {
|
||||
try {
|
||||
Image img = AvatarLocalResolver.load(imgUrl);
|
||||
if (img != null) profileImage.setImage(img);
|
||||
} catch (Exception ignore) {}
|
||||
} else {
|
||||
profileImage.setImage(
|
||||
new Image(getClass().getResourceAsStream(
|
||||
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))
|
||||
);
|
||||
}
|
||||
|
||||
// --- Other user ID (needed for block/delete) ---
|
||||
this.otherUserId = data.optString("other_user_id", entry.getId().toString());
|
||||
|
||||
// --- Block/Unblock ---
|
||||
boolean blocked = data.optBoolean("blocked", false)
|
||||
|| data.optBoolean("is_blocked", false)
|
||||
|| data.optBoolean("blocked_by_me", false);
|
||||
updateBlockMenu(blocked);
|
||||
}
|
||||
|
||||
private void handleDeleteChat() {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "delete_chat")
|
||||
.put("receiver_id", otherUserId)
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
MainController.getInstance().closeOverlay(profileCard.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBlock() {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "block_user")
|
||||
.put("target_id", otherUserId)
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
updateBlockMenu(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnblock() {
|
||||
JSONObject req = new JSONObject()
|
||||
.put("action", "unblock_user")
|
||||
.put("target_id", otherUserId)
|
||||
.put("viewer_id", Session.getUserUUID());
|
||||
|
||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||
updateBlockMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBlockMenu(boolean isBlocked) {
|
||||
blockItem.setVisible(!isBlocked);
|
||||
unblockItem.setVisible(isBlocked);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user