diff --git a/build.gradle b/build.gradle
index 5707d6c..f3bf6b5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,7 +26,7 @@ tasks.withType(JavaCompile) {
application {
mainModule = 'org.to.telegramfinalproject'
- mainClass = 'org.to.telegramfinalproject.HelloApplication'
+ mainClass = 'org.to.telegramfinalproject.Server.MainServer'
}
javafx {
diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java
index 23b57d6..c908fef 100644
--- a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java
+++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java
@@ -691,4 +691,83 @@ public class ChannelDatabase {
}
}
+ public static JSONObject getChannelInfo(UUID channelId, UUID viewerUuid) throws SQLException {
+ JSONObject result = new JSONObject();
+
+ try (Connection conn = ConnectionDb.connect()) {
+ // === Channel header (name, subscriber count, etc.)
+ String channelQuery = """
+ SELECT c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description,
+ COUNT(s.user_id) as subscriber_count
+ FROM channels c
+ LEFT JOIN channel_subscribers s ON c.internal_uuid = s.channel_id
+ WHERE c.internal_uuid = ?
+ GROUP BY c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(channelQuery)) {
+ ps.setObject(1, channelId);
+ ResultSet rs = ps.executeQuery();
+ if (rs.next()) {
+ result.put("internal_uuid", rs.getString("internal_uuid"));
+ result.put("channel_id", rs.getString("channel_id")); // varchar if needed
+ result.put("channel_name", rs.getString("channel_name"));
+ result.put("subscriber_count", rs.getInt("subscriber_count"));
+ result.put("image_url", rs.getString("image_url"));
+ result.put("description", rs.getString("description")); // ✅ here
+ } else {
+ return null; // no such channel
+ }
+ }
+
+ // === Subscribers (id, name, role, status, image)
+ JSONArray subscribersArr = new JSONArray();
+
+ String subscribersQuery = """
+ SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
+ cs.role, u.status, u.last_seen
+ FROM channel_subscribers cs
+ JOIN users u ON cs.user_id = u.internal_uuid
+ WHERE cs.channel_id = ?
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(subscribersQuery)) {
+ ps.setObject(1, channelId);
+ ResultSet rs = ps.executeQuery();
+ while (rs.next()) {
+ JSONObject sub = new JSONObject();
+ sub.put("user_id", rs.getString("internal_uuid"));
+ sub.put("profile_name", rs.getString("profile_name"));
+ sub.put("username", rs.getString("user_id"));
+ sub.put("image_url", rs.getString("image_url"));
+ sub.put("role", rs.getString("role"));
+ sub.put("status", rs.getString("status"));
+ sub.put("last_seen", rs.getString("last_seen"));
+ subscribersArr.put(sub);
+ }
+ }
+
+ result.put("subscribers", subscribersArr);
+
+ // === Viewer role (so UI knows if viewer can manage/delete)
+ String roleQuery = """
+ SELECT role
+ FROM channel_subscribers
+ WHERE channel_id = ? AND user_id = ?
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
+ ps.setObject(1, channelId);
+ ps.setObject(2, viewerUuid);
+ ResultSet rs = ps.executeQuery();
+ if (rs.next()) {
+ result.put("my_role", rs.getString("role"));
+ } else {
+ result.put("my_role", ""); // not a subscriber
+ }
+ }
+ }
+
+ return result;
+ }
}
diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
index d40f564..ba79729 100644
--- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
+++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
@@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Server;
import javafx.geometry.Side;
import org.json.JSONArray;
import org.json.JSONObject;
+import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Database.*;
import org.to.telegramfinalproject.Models.*;
import org.to.telegramfinalproject.Security.PasswordHashing;
@@ -3061,6 +3062,30 @@ public class ClientHandler implements Runnable {
break;
}
+ case "view_channel": {
+ if (currentUser == null) {
+ response = new ResponseModel("error", "Unauthorized. Please login first.");
+ break;
+ }
+
+ try {
+ UUID channelId = UUID.fromString(requestJson.getString("channel_id"));
+ UUID viewerId = currentUser.getInternal_uuid(); // logged-in user
+
+ // Query channel details
+ JSONObject channelData = ChannelDatabase.getChannelInfo(channelId, viewerId);
+
+ if (channelData == null) {
+ response = new ResponseModel("error", "Channel not found.");
+ } else {
+ response = new ResponseModel("success", "Channel info fetched.", channelData);
+ }
+ } catch (Exception e) {
+ response = new ResponseModel("error", "Error processing channel info: " + e.getMessage());
+ }
+ break;
+ }
+
default:
response = new ResponseModel("error", "Unknown action: " + action);
diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChannelInfoController.java b/src/main/java/org/to/telegramfinalproject/UI/ChannelInfoController.java
index 1f48397..8366f0e 100644
--- a/src/main/java/org/to/telegramfinalproject/UI/ChannelInfoController.java
+++ b/src/main/java/org/to/telegramfinalproject/UI/ChannelInfoController.java
@@ -1,11 +1,267 @@
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.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;
+import java.io.IOException;
+import java.net.URL;
+import java.util.UUID;
+
public class ChannelInfoController {
+
+ @FXML private VBox channelCard;
+ @FXML private Pane overlayBackground;
+ @FXML private Button closeButton;
+
+ @FXML private ImageView channelAvatar;
+ @FXML private Label channelName;
+ @FXML private Label subscriberCount;
+ @FXML private Label channelDescription;
+
+ @FXML private Button infoMoreButton;
+ @FXML private ContextMenu infoMoreMenu;
+ @FXML private MenuItem addMembersBtn;
+ @FXML private MenuItem manageChannelBtn;
+ @FXML private MenuItem deleteChannelBtn;
+ @FXML private ImageView moreIcon;
+ @FXML private ImageView manageChannelIcon;
+ @FXML private ImageView addMemberIcon;
+
+ @FXML private VBox subscribersList;
+ @FXML private Label subscribersHeader;
+ @FXML private Button addSubscriberButton;
+ @FXML private ImageView subscribersIcon;
+ @FXML private ScrollPane subscribersScroll;
+
+ private UUID channelId;
+ private String myRole;
+
+ private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
+
+ @FXML
+ private void initialize() {
+ closeButton.setOnAction(e ->
+ MainController.getInstance().closeOverlay(channelCard.getParent()));
+ overlayBackground.setOnMouseClicked(e ->
+ MainController.getInstance().closeOverlay(channelCard.getParent()));
+
+ infoMoreButton.setOnAction(e -> {
+ if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
+ });
+
+ if (manageChannelBtn != null) {
+ manageChannelBtn.setOnAction(e -> openManageChannel());
+ }
+ deleteChannelBtn.setOnAction(e -> handleDeleteChannel());
+
+ Platform.runLater(() -> {
+ if (channelCard.getScene() != null) {
+ ThemeManager.getInstance().registerScene(channelCard.getScene());
+ }
+ });
+
+ ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
+ updateIcons(newVal);
+ });
+
+ updateIcons(ThemeManager.getInstance().isDarkMode());
+
+ // Smooth scroll feel
+ subscribersScroll.getStylesheets().add(
+ getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
+ );
+ subscribersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
+ if (newSkin != null) {
+ ScrollBar vBar = (ScrollBar) subscribersScroll.lookup(".scroll-bar:vertical");
+ if (vBar != null) {
+ subscribersScroll.setOnScroll(event -> {
+ double deltaY = event.getDeltaY() * 0.003;
+ double newValue = vBar.getValue() - deltaY;
+ vBar.setValue(Math.max(0, Math.min(newValue, 1)));
+ });
+ }
+ }
+ });
+
+ if (addSubscriberButton != null) {
+ addSubscriberButton.setOnAction(e -> openAddSubscriberScene());
+ }
+ if (addMembersBtn != null) {
+ addMembersBtn.setOnAction(e -> openAddSubscriberScene());
+ }
+ }
+
public void setChannelDataFromJson(ChatEntry entry, JSONObject data) {
- // read name, status, bio, image url from JSON
- // update labels/images accordingly
+ this.channelId = entry.getId();
+
+ channelName.setText(data.optString("channel_name", entry.getName()));
+ subscriberCount.setText(data.optInt("subscriber_count", 0) + " subscribers");
+ channelDescription.setText(data.optString("description", ""));
+
+ // --- Role-based UI ---
+ myRole = data.optString("my_role", "subscriber").toLowerCase();
+
+ deleteChannelBtn.setVisible("owner".equals(myRole));
+
+ boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
+ addSubscriberButton.setVisible(canAdd);
+ addSubscriberButton.setManaged(canAdd);
+ if (addMembersBtn != null) {
+ addMembersBtn.setVisible(canAdd);
+ }
+
+ boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
+ infoMoreButton.setVisible(showMore);
+ infoMoreButton.setManaged(showMore);
+
+ // --- Avatar ---
+ String imgUrl = data.optString("image_url", "");
+ if (!imgUrl.isBlank()) {
+ try {
+ Image img = AvatarLocalResolver.load(imgUrl);
+ if (img != null) channelAvatar.setImage(img);
+ } catch (Exception ignore) {}
+ } else {
+ channelAvatar.setImage(new Image(
+ getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_channel_profile.png")
+ ));
+ }
+
+ // --- Subscribers list ---
+ subscribersList.getChildren().clear();
+ var arr = data.optJSONArray("subscribers");
+ if (arr != null) {
+ subscribersHeader.setText(arr.length() + " SUBSCRIBERS");
+ for (int i = 0; i < arr.length(); i++) {
+ JSONObject s = arr.getJSONObject(i);
+ addSubscriberRow(s);
+ }
+ }
+ }
+
+ private void addSubscriberRow(JSONObject s) {
+ 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);
+ AvatarFX.circleClip(avatar, 36);
+
+ String imgUrl = s.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 nameBox = new VBox(2);
+ Label name = new Label(s.optString("profile_name", "Unknown"));
+ name.getStyleClass().add("member-name");
+
+ String status;
+ if (s.optBoolean("is_online", false)) {
+ status = "online";
+ } else {
+ status = ChatPageController.getInstance().userStatusText(
+ false,
+ s.optString("last_seen", null)
+ );
+ }
+ Label statusLbl = new Label(status);
+ statusLbl.getStyleClass().add("member-status");
+
+ nameBox.getChildren().addAll(name, statusLbl);
+
+ // Role
+ Label roleLbl = new Label();
+ String roleStr = s.optString("role", "");
+ if (!roleStr.isBlank()) {
+ roleLbl.setText(roleStr.toLowerCase());
+ roleLbl.getStyleClass().add("member-role");
+ }
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ row.getChildren().addAll(avatar, nameBox, spacer, roleLbl);
+ subscribersList.getChildren().add(row);
+ }
+
+ private void openManageChannel() {
+ // TODO similar to ManageGroupController
+ }
+
+ private void openAddSubscriberScene() {
+ try {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource(
+ "/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); // point to new fxml
+ Node overlay = loader.load();
+
+ AddSubscriberController controller = loader.getController();
+ controller.setChannelInfo(channelId, channelName.getText(), "", null, "");
+ // passing UUID + name, other fields optional
+
+ MainController.getInstance().showOverlay(overlay);
+ } catch (IOException e) {
+ e.printStackTrace();
+ MainController.getInstance().showAlert(
+ "Error",
+ "Could not load Add Subscribers scene.",
+ Alert.AlertType.ERROR
+ );
+ }
+ }
+
+ private void handleDeleteChannel() {
+ System.out.println("Deleting channel...");
+ // TODO: implement backend call
+ }
+
+ private void updateIcons(boolean dark) {
+ String suffix = dark ? "_light.png" : "_dark.png";
+
+ moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
+ subscribersIcon.setImage(loadImage(ICON_PATH + "channel_subscriber" + suffix));
+ addSubscriberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
+ manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
+ addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
+ }
+
+ private ImageView makeIcon(String path) {
+ ImageView iv = new ImageView();
+ Image img = loadImage(path);
+ if (img != null) {
+ iv.setImage(img);
+ iv.setFitWidth(22);
+ iv.setFitHeight(22);
+ iv.setPreserveRatio(true);
+ }
+ return iv;
+ }
+
+ private Image loadImage(String path) {
+ URL res = getClass().getResource(path);
+ if (res == null) return null;
+ return new Image(res.toExternalForm());
}
}
diff --git a/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java b/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java
index 237f535..3632849 100644
--- a/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java
+++ b/src/main/java/org/to/telegramfinalproject/UI/GroupInfoController.java
@@ -35,6 +35,8 @@ public class GroupInfoController {
@FXML private MenuItem manageGroupItem;
@FXML private MenuItem deleteGroupItem;
@FXML private ImageView moreIcon;
+ @FXML private ImageView manageChannelIcon;
+ @FXML private ImageView addMemberIcon;
@FXML private VBox membersList;
@FXML private Label membersHeader;
@@ -335,7 +337,8 @@ public class GroupInfoController {
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
-
+ manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
+ addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
}
// --- helpers -------------------------------------------------------------
diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css
index bacf34a..d68630e 100644
--- a/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css
+++ b/src/main/resources/org/to/telegramfinalproject/CSS/dark_theme.css
@@ -1410,3 +1410,34 @@
-fx-border-color: #555;
-fx-text-fill: #aaa;
}
+
+.channel-info-card {
+ -fx-background-color: #2a2a2a;
+}
+
+.channel-title {
+ -fx-text-fill: #f5f5f5;
+}
+
+.channel-subcount {
+ -fx-text-fill: #aaaaaa;
+}
+
+.channel-buttons .button {
+ -fx-background-color: #3c3c3c;
+ -fx-text-fill: #f5f5f5;
+}
+
+.options-button {
+ -fx-text-fill: #dddddd;
+}
+
+.subscriber-item,
+.admin-item {
+ -fx-text-fill: #e0e0e0;
+}
+
+.info-description {
+ -fx-font-size: 12px;
+ -fx-text-fill: #777;
+}
diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css
index 31456ff..f1601a3 100644
--- a/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css
+++ b/src/main/resources/org/to/telegramfinalproject/CSS/light_theme.css
@@ -1387,3 +1387,8 @@
-fx-border-color: #ccc;
-fx-text-fill: #222;
}
+
+.info-description {
+ -fx-font-size: 12px;
+ -fx-text-fill: #777;
+}
diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/channel_info.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/channel_info.fxml
index f3b9457..37c2691 100644
--- a/src/main/resources/org/to/telegramfinalproject/Fxml/channel_info.fxml
+++ b/src/main/resources/org/to/telegramfinalproject/Fxml/channel_info.fxml
@@ -1,14 +1,122 @@
-
-
-
-
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/group_info.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/group_info.fxml
index a962a03..1139eaf 100644
--- a/src/main/resources/org/to/telegramfinalproject/Fxml/group_info.fxml
+++ b/src/main/resources/org/to/telegramfinalproject/Fxml/group_info.fxml
@@ -34,7 +34,7 @@