Merge remote-tracking branch 'origin/Main-UI' into Main-UI

# Conflicts:
#	src/main/java/org/to/telegramfinalproject/UI/UserInfoController.java
This commit is contained in:
2025-09-05 23:16:24 +03:30
4 changed files with 165 additions and 131 deletions
@@ -14,7 +14,7 @@ public final class AvatarFX {
Circle c = new Circle(); Circle c = new Circle();
c.radiusProperty().bind(Bindings.min(iv.fitWidthProperty(), iv.fitHeightProperty()).divide(2)); c.radiusProperty().bind(Bindings.min(iv.fitWidthProperty(), iv.fitHeightProperty()).divide(2));
c.centerXProperty().bind(iv.fitWidthProperty().divide(2)); c.centerXProperty().bind(iv.fitWidthProperty().divide(3));
c.centerYProperty().bind(iv.fitHeightProperty().divide(2)); c.centerYProperty().bind(iv.fitHeightProperty().divide(2));
iv.setClip(c); iv.setClip(c);
} }
@@ -842,7 +842,7 @@ public class ChatPageController {
} else { } else {
setDefaultHeaderAvatarByType(entry.getType()); setDefaultHeaderAvatarByType(entry.getType());
} }
AvatarFX.circleClip(userAvatar, 36); AvatarFX.circleClip(userAvatar, 40);
// حالت اولیه (بدون انتظار هدر) // حالت اولیه (بدون انتظار هدر)
@@ -863,11 +863,13 @@ public class ChatPageController {
// === (3-dot menu + header click) === // === (3-dot menu + header click) ===
configureHeaderActions(entry); configureHeaderActions(entry);
requestBlockStatusByChat(entry);
} }
private void requestBlockStatusByChat(ChatEntry entry) { public void requestBlockStatusByChat(ChatEntry entry) {
if (entry == null || !"private".equalsIgnoreCase(entry.getType())) return; if (entry == null || !"private".equalsIgnoreCase(entry.getType())) return;
String viewerId = Session.getUserUUID(); // internal_uuid کاربر فعلی String viewerId = Session.getUserUUID(); // internal_uuid کاربر فعلی
@@ -890,7 +892,7 @@ public class ChatPageController {
}).start(); }).start();
} }
private void applyBlockUi(boolean blockedByMe, boolean blockedMe) { public void applyBlockUi(boolean blockedByMe, boolean blockedMe) {
this.blockedByMeFlag = blockedByMe; this.blockedByMeFlag = blockedByMe;
this.blockedMeFlag = blockedMe; this.blockedMeFlag = blockedMe;
@@ -945,6 +947,8 @@ public class ChatPageController {
// Wire up menu + header click // Wire up menu + header click
configureHeaderActions(entry); configureHeaderActions(entry);
requestBlockStatusByChat(entry);
} }
private void configureHeaderActions(ChatEntry entry) { private void configureHeaderActions(ChatEntry entry) {
@@ -983,6 +987,7 @@ public class ChatPageController {
private void openInfoScene(ChatEntry entry) { private void openInfoScene(ChatEntry entry) {
JSONObject req = new JSONObject(); JSONObject req = new JSONObject();
String targetId = null; // 👈 capture it here for the private case
switch (entry.getType().toLowerCase()) { switch (entry.getType().toLowerCase()) {
case "private" -> { case "private" -> {
@@ -1003,7 +1008,7 @@ public class ChatPageController {
return; return;
} }
String targetId = targetResp.optJSONObject("data").optString("target_id", null); targetId = targetResp.optJSONObject("data").optString("target_id", null);
if (targetId == null || targetId.isBlank()) { if (targetId == null || targetId.isBlank()) {
Platform.runLater(() -> Platform.runLater(() ->
MainController.getInstance().showAlert( MainController.getInstance().showAlert(
@@ -1038,6 +1043,8 @@ public class ChatPageController {
} }
} }
final String finalTargetId = targetId; // 👈 capture for use in the FX thread
new Thread(() -> { new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req); JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
@@ -1074,7 +1081,8 @@ public class ChatPageController {
"/org/to/telegramfinalproject/Fxml/user_info.fxml")); "/org/to/telegramfinalproject/Fxml/user_info.fxml"));
overlay = loader.load(); overlay = loader.load();
UserInfoController c = loader.getController(); UserInfoController c = loader.getController();
c.setProfileDataFromJson(entry, data); // 👇 pass targetId so UserInfoController gets the real UUID
c.setProfileDataFromJson(entry, data, finalTargetId);
} }
case "group" -> { case "group" -> {
loader = new FXMLLoader(getClass().getResource( loader = new FXMLLoader(getClass().getResource(
@@ -44,6 +44,7 @@ public class UserInfoController {
private String otherUserId; // internal_uuid of the other user private String otherUserId; // internal_uuid of the other user
private ChatEntry entry;
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/"; private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@@ -77,8 +78,8 @@ public class UserInfoController {
}); });
deleteChatItem.setOnAction(e -> onDeleteChatClicked()); deleteChatItem.setOnAction(e -> onDeleteChatClicked());
blockItem.setOnAction(e -> handleBlock()); blockItem.setOnAction(e -> handleToggleBlock(entry));
unblockItem.setOnAction(e -> handleUnblock()); unblockItem.setOnAction(e -> handleToggleBlock(entry));
// Register scene for ThemeManager → stylesheet swap will handle colors/icons // Register scene for ThemeManager → stylesheet swap will handle colors/icons
Platform.runLater(() -> { Platform.runLater(() -> {
@@ -97,7 +98,11 @@ public class UserInfoController {
} }
/** Backend JSON → UI */ /** Backend JSON → UI */
public void setProfileDataFromJson(ChatEntry entry, JSONObject data) { public void setProfileDataFromJson(ChatEntry entry, JSONObject data, String targetUuid) {
// save the correct UUID
this.otherUserId = targetUuid;
this.entry = entry;
// --- Profile name --- // --- Profile name ---
String name = data.optString("profile_name", entry.getName()); String name = data.optString("profile_name", entry.getName());
profileName.setText(name); profileName.setText(name);
@@ -145,9 +150,6 @@ public class UserInfoController {
); );
} }
// --- Other user ID (needed for block/delete) ---
this.otherUserId = data.optString("other_user_id", entry.getId().toString());
// --- Block/Unblock --- // --- Block/Unblock ---
boolean blocked = data.optBoolean("blocked", false) boolean blocked = data.optBoolean("blocked", false)
|| data.optBoolean("is_blocked", false) || data.optBoolean("is_blocked", false)
@@ -167,128 +169,55 @@ public class UserInfoController {
} }
} }
private void handleToggleBlock(ChatEntry entry) {
if (otherUserId == null) return;
// 1) Send toggle request
JSONObject req = new JSONObject()
.put("action", "toggle_block")
.put("user_id", Session.getUserUUID())
.put("target_id", otherUserId);
@FXML JSONObject resp = ActionHandler.sendWithResponse(req);
private void onDeleteChatClicked() { if (resp == null) {
// if (!"private".equalsIgnoreCase(Session.currentChatType)) { MainController.getInstance().showAlert("Error", "No response from server.", Alert.AlertType.ERROR);
// new Alert(Alert.AlertType.INFORMATION, "This action is available only for private chats.").showAndWait();
// return;
// }
UUID chatId = Session.currentChatEntry != null
? Session.currentChatEntry.getId()
: (Session.currentChatId != null ? UUID.fromString(Session.currentChatId) : null);
if (chatId == null) {
new Alert(Alert.AlertType.ERROR, "Invalid chat id.").showAndWait();
return; return;
} }
ButtonType oneSide = new ButtonType("Delete one-sided", ButtonBar.ButtonData.LEFT); if (!"success".equalsIgnoreCase(resp.optString("status"))) {
ButtonType bothSide = new ButtonType("Delete both-sided", ButtonBar.ButtonData.OK_DONE); MainController.getInstance().showAlert("Error",
ButtonType cancel = ButtonType.CANCEL; resp.optString("message", "Failed to toggle block"),
Alert.AlertType.ERROR);
Alert dlg = new Alert(
Alert.AlertType.CONFIRMATION,
"Choose how you want to delete this private chat:",
oneSide, bothSide, cancel
);
dlg.setHeaderText("Delete Private Chat");
Optional<ButtonType> res = dlg.showAndWait();
if (res.isEmpty() || res.get() == cancel) return;
boolean both = (res.get() == bothSide);
performDeletePrivateChat(chatId, both);
}
private void performDeletePrivateChat(UUID chatId, boolean both) {
// نال‌سیف: هر کدوم هست disable کن
if (deleteChatButton != null) deleteChatButton.setDisable(true);
if (deleteChatItem != null) deleteChatItem.setDisable(true);
if (infoMoreMenu != null && infoMoreMenu.isShowing()) infoMoreMenu.hide();
if (infoMoreButton != null) infoMoreButton.setDisable(true);
new Thread(() -> {
JSONObject req = new JSONObject()
.put("action", "delete_private_chat")
.put("chat_id", chatId.toString())
.put("both", both);
JSONObject resp = ActionHandler.sendWithResponse(req);
Platform.runLater(() -> {
// re-enable نال‌سیف
if (deleteChatButton != null) deleteChatButton.setDisable(false);
if (deleteChatItem != null) deleteChatItem.setDisable(false);
if (infoMoreButton != null) infoMoreButton.setDisable(false);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
removeChatFromSessionAndGoBack(chatId);
MainController.getInstance().closeOverlay(profileCard.getParent());
// new Alert(Alert.AlertType.INFORMATION,
// "Chat deleted " + (both ? "for both sides." : "only for you.")
// ).showAndWait();
} else {
String msg = (resp != null ? resp.optString("message", "Unknown error")
: "No response from server.");
new Alert(Alert.AlertType.ERROR, "Failed to delete chat: " + msg).showAndWait();
}
});
}).start();
}
private void removeChatFromSessionAndGoBack(UUID chatId) {
// if (Session.chatList != null) Session.chatList.removeIf(e -> chatId.equals(e.getId()));
// if (Session.activeChats != null) Session.activeChats.removeIf(e -> chatId.equals(e.getId()));
// if (Session.archivedChats != null) Session.archivedChats.removeIf(e -> chatId.equals(e.getId()));
if (Session.currentChatId != null && Session.currentChatId.equals(chatId.toString())) {
Session.currentChatId = null;
Session.currentChatType = null;
Session.currentChatEntry = null;
Session.inChatMenu = false;
MainController.getInstance().closeOverlay(profileCard.getParent());
AppRouter.showMain();
return; return;
} }
try { // 2) Immediately re-check block state from server
MainController.getInstance().refreshChatListUI(); JSONObject checkReq = new JSONObject()
} catch (Exception ignore) { .put("action", "check_block_status_by_chat")
AppRouter.showMain(); .put("viewer_id", Session.getUserUUID())
.put("chat_id", entry.getId().toString());
JSONObject checkResp = ActionHandler.sendWithResponse(checkReq);
if (checkResp == null || !"success".equalsIgnoreCase(checkResp.optString("status"))) {
System.err.println("⚠️ Failed to refresh block status after toggle.");
return;
} }
}
JSONObject data = checkResp.optJSONObject("data");
boolean blockedByMe = data != null && data.optBoolean("blocked_by_me", false);
boolean blockedMe = data != null && data.optBoolean("blocked_me", false);
private void handleBlock() { // 3) Update UI on FX thread
JSONObject req = new JSONObject() Platform.runLater(() -> {
.put("action", "block_user") // update overlay (block/unblock menu)
.put("target_id", otherUserId) updateBlockMenu(blockedByMe);
.put("viewer_id", Session.getUserUUID());
JSONObject resp = ActionHandler.sendWithResponse(req); // update chat page input bar
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { ChatPageController c = ChatPageController.getInstance();
updateBlockMenu(true); if (c != null) {
} c.applyBlockUi(blockedByMe, blockedMe);
} }
});
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) { private void updateBlockMenu(boolean isBlocked) {
@@ -314,4 +243,101 @@ public class UserInfoController {
} }
return new Image(res.toExternalForm()); return new Image(res.toExternalForm());
} }
@FXML
private void onDeleteChatClicked() {
// if (!"private".equalsIgnoreCase(Session.currentChatType)) {
// new Alert(Alert.AlertType.INFORMATION, "This action is available only for private chats.").showAndWait();
// return;
// }
UUID chatId = Session.currentChatEntry != null
? Session.currentChatEntry.getId()
: (Session.currentChatId != null ? UUID.fromString(Session.currentChatId) : null);
if (chatId == null) {
new Alert(Alert.AlertType.ERROR, "Invalid chat id.").showAndWait();
return;
}
ButtonType oneSide = new ButtonType("Delete one-sided", ButtonBar.ButtonData.LEFT);
ButtonType bothSide = new ButtonType("Delete both-sided", ButtonBar.ButtonData.OK_DONE);
ButtonType cancel = ButtonType.CANCEL;
Alert dlg = new Alert(
Alert.AlertType.CONFIRMATION,
"Choose how you want to delete this private chat:",
oneSide, bothSide, cancel
);
dlg.setHeaderText("Delete Private Chat");
Optional<ButtonType> res = dlg.showAndWait();
if (res.isEmpty() || res.get() == cancel) return;
boolean both = (res.get() == bothSide);
performDeletePrivateChat(chatId, both);
}
private void performDeletePrivateChat(UUID chatId, boolean both) {
// نال‌سیف: هر کدوم هست disable کن
if (deleteChatButton != null) deleteChatButton.setDisable(true);
if (deleteChatItem != null) deleteChatItem.setDisable(true);
if (infoMoreMenu != null && infoMoreMenu.isShowing()) infoMoreMenu.hide();
if (infoMoreButton != null) infoMoreButton.setDisable(true);
new Thread(() -> {
JSONObject req = new JSONObject()
.put("action", "delete_private_chat")
.put("chat_id", chatId.toString())
.put("both", both);
JSONObject resp = ActionHandler.sendWithResponse(req);
Platform.runLater(() -> {
// re-enable نال‌سیف
if (deleteChatButton != null) deleteChatButton.setDisable(false);
if (deleteChatItem != null) deleteChatItem.setDisable(false);
if (infoMoreButton != null) infoMoreButton.setDisable(false);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
removeChatFromSessionAndGoBack(chatId);
MainController.getInstance().closeOverlay(profileCard.getParent());
// new Alert(Alert.AlertType.INFORMATION,
// "Chat deleted " + (both ? "for both sides." : "only for you.")
// ).showAndWait();
} else {
String msg = (resp != null ? resp.optString("message", "Unknown error")
: "No response from server.");
new Alert(Alert.AlertType.ERROR, "Failed to delete chat: " + msg).showAndWait();
}
});
}).start();
}
private void removeChatFromSessionAndGoBack(UUID chatId) {
// if (Session.chatList != null) Session.chatList.removeIf(e -> chatId.equals(e.getId()));
// if (Session.activeChats != null) Session.activeChats.removeIf(e -> chatId.equals(e.getId()));
// if (Session.archivedChats != null) Session.archivedChats.removeIf(e -> chatId.equals(e.getId()));
if (Session.currentChatId != null && Session.currentChatId.equals(chatId.toString())) {
Session.currentChatId = null;
Session.currentChatType = null;
Session.currentChatEntry = null;
Session.inChatMenu = false;
MainController.getInstance().closeOverlay(profileCard.getParent());
AppRouter.showMain();
return;
}
try {
MainController.getInstance().refreshChatListUI();
} catch (Exception ignore) {
AppRouter.showMain();
}
}
} }
@@ -35,15 +35,6 @@
<contextMenu> <contextMenu>
<ContextMenu fx:id="infoMoreMenu"> <ContextMenu fx:id="infoMoreMenu">
<items> <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;"> <MenuItem fx:id="blockItem" text="Block" style="-fx-text-fill: red;">
<graphic> <graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true"> <ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
@@ -62,6 +53,15 @@
</ImageView> </ImageView>
</graphic> </graphic>
</MenuItem> </MenuItem>
<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>
</items> </items>
</ContextMenu> </ContextMenu>
</contextMenu> </contextMenu>