Manage Channel UI + backend implemented.

This commit is contained in:
Asal Lotfi
2025-09-08 11:55:09 +03:30
parent 730ddba857
commit 4a4e646940
9 changed files with 826 additions and 1 deletions
@@ -208,7 +208,62 @@ public class ChannelInfoController {
}
private void openManageChannel() {
// TODO similar to ManageGroupController
new Thread(() -> {
try {
JSONObject req = new JSONObject()
.put("action", "view_channel")
.put("channel_id", channelId.toString()); // ✅ matches server case
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_channel.fxml"));
Node overlay = loader.load();
ManageChannelController controller = loader.getController();
controller.setChannelData(data); // ✅ pass JSON to controller
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Manage Channel scene.",
Alert.AlertType.ERROR
);
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Error while fetching channel info: " + e.getMessage(),
Alert.AlertType.ERROR
));
}
}).start();
}
private void openAddSubscriberScene() {
@@ -0,0 +1,228 @@
package org.to.telegramfinalproject.UI;
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.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ManageChannelAdminsController {
@FXML private VBox adminsCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addAdminButton;
@FXML private ScrollPane adminsScroll;
@FXML private VBox adminsList;
private String channelId; // internal_uuid of channel
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
//addAdminButton.setOnAction(e -> openAddAdminOverlay());
// Smooth scroll feel
adminsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
adminsScroll.setPannable(true);
adminsScroll.setFitToWidth(true);
adminsScroll.setFitToHeight(false);
adminsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
adminsScroll.setVvalue(adminsScroll.getVvalue() - deltaY);
});
}
public void setChannelData(String channelId, JSONArray admins) {
this.channelId = channelId;
adminsList.getChildren().clear();
// Convert to list for sorting
List<JSONObject> adminList = new ArrayList<>();
for (int i = 0; i < admins.length(); i++) {
adminList.add(admins.getJSONObject(i));
}
// Sort: owner first, then admins
adminList.sort((a, b) -> {
String roleA = a.optString("role", "subscriber");
String roleB = b.optString("role", "subscriber");
if ("owner".equalsIgnoreCase(roleA) && !"owner".equalsIgnoreCase(roleB)) return -1;
if ("owner".equalsIgnoreCase(roleB) && !"owner".equalsIgnoreCase(roleA)) return 1;
return 0; // keep relative order for admins
});
// Add rows
for (JSONObject a : adminList) {
addAdminRow(a);
}
}
private void addAdminRow(JSONObject a) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = a.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(a.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
a.optBoolean("is_online", false) ? "online"
: "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Remove admin (cannot remove owner)
String role = a.optString("role", "admin");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeAdmin(a.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
adminsList.getChildren().add(row);
// User clicks an admin → open set channel admin permissions scene
row.setOnMouseClicked(e -> {
if (!"owner".equalsIgnoreCase(role)) {
try {
// request current permissions from server
JSONObject req = new JSONObject()
.put("action", "get_channel_admin_permissions")
.put("channel_id", channelId)
.put("admin_id", a.optString("user_id"));
JSONObject resp = ActionHandler.sendWithResponse(req);
JSONObject oldPerms = (resp != null) ? resp.optJSONObject("permissions") : null;
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/set_channel_admin_permissions.fxml"));
Node overlay = loader.load();
// SetChannelAdminPermissionsController controller = loader.getController();
// controller.setTarget(
// UUID.fromString(channelId),
// UUID.fromString(a.optString("user_id")),
// true,
// oldPerms
// );
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
MainController.getInstance().showAlert("Error", "Could not open permissions scene.", Alert.AlertType.ERROR);
}
}
});
}
private void removeAdmin(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_admin_from_channel")
.put("channel_id", channelId)
.put("target_user_id", userId);
System.out.println("Sending remove_admin_from_channel request: " + req.toString(2));
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> adminsList.getChildren().remove(row));
} else {
Platform.runLater(() -> {
row.setDisable(false);
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove admin.") : "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
// private void openAddAdminOverlay() {
// try {
// // === Query the server for full subscribers list ===
// JSONObject req = new JSONObject()
// .put("action", "view_channel_subscribers")
// .put("channel_id", channelId);
//
// JSONObject resp = ActionHandler.sendWithResponse(req);
//
// if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
// MainController.getInstance().showAlert(
// "Error",
// "Could not fetch subscribers.",
// Alert.AlertType.ERROR
// );
// return;
// }
//
// JSONObject data = resp.optJSONObject("data");
// JSONArray subscribers = (data != null) ? data.optJSONArray("subscribers") : new JSONArray();
//
// // === Load Add Admins FXML (same FXML reused) ===
// FXMLLoader loader = new FXMLLoader(getClass().getResource(
// "/org/to/telegramfinalproject/Fxml/add_admins.fxml"));
// Node overlay = loader.load();
//
// AddChannelAdminsController controller = loader.getController();
// controller.setChannelData(channelId, subscribers); // send raw subscribers, filtering is done in AddChannelAdminsController
//
// MainController.getInstance().showOverlay(overlay);
//
// } catch (IOException e) {
// e.printStackTrace();
// MainController.getInstance().showAlert(
// "Error", "Could not load Add Admins scene.", Alert.AlertType.ERROR
// );
// }
// }
}
@@ -0,0 +1,180 @@
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.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
public class ManageChannelController {
@FXML private VBox manageChannelCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
@FXML private ImageView channelImage;
@FXML private TextField channelNameField;
@FXML private TextField channelIdField;
@FXML private TextArea channelDescriptionField;
@FXML private Label adminCount, subscriberCount;
@FXML private Button manageAdminsButton;
@FXML private Button manageSubscribersButton;
private String channelId; // Internal UUID of the channel
private String originalName;
private String originalDisplayId;
private String originalImageUrl;
private String originalDescription;
private File selectedImageFile;
private JSONObject data;
@FXML
public void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
changePicButton.setOnAction(e -> choosePicture());
saveButton.setOnAction(e -> saveChanges());
manageAdminsButton.setOnAction(e -> openManageAdminsScene(data));
manageSubscribersButton.setOnAction(e -> openManageSubscribersScene(data));
}
public void setChannelData(JSONObject data) {
this.data = data;
this.channelId = data.optString("internal_uuid");
originalName = data.optString("channel_name", "");
originalDisplayId = data.optString("channel_id", "");
originalImageUrl = data.optString("image_url", "");
originalDescription = data.optString("description", "");
channelNameField.setText(originalName);
channelIdField.setText(originalDisplayId);
channelDescriptionField.setText(originalDescription);
if (!originalImageUrl.isBlank()) {
channelImage.setImage(new Image(originalImageUrl, true));
}
adminCount.setText(String.valueOf(countRole(data, "admin") + 1)); // owner + admins
subscriberCount.setText(String.valueOf(countRole(data, "subscriber") + Integer.parseInt(adminCount.getText())));
}
private int countRole(JSONObject channelData, String role) {
var arr = channelData.optJSONArray("subscribers");
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;
}
private void choosePicture() {
FileChooser fc = new FileChooser();
fc.setTitle("Select channel picture");
selectedImageFile = fc.showOpenDialog(manageChannelCard.getScene().getWindow());
if (selectedImageFile != null) {
channelImage.setImage(new Image(selectedImageFile.toURI().toString()));
}
}
private void saveChanges() {
String newName = channelNameField.getText().trim();
String newChannelId = channelIdField.getText().trim();
String newDescription = channelDescriptionField.getText().trim();
String newImageUrl = (selectedImageFile != null)
? selectedImageFile.toURI().toString()
: originalImageUrl;
boolean changed =
!Objects.equals(originalName, newName) ||
!Objects.equals(originalDisplayId, newChannelId) ||
!Objects.equals(originalDescription, newDescription) ||
!Objects.equals(originalImageUrl, newImageUrl);
if (!changed) {
MainController.getInstance().closeOverlay(manageChannelCard.getParent());
return;
}
JSONObject req = new JSONObject()
.put("action", "edit_channel_info")
.put("channel_id", channelId) // internal UUID
.put("new_channel_id", newChannelId)
.put("name", newName)
.put("description", newDescription);
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(manageChannelCard.getParent());
} else {
String msg = (resp != null)
? resp.optString("message", "Failed to update channel")
: "No response from server";
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
a.show();
}
}
private void openManageAdminsScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_channel_admins.fxml"));
Node overlay = loader.load();
ManageChannelAdminsController controller = loader.getController();
// Pass channelId and subscribers list from JSON
controller.setChannelData(
data.optString("internal_uuid"),
data.optJSONArray("subscribers")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Channel Admins scene.", Alert.AlertType.ERROR
);
}
}
private void openManageSubscribersScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_subscribers.fxml"));
Node overlay = loader.load();
ManageSubscribersController controller = loader.getController();
// Pass channelId and subscribers list from the JSON data
controller.setChannelData(
data.optString("internal_uuid"),
data.optJSONArray("subscribers")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Subscribers scene.", Alert.AlertType.ERROR);
}
}
}
@@ -0,0 +1,160 @@
package org.to.telegramfinalproject.UI;
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.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.UUID;
public class ManageSubscribersController {
@FXML private VBox subscribersCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addSubscribersButton;
@FXML private ScrollPane subscribersScroll;
@FXML private VBox subscribersList;
private String channelId; // internal UUID
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
addSubscribersButton.setOnAction(e -> openAddSubscribersOverlay(channelId));
// Smooth scroll
subscribersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
subscribersScroll.setPannable(true);
subscribersScroll.setFitToWidth(true);
subscribersScroll.setFitToHeight(false);
subscribersScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
subscribersScroll.setVvalue(subscribersScroll.getVvalue() - deltaY);
});
}
public void setChannelData(String channelId, JSONArray subscribers) {
this.channelId = channelId;
subscribersList.getChildren().clear();
for (int i = 0; i < subscribers.length(); i++) {
Object raw = subscribers.get(i);
JSONObject sub = (raw instanceof JSONObject)
? (JSONObject) raw
: new JSONObject((java.util.Map<?, ?>) raw);
addSubscriberRow(sub);
}
}
private void addSubscriberRow(JSONObject sub) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = sub.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(sub.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
sub.optBoolean("is_online", false) ? "online" : "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Only non-owners can be removed
String role = sub.optString("role", "subscriber");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeSubscriber(sub.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
subscribersList.getChildren().add(row);
}
private void removeSubscriber(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_subscriber_from_channel")
.put("channel_id", channelId)
.put("user_id", userId);
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> subscribersList.getChildren().remove(row));
} else {
Platform.runLater(() -> {
row.setDisable(false);
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove subscriber.")
: "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
private void openAddSubscribersOverlay(String channelId) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml"));
Node overlay = loader.load();
AddSubscriberController controller = loader.getController();
controller.setChannelInfo(UUID.fromString(channelId), "", "", null, "");
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Add Subscribers scene.", Alert.AlertType.ERROR
);
}
}
}