Private chat info UI + backend implemented.

This commit is contained in:
Asal Lotfi
2025-09-05 15:31:01 +03:30
parent e112ae1308
commit 63b201b697
20 changed files with 716 additions and 12 deletions
@@ -7,7 +7,7 @@ import java.sql.SQLException;
public class ConnectionDb { public class ConnectionDb {
private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/Telegram"; private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/Telegram";
private static final String USERNAME = "postgres"; private static final String USERNAME = "postgres";
private static final String PASSWORD = "Partow@1384"; private static final String PASSWORD = "124postpass";
public ConnectionDb() { 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.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side; import javafx.geometry.Side;
@@ -23,6 +24,7 @@ import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@@ -58,6 +60,8 @@ public class ChatPageController {
private Label chatTitle; // contact/group title private Label chatTitle; // contact/group title
@FXML @FXML
private Label chatStatus; // last seen / online private Label chatStatus; // last seen / online
@FXML
private HBox chatHeader; // Header
@FXML @FXML
private Button searchInChatButton; // magnifier button private Button searchInChatButton; // magnifier button
@@ -84,7 +88,7 @@ public class ChatPageController {
@FXML private VBox joinPane; @FXML private VBox joinPane;
@FXML private VBox addContactPane; @FXML private VBox addContactPane;
//For search handeling // For search handling
@FXML private Button joinButton; @FXML private Button joinButton;
@FXML private Button addContactButton; @FXML private Button addContactButton;
@@ -124,6 +128,7 @@ public class ChatPageController {
// Where your icons live // Where your icons live
private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/"; private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/";
private static ChatPageController instance;
private ChatEntry currentChat; private ChatEntry currentChat;
private UUID me; 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) { 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 ""; } 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); AvatarFX.circleClip(userAvatar, 36);
// حالت اولیه (بدون انتظار هدر) // حالت اولیه (بدون انتظار هدر)
if ("channel".equalsIgnoreCase(entry.getType())) { if ("channel".equalsIgnoreCase(entry.getType())) {
boolean canPostLocal = entry.isOwner() || entry.isAdmin() boolean canPostLocal = entry.isOwner() || entry.isAdmin()
@@ -830,6 +842,8 @@ public void showChat(ChatEntry entry) {
// حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم // حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم
fetchAndRenderHeader(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) { public void showChat(ChatEntry entry, ChatViewMode mode) {
this.currentChat = entry; this.currentChat = entry;
// --- Header --- // --- Header (fallback until server responds) ---
chatTitle.setText(entry.getName()); chatTitle.setText(entry.getName());
if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) {
Image img = AvatarLocalResolver.load(entry.getImageUrl()); 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 { } else {
setDefaultHeaderAvatarByType(entry.getType()); setDefaultHeaderAvatarByType(entry.getType());
} }
AvatarFX.circleClip(userAvatar, 36); AvatarFX.circleClip(userAvatar, 36);
// Fetch live header info from server
fetchAndRenderHeader(entry); fetchAndRenderHeader(entry);
// --- Messages --- // --- Messages ---
@@ -900,13 +916,222 @@ public void showChat(ChatEntry entry) {
loadMessages(entry); loadMessages(entry);
markAsRead(entry); markAsRead(entry);
// --- حالت UI (Composer / Join / Add Contact) --- // --- UI mode (Composer / Join / Add Contact) ---
applyMode(mode); applyMode(mode);
// فوکوس اگر در حالت نرمال هستیم // Focus if normal mode
if (mode == ChatViewMode.NORMAL) { if (mode == ChatViewMode.NORMAL) {
Platform.runLater(() -> messageInput.requestFocus()); 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) { 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"; if (online) return "online";
LocalDateTime ts = parseWhen(lastSeenIso); 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 blockedByMeFlag = false;
private boolean blockedMeFlag = false; private boolean blockedMeFlag = false;
private enum SearchMode { private enum SearchMode {
GLOBAL, GLOBAL,
CHAT 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 ===== // ===== Search UI state =====
private final java.util.List<SearchResult> searchBacking = new java.util.ArrayList<>(); 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);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

@@ -1079,3 +1079,56 @@
-fx-border-color: red; -fx-border-color: red;
-fx-border-radius: 6; -fx-border-radius: 6;
} }
/* === User Info (Dark) === */
.user-info-card {
-fx-background-color: #1f1f1f;
-fx-background-radius: 8;
-fx-padding: 16;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.5), 12, 0, 0, 3);
}
.user-info-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #f2f2f2;
}
.user-info-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #fff;
}
.user-info-status {
-fx-font-size: 12px;
-fx-text-fill: #aaa;
}
.user-info-main {
-fx-font-size: 13px;
-fx-text-fill: #f0f0f0;
}
.user-info-sub {
-fx-font-size: 11px;
-fx-text-fill: #999;
}
.user-info-separator {
-fx-background-color: #333;
}
/* === Shared buttons === */
.danger-button {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
-fx-background-radius: 6;
-fx-padding: 4 12;
}
.danger-button:hover {
-fx-background-color: #c0392b;
}
/* === Profile image === */
.profile-picture {
-fx-background-color: #2c2c2c;
-fx-background-radius: 50%;
-fx-border-color: #444;
-fx-border-radius: 50%;
-fx-border-width: 1;
}
@@ -1070,3 +1070,56 @@
-fx-border-color: red; -fx-border-color: red;
-fx-border-radius: 6; -fx-border-radius: 6;
} }
/* === User Info (Light) === */
.user-info-card {
-fx-background-color: #ffffff;
-fx-background-radius: 8;
-fx-padding: 16;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 8, 0, 0, 2);
}
.user-info-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #111;
}
.user-info-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-text-fill: #111;
}
.user-info-status {
-fx-font-size: 12px;
-fx-text-fill: #555;
}
.user-info-main {
-fx-font-size: 13px;
-fx-text-fill: #222;
}
.user-info-sub {
-fx-font-size: 11px;
-fx-text-fill: #888;
}
.user-info-separator {
-fx-background-color: #e6e6e6;
}
/* === Shared buttons === */
.danger-button {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
-fx-background-radius: 6;
-fx-padding: 4 12;
}
.danger-button:hover {
-fx-background-color: #c0392b;
}
/* === Profile image === */
.profile-picture {
-fx-background-color: #f0f0f0;
-fx-background-radius: 50%;
-fx-border-color: #ddd;
-fx-border-radius: 50%;
-fx-border-width: 1;
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.ChannelInfoController"
prefHeight="400.0" prefWidth="600.0">
</AnchorPane>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.GroupInfoController"
prefHeight="400.0" prefWidth="600.0">
</AnchorPane>
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.Image?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.UserInfoController"
styleClass="overlay-root">
<!-- Dimmed background -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Profile card -->
<VBox fx:id="profileCard" styleClass="profile-card" spacing="16"
prefWidth="360" maxWidth="360"
prefHeight="480" maxHeight="480">
<!-- ===== Header: User Info (left) + actions (right) ===== -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="User Info" styleClass="user-info-title" HBox.hgrow="ALWAYS"/>
<Pane HBox.hgrow="ALWAYS"/> <!-- pushes buttons to right -->
<!-- More / overflow -->
<Button fx:id="moreButton" styleClass="icon-button">
<graphic>
<ImageView fitWidth="18" fitHeight="18" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/more_dark.png"/>
</image>
</ImageView>
</graphic>
<contextMenu>
<ContextMenu fx:id="moreMenu">
<items>
<MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
<MenuItem fx:id="blockItem" text="Block" style="-fx-text-fill: red;">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/block_red.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
<MenuItem fx:id="unblockItem" text="Unblock" visible="false" style="-fx-text-fill: red;">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/block_red.png"/>
</image>
</ImageView>
</graphic>
</MenuItem>
</items>
</ContextMenu>
</contextMenu>
</Button>
<!-- Close -->
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
</HBox>
<!-- Profile: picture + name/status -->
<HBox alignment="CENTER_LEFT" spacing="12">
<ImageView fx:id="profileImage" fitWidth="80" fitHeight="80" preserveRatio="true"
styleClass="profile-picture"/>
<VBox alignment="CENTER_LEFT" spacing="4">
<Label fx:id="profileName" text="User Name" styleClass="profile-name"/>
<Label fx:id="profileStatus" text="last seen recently" styleClass="profile-status"/>
</VBox>
</HBox>
<Separator styleClass="section-separator"/>
<!-- Info blocks -->
<HBox alignment="TOP_LEFT">
<Pane prefWidth="30"/> <!-- left spacer -->
<VBox spacing="16" styleClass="info-blocks">
<!-- Bio -->
<VBox fx:id="bioBlock" spacing="2">
<HBox spacing="6" alignment="CENTER_LEFT">
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/bio_dark.png"/>
</image>
</ImageView>
<Label fx:id="userBio" wrapText="true" styleClass="profile-info-main"/>
</HBox>
<Label text="Bio" styleClass="profile-info-sub"/>
</VBox>
<!-- Username -->
<VBox fx:id="usernameBlock" spacing="2">
<HBox spacing="6" alignment="CENTER_LEFT">
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/user_id_dark.png"/>
</image>
</ImageView>
<Label fx:id="userId" styleClass="profile-info-main"/>
</HBox>
<Label text="Username" styleClass="profile-info-sub"/>
</VBox>
</VBox>
</HBox>
<Separator styleClass="section-separator"/>
</VBox>
</StackPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B