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 @@ - + @@ -45,7 +45,7 @@ - +