Group info UI + backend implemented.

This commit is contained in:
Asal Lotfi
2025-09-07 10:35:56 +03:30
parent f79eeb2086
commit 5f84e6af55
18 changed files with 638 additions and 32 deletions
@@ -679,4 +679,83 @@ public class GroupDatabase {
// return result;
// }
public static JSONObject getGroupInfo(UUID groupId, UUID viewerUuid) throws SQLException {
JSONObject result = new JSONObject();
try (Connection conn = ConnectionDb.connect()) {
// === Group header (name, member count, etc.)
String groupQuery = """
SELECT g.internal_uuid, g.group_id, g.group_name, g.image_url,
COUNT(m.user_id) as member_count
FROM groups g
LEFT JOIN group_members m ON g.internal_uuid = m.group_id
WHERE g.internal_uuid = ?
GROUP BY g.internal_uuid, g.group_id, g.group_name, g.image_url
""";
try (PreparedStatement ps = conn.prepareStatement(groupQuery)) {
ps.setObject(1, groupId); // ✅ compare UUID with UUID
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("internal_uuid", rs.getString("internal_uuid"));
result.put("group_id", rs.getString("group_id")); // varchar handle
result.put("group_name", rs.getString("group_name"));
result.put("member_count", rs.getInt("member_count"));
result.put("image_url", rs.getString("image_url"));
} else {
return null; // no such group
}
}
// === Members (id, name, role, status, image)
JSONArray membersArr = new JSONArray();
String membersQuery = """
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
gm.role, u.status, u.last_seen
FROM group_members gm
JOIN users u ON gm.user_id = u.internal_uuid
WHERE gm.group_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(membersQuery)) {
ps.setObject(1, groupId); // ✅ gm.group_id is UUID
ResultSet rs = ps.executeQuery();
while (rs.next()) {
JSONObject member = new JSONObject();
member.put("user_id", rs.getString("internal_uuid"));
member.put("profile_name", rs.getString("profile_name"));
member.put("username", rs.getString("user_id"));
member.put("image_url", rs.getString("image_url"));
member.put("role", rs.getString("role"));
member.put("status", rs.getString("status"));
member.put("last_seen", rs.getString("last_seen"));
membersArr.put(member);
}
}
result.put("members", membersArr);
// === Viewer role (to decide if they can delete group, etc.)
String roleQuery = """
SELECT role
FROM group_members
WHERE group_id = ? AND user_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
ps.setObject(1, groupId);
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 member
}
}
}
return result;
}
}
@@ -2997,7 +2997,29 @@ public class ClientHandler implements Runnable {
break;
}
case "view_group": {
if (currentUser == null) {
response = new ResponseModel("error", "Unauthorized. Please login first.");
break;
}
try {
UUID groupId = UUID.fromString(requestJson.getString("group_id"));
UUID viewerId = UUID.fromString(requestJson.getString("viewer_id"));
// Query group details
JSONObject groupData = GroupDatabase.getGroupInfo(groupId, viewerId);
if (groupData == null) {
response = new ResponseModel("error", "Group not found.");
} else {
response = new ResponseModel("success", "Group info fetched.", groupData);
}
} catch (Exception e) {
response = new ResponseModel("error", "Error processing group info: " + e.getMessage());
}
break;
}
default:
@@ -51,7 +51,7 @@ public class BlockedUsersController {
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal));
updateBackIcon(ThemeManager.getInstance().isDarkMode());
// اسکرول نرم
// Smooth scroll feel
blockedList.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
@@ -954,28 +954,38 @@ public class ChatPageController {
}
private void configureHeaderActions(ChatEntry entry) {
chatMoreMenu.getItems().clear();
// Hide everything by default
archiveItem.setVisible(false);
viewProfileItem.setVisible(false);
deleteChatItem.setVisible(false);
viewGroupItem.setVisible(false);
leaveGroupItem.setVisible(false);
viewChannelItem.setVisible(false);
leaveChannelItem.setVisible(false);
switch (entry.getType().toLowerCase(Locale.ROOT)) {
case "private" -> {
archiveItem.setVisible(true);
viewProfileItem.setVisible(true);
deleteChatItem.setVisible(true);
archiveItem.setOnAction(e -> toggleArchive(entry));
viewProfileItem.setOnAction(e -> openInfoScene(entry));
deleteChatItem.setOnAction(e -> deleteChatButton(entry));
chatMoreMenu.getItems().addAll(archiveItem, viewProfileItem, deleteChatItem);
}
case "group" -> {
MenuItem viewGroup = new MenuItem("View group info");
viewGroup.setOnAction(e -> openInfoScene(entry));
MenuItem leaveGroup = new MenuItem("Leave group");
leaveGroup.setOnAction(e -> leaveGroupButton(entry));
chatMoreMenu.getItems().addAll(viewGroup, leaveGroup);
viewGroupItem.setVisible(true);
leaveGroupItem.setVisible(true);
viewGroupItem.setOnAction(e -> openInfoScene(entry));
leaveGroupItem.setOnAction(e -> leaveGroupButton(entry));
}
case "channel" -> {
MenuItem viewChannel = new MenuItem("View channel info");
viewChannel.setOnAction(e -> openInfoScene(entry));
MenuItem leaveChannel = new MenuItem("Leave channel");
leaveChannel.setOnAction(e -> leaveChannelButton(entry));
chatMoreMenu.getItems().addAll(viewChannel, leaveChannel);
viewChannelItem.setVisible(true);
leaveChannelItem.setVisible(true);
viewChannelItem.setOnAction(e -> openInfoScene(entry));
leaveChannelItem.setOnAction(e -> leaveChannelButton(entry));
}
}
@@ -1027,7 +1037,8 @@ public class ChatPageController {
}
case "group" -> {
req.put("action", "view_group")
.put("group_id", entry.getId().toString());
.put("group_id", entry.getId().toString())
.put("viewer_id", Session.getUserUUID());
}
case "channel" -> {
req.put("action", "view_channel")
@@ -3036,6 +3047,14 @@ public class ChatPageController {
if (deleteChatItem != null && deleteChatItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("delete_red.png")); // stays red in both themes
}
if (viewGroupItem != null && viewGroupItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("group" + suffix));
}
if (viewChannelItem != null && viewChannelItem.getGraphic() instanceof ImageView iv) {
iv.setImage(loadIcon("group" + suffix));
}
}
// ChatPageController
@@ -1,11 +1,249 @@
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.Models.ChatEntry;
import java.io.IOException;
import java.net.URL;
public class GroupInfoController {
@FXML private VBox groupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private ImageView groupImage;
@FXML private Label groupName;
@FXML private Label memberCount;
@FXML private Button infoMoreButton;
@FXML private ContextMenu infoMoreMenu;
@FXML private MenuItem addMemberItem;
@FXML private MenuItem manageGroupItem;
@FXML private MenuItem deleteGroupItem;
@FXML private ImageView moreIcon;
@FXML private VBox membersList;
@FXML private Label membersHeader;
@FXML private Button addMemberButton;
@FXML private ImageView membersIcon;
@FXML private ScrollPane membersScroll;
private String groupId; // the UUID of this group
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML
private void initialize() {
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
infoMoreButton.setOnAction(e -> {
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
});
deleteGroupItem.setOnAction(e -> handleDeleteGroup());
Platform.runLater(() -> {
if (groupCard.getScene() != null) {
ThemeManager.getInstance().registerScene(groupCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateIcons(newVal);
});
updateIcons(ThemeManager.getInstance().isDarkMode());
// Smooth scroll feel
membersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
membersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
ScrollBar vBar = (ScrollBar) membersScroll.lookup(".scroll-bar:vertical");
if (vBar != null) {
membersScroll.setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
double newValue = vBar.getValue() - deltaY;
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
});
}
}
});
if (addMemberButton != null) {
addMemberButton.setOnAction(e -> openAddMemberScene());
}
if (addMemberItem != null) {
addMemberItem.setOnAction(e -> openAddMemberScene());
}
}
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
// read name, status, bio, image url from JSON
// update labels/images accordingly
this.groupId = data.optString("internal_uuid", entry.getId().toString());
groupName.setText(data.optString("group_name", entry.getName()));
memberCount.setText(data.optInt("member_count", 0) + " members");
// show/hide Delete button if user is owner
String myRole = data.optString("my_role", "member");
if ("owner".equalsIgnoreCase(myRole)) {
deleteGroupItem.setVisible(true);
} else {
deleteGroupItem.setVisible(false);
}
// Group picture
String imgUrl = data.optString("image_url", "");
if (!imgUrl.isBlank()) {
try {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) groupImage.setImage(img);
} catch (Exception ignore) {}
} else {
groupImage.setImage(
new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))
);
}
// Members
membersList.getChildren().clear();
var arr = data.optJSONArray("members");
if (arr != null) {
membersHeader.setText(arr.length() + " MEMBERS");
for (int i = 0; i < arr.length(); i++) {
JSONObject m = arr.getJSONObject(i);
addMemberRow(m);
}
}
}
private void addMemberRow(JSONObject m) {
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 = m.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(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
// Status = online / last seen recently
String status;
if (m.optBoolean("is_online", false)) {
status = "online";
} else {
status = ChatPageController.getInstance().userStatusText(
false,
m.optString("last_seen", null)
);
}
Label statusLbl = new Label(status);
statusLbl.getStyleClass().add("member-status");
nameBox.getChildren().addAll(name, statusLbl);
// === Role (owner/admin/member) ===
Label role = new Label();
String roleStr = m.optString("role", "");
if (!roleStr.isBlank()) {
role.setText(roleStr.toLowerCase());
role.getStyleClass().add("member-role");
}
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, nameBox, spacer, role);
membersList.getChildren().add(row);
}
private void openAddMemberScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
Node overlay = loader.load();
AddMembersController controller = loader.getController();
// 👇 Pass group id so AddMember scene knows which group to add to
controller.setGroupForAdd(groupId, groupName);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Member scene.",
Alert.AlertType.ERROR
);
}
}
private void handleDeleteGroup() {
System.out.println("Deleting group...");
// TODO: implement backend call
}
private void updateIcons(boolean dark) {
String suffix = dark ? "_light.png" : "_dark.png";
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
}
// --- helpers -------------------------------------------------------------
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());
}
}
@@ -63,7 +63,7 @@ public class NewChannelController {
createButton.setOnAction(e -> onCreateChannel());
// محدودیت توضیح
// Limit description
final int MAX_LENGTH = 255;
channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> {
if (channelDescField.getText().length() >= MAX_LENGTH) e.consume();
@@ -79,7 +79,7 @@ public class NewChannelController {
});
descCounter.setText("0 / 255");
// پاک کردن استایل خطا هنگام تایپ
// Reset error when user types again
channelNameField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) {
channelNameField.getStyleClass().remove("error");