Manage Group UI + backend implemented.

This commit is contained in:
Asal Lotfi
2025-09-07 15:58:12 +03:30
parent 5f84e6af55
commit 1835ebdc97
10 changed files with 517 additions and 17 deletions
@@ -12,10 +12,12 @@ import javafx.scene.layout.*;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver; import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.UUID;
public class GroupInfoController { public class GroupInfoController {
@@ -55,6 +57,10 @@ public class GroupInfoController {
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0); if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
}); });
if (manageGroupItem != null) {
manageGroupItem.setOnAction(e -> openManageGroupScene());
}
deleteGroupItem.setOnAction(e -> handleDeleteGroup()); deleteGroupItem.setOnAction(e -> handleDeleteGroup());
Platform.runLater(() -> { Platform.runLater(() -> {
@@ -100,15 +106,26 @@ public class GroupInfoController {
groupName.setText(data.optString("group_name", entry.getName())); groupName.setText(data.optString("group_name", entry.getName()));
memberCount.setText(data.optInt("member_count", 0) + " members"); memberCount.setText(data.optInt("member_count", 0) + " members");
// show/hide Delete button if user is owner // --- Role-based UI ---
String myRole = data.optString("my_role", "member"); String myRole = data.optString("my_role", "member").toLowerCase();
if ("owner".equalsIgnoreCase(myRole)) {
deleteGroupItem.setVisible(true); // Delete group → only owner
} else { deleteGroupItem.setVisible("owner".equals(myRole));
deleteGroupItem.setVisible(false);
// Add member (button + menu item) → owner or admin
boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
addMemberButton.setVisible(canAdd);
addMemberButton.setManaged(canAdd);
if (addMemberItem != null) {
addMemberItem.setVisible(canAdd);
} }
// Group picture // More menu (3-dot) → hide entirely for plain members
boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
infoMoreButton.setVisible(showMore);
infoMoreButton.setManaged(showMore);
// --- Group picture ---
String imgUrl = data.optString("image_url", ""); String imgUrl = data.optString("image_url", "");
if (!imgUrl.isBlank()) { if (!imgUrl.isBlank()) {
try { try {
@@ -122,7 +139,7 @@ public class GroupInfoController {
); );
} }
// Members // --- Members list ---
membersList.getChildren().clear(); membersList.getChildren().clear();
var arr = data.optJSONArray("members"); var arr = data.optJSONArray("members");
if (arr != null) { if (arr != null) {
@@ -191,6 +208,66 @@ public class GroupInfoController {
membersList.getChildren().add(row); membersList.getChildren().add(row);
} }
private void openManageGroupScene() {
new Thread(() -> {
try {
JSONObject req = new JSONObject()
.put("action", "view_group")
.put("group_id", groupId)
.put("viewer_id", Session.getUserUUID());
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 = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_group.fxml"));
Node overlay = loader.load();
ManageGroupController controller = loader.getController();
controller.setGroupData(data); // pass the full JSON
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Manage Group scene.",
Alert.AlertType.ERROR
);
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Error while fetching group info: " + e.getMessage(),
Alert.AlertType.ERROR
));
}
}).start();
}
private void openAddMemberScene() { private void openAddMemberScene() {
try { try {
FXMLLoader loader = new FXMLLoader(getClass().getResource( FXMLLoader loader = new FXMLLoader(getClass().getResource(
@@ -198,8 +275,7 @@ public class GroupInfoController {
Node overlay = loader.load(); Node overlay = loader.load();
AddMembersController controller = loader.getController(); AddMembersController controller = loader.getController();
// 👇 Pass group id so AddMember scene knows which group to add to controller.setGroupForAdd(UUID.fromString(groupId), String.valueOf(groupName));
controller.setGroupForAdd(groupId, groupName);
MainController.getInstance().showOverlay(overlay); MainController.getInstance().showOverlay(overlay);
} catch (IOException e) { } catch (IOException e) {
@@ -0,0 +1,170 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
public class ManageGroupController {
@FXML private VBox manageGroupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
@FXML private ImageView groupImage;
@FXML private TextField groupNameField;
@FXML private Label adminCount, memberCount;
@FXML private TextField groupIdField;
@FXML private Button manageAdminsButton;
@FXML private Button manageMembersButton;
private String groupId; // Internal UUID of the current group
private String originalName;
private String originalGroupId;
private String originalImageUrl;
private File selectedImageFile;
private JSONObject data;
@FXML
public void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
changePicButton.setOnAction(e -> choosePicture());
saveButton.setOnAction(e -> saveChanges());
manageAdminsButton.setOnAction(e -> openManageAdminsScene());
manageMembersButton.setOnAction(e -> openManageMembersScene(data));
}
private void openManageAdminsScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_admins.fxml"));
Node overlay = loader.load();
// ManageAdminsController controller = loader.getController();
// controller.setGroupId(UUID.fromString(groupId));
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Admins scene.", Alert.AlertType.ERROR);
}
}
private void openManageMembersScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_members.fxml"));
Node overlay = loader.load();
ManageMembersController controller = loader.getController();
// Pass groupId and members list from the JSON data
controller.setGroupData(
data.optString("internal_uuid"),
data.optJSONArray("members")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Members scene.", Alert.AlertType.ERROR);
}
}
public void setGroupData(JSONObject data) {
this.data = data;
this.groupId = data.optString("internal_uuid");
originalName = data.optString("group_name", "");
originalGroupId = data.optString("group_id", "");
originalImageUrl = data.optString("image_url", "");
groupNameField.setText(originalName);
groupIdField.setText(originalGroupId);
if (!originalImageUrl.isBlank()) {
groupImage.setImage(new Image(originalImageUrl, true));
}
adminCount.setText(String.valueOf(countRole(data, "admin")));
memberCount.setText(String.valueOf(countRole(data, "member")));
}
private int countRole(JSONObject groupData, String role) {
var arr = groupData.optJSONArray("members");
if (arr == null) return 0;
int count = 0;
for (int i = 0; i < arr.length(); i++) {
if (role.equalsIgnoreCase(arr.getJSONObject(i).optString("role"))) count++;
}
return count + 1;
}
private void choosePicture() {
FileChooser fc = new FileChooser();
fc.setTitle("Select group picture");
selectedImageFile = fc.showOpenDialog(manageGroupCard.getScene().getWindow());
if (selectedImageFile != null) {
groupImage.setImage(new Image(selectedImageFile.toURI().toString()));
}
}
private void saveChanges() {
String newName = groupNameField.getText().trim();
String newGroupId = groupIdField.getText().trim(); // make sure you added this field
String newImageUrl = (selectedImageFile != null)
? selectedImageFile.toURI().toString()
: originalImageUrl;
// Check if anything actually changed
boolean changed =
!Objects.equals(originalName, newName) ||
!Objects.equals(originalGroupId, newGroupId) ||
!Objects.equals(originalImageUrl, newImageUrl);
if (!changed) {
// Nothing changed → just close overlay
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
return;
}
// Build request with all fields (server expects them)
JSONObject req = new JSONObject()
.put("action", "edit_group_info")
.put("group_id", groupId) // internal_uuid
.put("new_group_id", newGroupId) // display id
.put("name", newName);
if (newImageUrl != null && !newImageUrl.isBlank()) {
req.put("image_url", newImageUrl);
} else {
req.put("image_url", JSONObject.NULL);
}
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
} else {
String msg = (resp != null)
? resp.optString("message", "Failed to update group")
: "No response from server";
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
a.show();
}
}
}
@@ -135,7 +135,8 @@ public class NewChannelController {
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> { Platform.runLater(() -> {
createButton.setDisable(false); createButton.setDisable(false);
showToast("Create failed: " + (resp == null ? "no response" : resp.optString("message",""))); channelIdField.getStyleClass().add("error");
channelIdLabel.getStyleClass().add("error");
}); });
return; return;
} }
@@ -110,8 +110,10 @@ public class NewGroupController {
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"))) {
Platform.runLater(() -> showToast("Create failed: " + Platform.runLater(() -> {
(resp == null ? "no response" : resp.optString("message","")))); groupIdField.getStyleClass().add("error");
groupIdLabel.getStyleClass().add("error");
});
return; return;
} }
@@ -182,7 +184,4 @@ public class NewGroupController {
a.initOwner(overlayRoot.getScene().getWindow()); a.initOwner(overlayRoot.getScene().getWindow());
a.show(); a.show();
} }
} }
@@ -1188,3 +1188,99 @@
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-text-fill: #4fa8f0; /* same Telegram blue */ -fx-text-fill: #4fa8f0; /* same Telegram blue */
} }
/* Inputs */
.input-field, .input-area {
-fx-background-color: transparent;
-fx-border-width: 0 0 1 0;
-fx-border-color: #444; /* subtle underline */
-fx-text-fill: #e6e6e6; /* light text */
-fx-padding: 4 0 4 0;
-fx-font-size: 14px;
}
.input-area {
-fx-font-size: 12px;
}
/* Info rows */
.info-row-label {
-fx-font-size: 14px;
-fx-text-fill: #e6e6e6;
}
.info-row-value {
-fx-font-size: 14px;
-fx-text-fill: #4fa8f0; /* Telegram blue */
-fx-font-weight: bold;
}
/* Buttons */
.primary-btn {
-fx-background-color: #4fa8f0;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-background-radius: 6;
-fx-padding: 6 14;
}
.secondary-btn {
-fx-background-color: transparent;
-fx-border-color: #8ea1b2;
-fx-border-radius: 6;
-fx-text-fill: #e6e6e6;
-fx-padding: 6 14;
}
.circle-pic-btn {
-fx-background-color: transparent;
-fx-padding: 0;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #4fa8f0; /* brighter blue for dark mode */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #4fa8f0; /* brighter blue for dark mode */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
/* Shared */
.member-row {
-fx-alignment: CENTER_LEFT;
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2;
}
.link-btn {
-fx-background-color: transparent;
-fx-text-fill: -fx-accent;
-fx-cursor: hand;
-fx-padding: 0 6 0 6;
}
.link-btn:hover {
-fx-underline: true;
}
@@ -1178,3 +1178,87 @@
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-text-fill: #4fa8f0; /* Telegram blue */ -fx-text-fill: #4fa8f0; /* Telegram blue */
} }
/* Inputs */
.input-field, .input-area {
-fx-background-color: transparent;
-fx-border-width: 0 0 1 0;
-fx-border-color: #d0d0d0; /* light underline */
-fx-text-fill: #2c2c2c; /* dark text */
-fx-padding: 4 0 4 0;
-fx-font-size: 14px;
}
.input-area {
-fx-font-size: 12px;
}
/* Info rows */
.info-row-label {
-fx-font-size: 14px;
-fx-text-fill: #2c2c2c;
}
.info-row-value {
-fx-font-size: 14px;
-fx-text-fill: #0078d7; /* Telegram blue */
-fx-font-weight: bold;
}
/* Buttons */
.primary-btn {
-fx-background-color: #0078d7;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-background-radius: 6;
-fx-padding: 6 14;
}
.secondary-btn {
-fx-background-color: transparent;
-fx-border-color: #8ea1b2;
-fx-border-radius: 6;
-fx-text-fill: #2c2c2c;
-fx-padding: 6 14;
}
.circle-pic-btn {
-fx-background-color: transparent;
-fx-padding: 0;
}
.manage-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #0078d7; /* Telegram blue */
-fx-underline: false;
-fx-cursor: hand;
-fx-font-size: 13px;
-fx-padding: 0 4 0 4;
}
.manage-link-btn:hover {
-fx-underline: true;
}
/* Shared */
.member-row {
-fx-alignment: CENTER_LEFT;
-fx-padding: 6 0;
}
.member-name {
-fx-font-size: 14px;
-fx-font-weight: bold;
}
.member-status {
-fx-font-size: 12px;
-fx-text-fill: #8ea1b2;
}
.link-btn {
-fx-background-color: transparent;
-fx-text-fill: -fx-accent;
-fx-cursor: hand;
-fx-padding: 0 6 0 6;
}
.link-btn:hover {
-fx-underline: true;
}
@@ -19,7 +19,7 @@
<!-- ===== Header ===== --> <!-- ===== Header ===== -->
<HBox alignment="CENTER_LEFT" spacing="10" styleClass="contacts-header"> <HBox alignment="CENTER_LEFT" spacing="10" styleClass="contacts-header">
<Label text="Add Members" styleClass="contacts-title"/> <Label text="Add Subscribers" styleClass="contacts-title"/>
<!-- Member count --> <!-- Member count -->
<Label fx:id="memberCountLabel" text="0 / 200000" styleClass="member-count"/> <Label fx:id="memberCountLabel" text="0 / 200000" styleClass="member-count"/>
<Pane HBox.hgrow="ALWAYS"/> <!-- pushes elements to right --> <Pane HBox.hgrow="ALWAYS"/> <!-- pushes elements to right -->
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.Insets?>
<StackPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.ManageGroupController"
styleClass="overlay-root">
<!-- Background -->
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
<!-- Card -->
<VBox fx:id="manageGroupCard" styleClass="profile-card" spacing="16"
prefWidth="360" maxWidth="360"
prefHeight="520" maxHeight="520">
<!-- ===== Header ===== -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Edit group" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
<Pane HBox.hgrow="ALWAYS"/>
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
</HBox>
<!-- ===== Group picture + name + description ===== -->
<VBox alignment="CENTER" spacing="12">
<!-- Group picture -->
<Button fx:id="changePicButton" styleClass="circle-pic-btn">
<graphic>
<ImageView fx:id="groupImage" fitWidth="80" fitHeight="80" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Avatars/default_group_profile.png"/>
</image>
</ImageView>
</graphic>
</Button>
<!-- Group name -->
<TextField fx:id="groupNameField" promptText="Group name" styleClass="input-field"/>
<!-- Group ID -->
<TextField fx:id="groupIdField" promptText="Group ID (unique)" styleClass="input-field"/>
</VBox>
<!-- ===== Options ===== -->
<VBox spacing="10" styleClass="info-blocks" VBox.vgrow="ALWAYS">
<!-- Administrators -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Administrators" styleClass="info-row-label"/>
<Pane HBox.hgrow="ALWAYS"/>
<Label fx:id="adminCount" text="0" styleClass="info-row-value"/>
<Button fx:id="manageAdminsButton" text="Manage" styleClass="manage-link-btn"/>
</HBox>
<!-- Members -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="Members" styleClass="info-row-label"/>
<Pane HBox.hgrow="ALWAYS"/>
<Label fx:id="memberCount" text="0" styleClass="info-row-value"/>
<Button fx:id="manageMembersButton" text="Manage" styleClass="manage-link-btn"/>
</HBox>
</VBox>
<!-- ===== Footer ===== -->
<HBox spacing="10" alignment="CENTER_RIGHT">
<Button fx:id="cancelButton" text="Cancel" styleClass="secondary-btn"/>
<Button fx:id="saveButton" text="Save" styleClass="primary-btn"/>
</HBox>
</VBox>
</StackPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B