diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 3fefe32..09b6964 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -54,6 +54,15 @@ public class ChatEntry { } + public ChatEntry(UUID internalId, String type, String name, String displayId) { + this.internalId = internalId; + this.type = type; + this.displayId = displayId; + this.name = name; + + } + + public boolean isOwner() { return isOwner; } @@ -74,6 +83,7 @@ public class ChatEntry { return internalId; } + public String getDisplayId() { return displayId; } @@ -180,4 +190,21 @@ public class ChatEntry { this.lastMessageTime = t; } + + + public static ChatEntry fromServer(UUID internalId, + String type, + String name, + String displayId, + String imageUrl, + boolean isOwner, + boolean isAdmin) { + ChatEntry e = new ChatEntry(internalId, type, name, displayId); // اگر سازنده‌ات فرق دارد، مطابق آن بساز + e.setImageUrl(imageUrl); + e.setOwner(isOwner); + e.setAdmin(isAdmin); + return e; + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 84ba9c3..10f6aa2 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -887,24 +887,28 @@ public class ClientHandler implements Runnable { break; } try { - String channelId = requestJson.getString("channel_id"); + String channelId = requestJson.getString("channel_id"); String channelName = requestJson.getString("channel_name"); - String userIdStr = requestJson.getString("user_id"); - String imageUrl = requestJson.optString("image_url", null); + String userIdStr = requestJson.getString("user_id"); + String imageUrl = requestJson.optString("image_url", null); + String description = requestJson.optString("description", null); // اختیاری UUID creatorUUID = UUID.fromString(userIdStr); + // اگر سرویس‌ات ورودی توضیح را می‌پذیرد، از متد اورلودشده استفاده کن: + // boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl, description); boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl); if (created) { Channel createdChannel = ChannelDatabase.findByChannelId(channelId); if (createdChannel != null) { - JSONObject data = new JSONObject(); + org.json.JSONObject data = new org.json.JSONObject(); data.put("internal_id", createdChannel.getInternal_uuid().toString()); data.put("id", createdChannel.getChannel_id()); data.put("name", createdChannel.getChannel_name()); data.put("image_url", createdChannel.getImage_url()); data.put("type", "channel"); + if (description != null) data.put("description", description); response = new ResponseModel("success", "Channel created.", data); } else { @@ -1163,44 +1167,104 @@ public class ClientHandler implements Runnable { } - case "add_member_to_group": { +// case "add_member_to_group": { +// if (currentUser == null) { +// response = new ResponseModel("error", "Unauthorized. Please login first."); +// break; +// } +// UUID groupId = UUID.fromString(requestJson.getString("group_id")); +// UUID targetUserId = UUID.fromString(requestJson.getString("user_id")); +// +// if (!GroupPermissionUtil.canAddMembers(groupId, currentUser.getInternal_uuid())) { +// response = new ResponseModel("error", "You are not allowed to add members."); +// break; +// } +// +// if (GroupDatabase.isUserInGroup(targetUserId, groupId)) { +// response = new ResponseModel("error", "User is already a member."); +// break; +// } +// +// boolean success = GroupDatabase.addMemberToGroup(targetUserId, groupId); +// +// Group group = GroupDatabase.findByInternalUUID(groupId); +// +// //RealTime +// if (success && group != null) { +// RealTimeEventDispatcher.notifyAddedToChat( +// "group", +// group.getInternal_uuid(), +// group.getGroup_name(), +// group.getImage_url(), +// targetUserId +// ); +// } +// +// response = success +// ? new ResponseModel("success", "Member added to group.") +// : new ResponseModel("error", "Failed to add member."); +// break; +// +// } + + + case "add_members_to_group": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); break; } UUID groupId = UUID.fromString(requestJson.getString("group_id")); - UUID targetUserId = UUID.fromString(requestJson.getString("user_id")); - if (!GroupPermissionUtil.canAddMembers(groupId, currentUser.getInternal_uuid())) { response = new ResponseModel("error", "You are not allowed to add members."); break; } - if (GroupDatabase.isUserInGroup(targetUserId, groupId)) { - response = new ResponseModel("error", "User is already a member."); + org.json.JSONArray arr = requestJson.optJSONArray("user_ids"); + if (arr == null || arr.isEmpty()) { + response = new ResponseModel("error", "user_ids is empty."); break; } - boolean success = GroupDatabase.addMemberToGroup(targetUserId, groupId); - Group group = GroupDatabase.findByInternalUUID(groupId); - - //RealTime - if (success && group != null) { - RealTimeEventDispatcher.notifyAddedToChat( - "group", - group.getInternal_uuid(), - group.getGroup_name(), - group.getImage_url(), - targetUserId - ); + if (group == null) { + response = new ResponseModel("error", "Group not found."); + break; } - response = success - ? new ResponseModel("success", "Member added to group.") - : new ResponseModel("error", "Failed to add member."); - break; + int added = 0, skipped = 0, failed = 0; + for (int i = 0; i < arr.length(); i++) { + try { + UUID targetUserId = UUID.fromString(arr.getString(i)); + if (GroupDatabase.isUserInGroup(targetUserId, groupId)) { + skipped++; + continue; + } + boolean ok = GroupDatabase.addMemberToGroup(targetUserId, groupId); + if (ok) { + added++; + RealTimeEventDispatcher.notifyAddedToChat( + "group", + group.getInternal_uuid(), + group.getGroup_name(), + group.getImage_url(), + targetUserId + ); + } else { + failed++; + } + } catch (Exception ex) { + failed++; + } + } + JSONObject data = new JSONObject() + .put("added", added) + .put("skipped", skipped) + .put("failed", failed) + .put("group_id", group.getInternal_uuid().toString()); + + response = new ResponseModel("success", "Batch add finished.", data); + break; } diff --git a/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java b/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java index f071334..262dd2f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/AddMembersController.java @@ -1,12 +1,16 @@ package org.to.telegramfinalproject.UI; import javafx.application.Platform; -import javafx.fxml.FXML; import javafx.geometry.Pos; +import javafx.fxml.FXML; 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.Session; import java.io.File; import java.util.*; @@ -25,68 +29,23 @@ public class AddMembersController { @FXML private Button cancelButton; @FXML private Button createButton; - // Keep selected contacts - private final Set selectedContacts = new HashSet<>(); - + // گروه هدف + private UUID groupInternalId; private String groupName; - private String groupId; + private String groupDisplayId; private File groupImageFile; - // Sample data for testing - private final List allContacts = Arrays.asList( - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") - ); + // انتخاب‌ها + private final Set selectedContacts = new HashSet<>(); + // همه‌ی کانتکت‌ها + private final List allContacts = new ArrayList<>(); @FXML public void initialize() { - updateMemberCount(); - - // Sort + render initially - List sorted = allContacts.stream() - .sorted(Comparator.comparing(Contact::getName)) - .collect(Collectors.toList()); - renderContacts(sorted); - - // Search filter - searchField.textProperty().addListener((obs, oldVal, newVal) -> { - String filter = newVal.toLowerCase(); - List filtered = allContacts.stream() - .filter(c -> c.getName().toLowerCase().contains(filter)) - .sorted(Comparator.comparing(Contact::getName)) - .collect(Collectors.toList()); - renderContacts(filtered); - }); - - // Auto-focus - Platform.runLater(() -> searchField.requestFocus()); - - // Cancel closes overlay - cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); - - // Close when clicking outside - overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); - - // Create action - createButton.setOnAction(e -> { - if (selectedContacts.isEmpty()) { - return; - } - createGroup(); - }); - - // Smooth scroll - contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); + // اسکرول نرم + contactsScroll.getStylesheets().add( + getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm() + ); contactsScroll.setPannable(true); contactsScroll.setFitToWidth(true); contactsScroll.setFitToHeight(false); @@ -95,64 +54,78 @@ public class AddMembersController { contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY); }); - // Theme icon handling + // آیکن سرچ با تم Platform.runLater(() -> { if (addMembersCard.getScene() != null) { ThemeManager.getInstance().registerScene(addMembersCard.getScene()); } }); - ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal)); + ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv)); updateSearchIcon(ThemeManager.getInstance().isDarkMode()); + + // بستن اُورلی + cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); + + // دکمه Add (افزودن اعضا) + createButton.setOnAction(e -> onAddMembers()); + + // سرچ + searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv)); + + Platform.runLater(() -> searchField.requestFocus()); + + // دیتا را از Session بارگذاری کن + loadContactsFromSession(); + updateMemberCount(); + renderContacts(allContacts); } - private void createGroup() { -// String groupName = this.groupName; // set earlier from setGroupInfo() -// File groupImage = this.groupImageFile; // also passed earlier -// -// // Collect selected members -// List memberIds = selectedContacts.stream() -// .map(Contact::getId) // you need some unique identifier for contacts -// .collect(Collectors.toList()); -// -// // Build JSON payload for server -// JSONObject req = new JSONObject(); -// req.put("action", "create_group"); -// req.put("name", groupName); -// req.put("members", memberIds); -// -// if (groupImage != null) { -// req.put("image_path", groupImage.getAbsolutePath()); -// // or upload the file separately depending on your backend design -// } -// -// try { -// JSONObject res = NetworkClient.sendWithResponse(req); // your socket wrapper -// if ("success".equals(res.getString("status"))) { -// // Get new group chat ID from server -// String chatId = res.getString("chat_id"); -// -// // ✅ Close overlay -// MainController.getInstance().closeOverlay(overlayRoot); -// -// // ✅ Open chat immediately -// FXMLLoader loader = new FXMLLoader(getClass().getResource( -// "/org/to/telegramfinalproject/Fxml/chat_page.fxml")); -// Node chatPage = loader.load(); -// -// ChatPageController chatController = loader.getController(); -// chatController.setChat(groupName, -// groupImage != null ? groupImage.toURI().toString() -// : "/org/to/telegramfinalproject/Avatars/default_group.png"); -// -// MainController.getInstance().getChatDisplayArea().getChildren().setAll(chatPage); -// -// } else { -// showAlert("Failed to create group: " + res.getString("message")); -// } -// } catch (Exception ex) { -// ex.printStackTrace(); -// showAlert("Error creating group."); -// } + // این متد را NewGroupController بعد از ساخت گروه صدا بزند + public void setGroupInfo(UUID internalId, String groupName, String displayId, File groupImageFile) { + this.groupInternalId = internalId; + this.groupName = groupName; + this.groupDisplayId = displayId; + this.groupImageFile = groupImageFile; + } + + // — اگر هنوز امضای قدیمی را صدا می‌زنی، موقتاً این اوِرلود هست (displayId را می‌گیرد اما internal_id لازم است) — + public void setGroupInfo(String groupName, String groupId, File groupImageFile) { + // ⚠️ فقط برای سازگاری موقت؛ حتماً NewGroupController را طوری به‌روزرسانی کن + // که internal_id را بدهد (امضای بالایی). + this.groupName = groupName; + this.groupDisplayId = groupId; + this.groupImageFile = groupImageFile; + } + + private void loadContactsFromSession() { + allContacts.clear(); + var u = Session.currentUser; + var arr = (u == null) ? null : u.optJSONArray("contact_list"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + var c = arr.optJSONObject(i); + if (c == null) continue; + String name = c.optString("profile_name", ""); + String id = c.optString("contact_id", ""); // ← internal_uuid مخاطب + if (id.isBlank()) continue; + String imageUrl = c.optString("image_url", + "/org/to/telegramfinalproject/Avatars/default_user_profile.png"); + String status = Optional.ofNullable(c.optString("last_seen", "")) + .filter(s -> !s.isBlank()).map(s -> "last seen " + s).orElse(""); + allContacts.add(new Contact(id, name, status, imageUrl)); + } + } + // اگر خالی بود، چیزی نشون نده (یا می‌تونی دمو بسازی) + allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER)); + } + + private void applyFilter(String q) { + String f = (q == null) ? "" : q.trim().toLowerCase(Locale.ROOT); + List filtered = allContacts.stream() + .filter(c -> c.getName().toLowerCase(Locale.ROOT).contains(f)) + .collect(Collectors.toList()); + renderContacts(filtered); } private void renderContacts(List contacts) { @@ -160,12 +133,10 @@ public class AddMembersController { if (contacts.isEmpty()) { StackPane emptyPane = new StackPane(); - emptyPane.setPrefHeight(300); + emptyPane.setPrefHeight(240); emptyPane.setAlignment(Pos.CENTER); - Label emptyLabel = new Label("No contacts found"); emptyLabel.getStyleClass().add("no-contacts-label"); - emptyPane.getChildren().add(emptyLabel); contactsList.getChildren().add(emptyPane); return; @@ -175,10 +146,8 @@ public class AddMembersController { HBox item = new HBox(10); item.getStyleClass().add("contact-item"); - // Avatar - ImageView avatar = new ImageView(new Image( - Objects.requireNonNull(getClass().getResourceAsStream(c.getImageUrl())) - )); + // آواتار + ImageView avatar = new ImageView(loadAvatar(c.getImageUrl())); avatar.setFitWidth(48); avatar.setFitHeight(48); avatar.setPreserveRatio(true); @@ -188,32 +157,100 @@ public class AddMembersController { nameLabel.getStyleClass().add("contact-name"); Label statusLabel = new Label(c.getStatus()); statusLabel.getStyleClass().add("contact-status"); - details.getChildren().addAll(nameLabel, statusLabel); - item.getChildren().addAll(avatar, details); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); - // Click to toggle selection - item.setOnMouseClicked(e -> toggleSelection(c)); + CheckBox cb = new CheckBox(); + cb.setSelected(selectedContacts.contains(c)); + cb.selectedProperty().addListener((obs, ov, nv) -> { + if (nv) selectedContacts.add(c); else selectedContacts.remove(c); + updateMemberCount(); + updateSelectedMembersPane(); + // برای هایلایت + if (nv) item.getStyleClass().add("contact-selected"); + else item.getStyleClass().remove("contact-selected"); + }); - // Highlight if already selected - if (selectedContacts.contains(c)) { - item.getStyleClass().add("contact-selected"); - } + item.getChildren().addAll(avatar, details, spacer, cb); + + // کلیک روی ردیف = toggle + item.setOnMouseClicked(e -> { + boolean newVal = !cb.isSelected(); + cb.setSelected(newVal); + }); + + // هایلایت انتخاب‌شده + if (selectedContacts.contains(c)) item.getStyleClass().add("contact-selected"); contactsList.getChildren().add(item); } } - private void toggleSelection(Contact contact) { - if (selectedContacts.contains(contact)) { - selectedContacts.remove(contact); - } else { - selectedContacts.add(contact); + private Image loadAvatar(String path) { + try { + // 1) اگر ریسورس داخلی باشد + var in = getClass().getResourceAsStream(path); + if (in != null) return new Image(in); + + // 2) اگر URL کامل یا file URI + return new Image(path, true); + } catch (Exception e) { + return new Image( + Objects.requireNonNull( + getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png") + ) + ); } - updateMemberCount(); - updateSelectedMembersPane(); - renderContacts(allContacts); + } + + private void onAddMembers() { + if (groupInternalId == null) { + showToast("Group internal_id is missing. Make sure setGroupInfo(UUID, ...) was called."); + return; + } + if (selectedContacts.isEmpty()) { + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + return; + } + + List ids = selectedContacts.stream().map(Contact::getId).toList(); + + // تلاش برای batch + JSONObject batchReq = new JSONObject() + .put("action", "add_members_to_group") + .put("group_id", groupInternalId.toString()) + .put("user_ids", new JSONArray(ids)); + + new Thread(() -> { + JSONObject resp = ActionHandler.sendWithResponse(batchReq); + if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { + Platform.runLater(() -> { + showToast("Members added."); + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + }); + return; + } + + // fallback: تک‌به‌تک + boolean allOk = true; + for (String uid : ids) { + JSONObject single = new JSONObject() + .put("action", "add_member_to_group") + .put("group_id", groupInternalId.toString()) + .put("user_id", uid); + JSONObject r = ActionHandler.sendWithResponse(single); + if (r == null || !"success".equalsIgnoreCase(r.optString("status"))) { + allOk = false; + } + } + boolean finalAllOk = allOk; + Platform.runLater(() -> { + showToast(finalAllOk ? "Members added." : "Some members failed."); + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + }); + }).start(); } private void updateSelectedMembersPane() { @@ -233,39 +270,41 @@ public class AddMembersController { String iconPath = darkMode ? "/org/to/telegramfinalproject/Icons/search_light.png" : "/org/to/telegramfinalproject/Icons/search_dark.png"; - ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath))); icon.setFitWidth(16); icon.setFitHeight(16); searchIcon.setGraphic(icon); } - public void setGroupInfo(String groupName, String groupId, File groupImageFile) { - this.groupName = groupName; - this.groupId = groupId; - this.groupImageFile = groupImageFile; - - // You can use these later when creating the group - System.out.println("Group name passed: " + groupName); - if (groupImageFile != null) { - System.out.println("Group image: " + groupImageFile.getName()); - } + private void showToast(String msg) { + Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); + a.initOwner(addMembersCard.getScene().getWindow()); + a.show(); } - // Inner class for contact data + // ================== مدل Contact ================== public static class Contact { + private final String id; // internal_uuid کاربر private final String name; private final String status; private final String imageUrl; - public Contact(String name, String status, String imageUrl) { + public Contact(String id, String name, String status, String imageUrl) { + this.id = id; this.name = name; this.status = status; this.imageUrl = imageUrl; } - + public String getId() { return id; } public String getName() { return name; } public String getStatus() { return status; } public String getImageUrl() { return imageUrl; } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Contact c)) return false; + return Objects.equals(id, c.id); + } + @Override public int hashCode() { return Objects.hash(id); } } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java b/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java index 5f4afcc..a2a287c 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/AddSubscriberController.java @@ -7,6 +7,10 @@ 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.Session; import java.io.File; import java.util.*; @@ -25,69 +29,23 @@ public class AddSubscriberController { @FXML private Button skipButton; @FXML private Button addButton; - // Keep selected contacts - private final Set selectedContacts = new HashSet<>(); - + // اطلاعات کانال + private UUID channelInternalId; private String channelName; - private String channelId; + private String channelDisplayId; private File channelImageFile; private String description; - // Sample data for testing - private final List allContacts = Arrays.asList( - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Ali", "last seen recently", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Iman", "last seen a long time ago", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Amir", "last seen within a month", "/org/to/telegramfinalproject/Avatars/default_user_profile.png"), - new Contact("Sara", "online", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") - ); + // انتخاب‌ها و منبع داده + private final Set selected = new HashSet<>(); + private final List allContacts = new ArrayList<>(); @FXML public void initialize() { - updateMemberCount(); - - // Sort + render initially - renderContacts(allContacts.stream() - .sorted(Comparator.comparing(Contact::getName)) - .collect(Collectors.toList())); - - // Search filter - searchField.textProperty().addListener((obs, oldVal, newVal) -> { - String filter = newVal.toLowerCase(); - List filtered = allContacts.stream() - .filter(c -> c.getName().toLowerCase().contains(filter)) - .sorted(Comparator.comparing(Contact::getName)) - .collect(Collectors.toList()); - renderContacts(filtered); - }); - - // Auto-focus - Platform.runLater(() -> searchField.requestFocus()); - - // Skip → ignore selection and create chat - skipButton.setOnAction(e -> { - createChannel(); // always creates, even if no members selected - }); - - // Add → requires at least one member - addButton.setOnAction(e -> { - if (!selectedContacts.isEmpty()) { - createChannel(); - } - }); - - // Close when clicking outside - overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); - - // Smooth scroll - contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()); + // اسکرول نرم + contactsScroll.getStylesheets().add( + getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm() + ); contactsScroll.setPannable(true); contactsScroll.setFitToWidth(true); contactsScroll.setFitToHeight(false); @@ -96,25 +54,74 @@ public class AddSubscriberController { contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY); }); - // Theme icon handling + // تم آیکن Platform.runLater(() -> { if (addMembersCard.getScene() != null) { ThemeManager.getInstance().registerScene(addMembersCard.getScene()); } }); - ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal)); + ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv)); updateSearchIcon(ThemeManager.getInstance().isDarkMode()); + + // بستن اُورلی + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); + skipButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent())); + addButton.setOnAction(e -> onAddSubscribers()); + + // سرچ + searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv)); + Platform.runLater(() -> searchField.requestFocus()); + + // لود کانتکت‌ها از Session + loadContactsFromSession(); + updateCount(); + renderContacts(allContacts); } - private void createChannel() { - // TODO: real server request → for now just print and close overlay - System.out.println("✅ Creating group/channel: " + channelName); - System.out.println("Selected members: " + selectedContacts.stream() - .map(Contact::getName).collect(Collectors.joining(", "))); + /** NewChannelController این را بعد از ساخت کانال صدا بزند */ + public void setChannelInfo(UUID internalId, String name, String displayId, File imageFile, String description) { + this.channelInternalId = internalId; + this.channelName = name; + this.channelDisplayId = displayId; + this.channelImageFile = imageFile; + this.description = description; + } - MainController.getInstance().closeOverlay(addMembersCard.getParent()); + // برای سازگاری با امضای قدیمی (اختیاری) + public void setChannelInfo(String name, String id, String description, File image) { + this.channelName = name; + this.channelDisplayId = id; + this.channelImageFile = image; + this.description = description; + } - // TODO: open chat immediately (like you did with newGroup) + private void loadContactsFromSession() { + allContacts.clear(); + var u = Session.currentUser; + var arr = (u == null) ? null : u.optJSONArray("contact_list"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + var c = arr.optJSONObject(i); + if (c == null) continue; + String name = c.optString("profile_name", ""); + String id = c.optString("contact_id", ""); // internal_uuid + if (id.isBlank()) continue; + String imageUrl = c.optString("image_url", + "/org/to/telegramfinalproject/Avatars/default_user_profile.png"); + String status = Optional.ofNullable(c.optString("last_seen", "")) + .filter(s -> !s.isBlank()).map(s -> "last seen " + s).orElse(""); + allContacts.add(new Contact(id, name, status, imageUrl)); + } + } + allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER)); + } + + private void applyFilter(String q) { + String f = (q == null) ? "" : q.trim().toLowerCase(Locale.ROOT); + List filtered = allContacts.stream() + .filter(c -> c.getName().toLowerCase(Locale.ROOT).contains(f)) + .collect(Collectors.toList()); + renderContacts(filtered); } private void renderContacts(List contacts) { @@ -122,12 +129,10 @@ public class AddSubscriberController { if (contacts.isEmpty()) { StackPane emptyPane = new StackPane(); - emptyPane.setPrefHeight(300); + emptyPane.setPrefHeight(240); emptyPane.setAlignment(Pos.CENTER); - Label emptyLabel = new Label("No contacts found"); emptyLabel.getStyleClass().add("no-contacts-label"); - emptyPane.getChildren().add(emptyLabel); contactsList.getChildren().add(emptyPane); return; @@ -137,10 +142,7 @@ public class AddSubscriberController { HBox item = new HBox(10); item.getStyleClass().add("contact-item"); - // Avatar - ImageView avatar = new ImageView(new Image( - Objects.requireNonNull(getClass().getResourceAsStream(c.getImageUrl())) - )); + ImageView avatar = new ImageView(loadAvatar(c.getImageUrl())); avatar.setFitWidth(48); avatar.setFitHeight(48); avatar.setPreserveRatio(true); @@ -150,79 +152,141 @@ public class AddSubscriberController { nameLabel.getStyleClass().add("contact-name"); Label statusLabel = new Label(c.getStatus()); statusLabel.getStyleClass().add("contact-status"); - details.getChildren().addAll(nameLabel, statusLabel); - item.getChildren().addAll(avatar, details); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); - // Click to toggle selection - item.setOnMouseClicked(e -> toggleSelection(c)); + CheckBox cb = new CheckBox(); + cb.setSelected(selected.contains(c)); + cb.selectedProperty().addListener((obs, ov, nv) -> { + if (nv) selected.add(c); else selected.remove(c); + updateCount(); + updateSelectedPane(); + if (nv) item.getStyleClass().add("contact-selected"); + else item.getStyleClass().remove("contact-selected"); + }); - // Highlight if already selected - if (selectedContacts.contains(c)) { - item.getStyleClass().add("contact-selected"); - } + item.getChildren().addAll(avatar, details, spacer, cb); + + item.setOnMouseClicked(e -> cb.setSelected(!cb.isSelected())); + + if (selected.contains(c)) item.getStyleClass().add("contact-selected"); contactsList.getChildren().add(item); } } - private void toggleSelection(Contact contact) { - if (selectedContacts.contains(contact)) { - selectedContacts.remove(contact); - } else { - selectedContacts.add(contact); + private Image loadAvatar(String path) { + try { + var in = getClass().getResourceAsStream(path); + if (in != null) return new Image(in); + return new Image(path, true); + } catch (Exception e) { + return new Image( + Objects.requireNonNull( + getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png") + ) + ); } - updateMemberCount(); - updateSelectedMembersPane(); - renderContacts(allContacts); } - private void updateSelectedMembersPane() { + private void onAddSubscribers() { + if (channelInternalId == null) { + showToast("Channel internal_id is missing. Make sure setChannelInfo(UUID, ...) was called."); + return; + } + if (selected.isEmpty()) { + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + return; + } + + List ids = selected.stream().map(Contact::getId).toList(); + + JSONObject batchReq = new JSONObject() + .put("action", "add_subscribers_to_channel") + .put("channel_id", channelInternalId.toString()) + .put("user_ids", new JSONArray(ids)); + + new Thread(() -> { + JSONObject resp = ActionHandler.sendWithResponse(batchReq); + if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { + Platform.runLater(() -> { + showToast("Subscribers added."); + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + }); + return; + } + + // fallback: تک‌به‌تک + boolean allOk = true; + for (String uid : ids) { + JSONObject single = new JSONObject() + .put("action", "add_subscriber_to_channel") + .put("channel_id", channelInternalId.toString()) + .put("user_id", uid); + JSONObject r = ActionHandler.sendWithResponse(single); + if (r == null || !"success".equalsIgnoreCase(r.optString("status"))) { + allOk = false; + } + } + boolean finalAllOk = allOk; + Platform.runLater(() -> { + showToast(finalAllOk ? "Subscribers added." : "Some subscribers failed."); + MainController.getInstance().closeOverlay(addMembersCard.getParent()); + }); + }).start(); + } + + private void updateSelectedPane() { selectedMembersPane.getChildren().clear(); - for (Contact c : selectedContacts) { + for (Contact c : selected) { Label chip = new Label(c.getName()); chip.getStyleClass().add("member-chip"); selectedMembersPane.getChildren().add(chip); } } - private void updateMemberCount() { - memberCountLabel.setText(selectedContacts.size() + " / 200000"); + private void updateCount() { + memberCountLabel.setText(selected.size() + " / 200000"); } private void updateSearchIcon(boolean darkMode) { String iconPath = darkMode ? "/org/to/telegramfinalproject/Icons/search_light.png" : "/org/to/telegramfinalproject/Icons/search_dark.png"; - ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath))); icon.setFitWidth(16); icon.setFitHeight(16); searchIcon.setGraphic(icon); } - public void setChannelInfo(String name, String id, String description, File image) { - this.channelName = name; - this.channelId = id; - this.channelImageFile = image; - this.description = description; + private void showToast(String msg) { + Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); + a.initOwner(addMembersCard.getScene().getWindow()); + a.show(); } - // Inner class for contact data + // ===== مدل Contact ===== public static class Contact { + private final String id; // internal_uuid کاربر private final String name; private final String status; private final String imageUrl; - public Contact(String name, String status, String imageUrl) { - this.name = name; - this.status = status; - this.imageUrl = imageUrl; + public Contact(String id, String name, String status, String imageUrl) { + this.id = id; this.name = name; this.status = status; this.imageUrl = imageUrl; } - + public String getId() { return id; } public String getName() { return name; } public String getStatus() { return status; } public String getImageUrl() { return imageUrl; } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Contact c)) return false; + return Objects.equals(id, c.id); + } + @Override public int hashCode() { return Objects.hash(id); } } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 9cfbe52..3ad086f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -36,6 +36,9 @@ public class MainController { @FXML private ImageView noResultIcon; @FXML private ScrollPane globalSearchScroll; @FXML private VBox globalSearchResultsContainer; + + + private enum SearchMode { GLOBAL, CHAT @@ -1113,6 +1116,25 @@ public class MainController { } + public void addChatAndSelect(org.to.telegramfinalproject.Models.ChatEntry entry) { + // در Session نگه‌داری + if (org.to.telegramfinalproject.Client.Session.chatList.stream() + .noneMatch(c -> c.getId().equals(entry.getId()))) { + org.to.telegramfinalproject.Client.Session.chatList.add(0, entry); + org.to.telegramfinalproject.Client.Session.activeChats.add(0, entry); + } + // سایدبارت اگر متدی برای رفرش دارد صداش بزن (اسمش را با کلاس خودت هماهنگ کن) + try { + this.refreshChatListUI(); // اگر نداری، این خط را حذف کن + } catch (Exception ignore) {} + + // نمایش پیج چت + if (getChatPageController() != null) { + getChatPageController().showChat(entry); + } + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java index 68e7329..2bbb95f 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java @@ -10,11 +10,15 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.scene.layout.Pane; + +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Models.ChatEntry; + import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; +import java.util.UUID; public class NewChannelController { @@ -28,7 +32,7 @@ public class NewChannelController { @FXML private ImageView cameraIcon; @FXML private Button cancelButton; @FXML private Button createButton; - @FXML private StackPane overlayRoot; // the root + @FXML private StackPane overlayRoot; @FXML private Label descCounter; @FXML private Label channelIdLabel; @FXML private TextField channelIdField; @@ -37,12 +41,10 @@ public class NewChannelController { @FXML public void initialize() { - // Load default camera icon cameraIcon.setImage(new Image( getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/camera.png") )); - // Camera button action → choose image cameraButton.setOnAction(e -> { FileChooser chooser = new FileChooser(); chooser.setTitle("Choose Channel Picture"); @@ -56,109 +58,135 @@ public class NewChannelController { } }); - // Cancel → close overlay - cancelButton.setOnAction(e -> { - MainController.getInstance().closeOverlay(overlayRoot); - }); + cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayRoot)); + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(overlayRoot)); - // Close when clicking outside card - overlayBackground.setOnMouseClicked(e -> { - MainController.getInstance().closeOverlay(overlayRoot); - }); - - // Create button → validate name + submit - createButton.setOnAction(e -> { - String channelName = channelNameField.getText().trim(); - String channelId = channelIdField.getText().trim(); - String description = channelDescField.getText().trim(); - - if (channelName.isEmpty()) { - // Apply error style - channelNameField.getStyleClass().add("error"); - channelNameLabel.getStyleClass().add("error"); - return; - } - - if (channelId.isEmpty()) { - // Apply error style - channelIdField.getStyleClass().add("error"); - channelIdLabel.getStyleClass().add("error"); - return; - } - - try { - // Load Add Members overlay - FXMLLoader loader = new FXMLLoader(getClass().getResource( - "/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); - StackPane addSubscribersOverlay = loader.load(); - - // Pass channel info to AddMembersController - AddSubscriberController controller = loader.getController(); - controller.setChannelInfo(channelName, channelId, description, channelImageFile); - - // Close the current "New Channel" overlay - MainController.getInstance().closeOverlay(overlayRoot); - - // Then open the "Add Members" overlay - MainController.getInstance().showOverlay(addSubscribersOverlay); - - } catch (IOException ex) { - ex.printStackTrace(); - } - }); + createButton.setOnAction(e -> onCreateChannel()); + // محدودیت توضیح final int MAX_LENGTH = 255; channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> { - if (channelDescField.getText().length() >= MAX_LENGTH) { - e.consume(); // stop extra character from being typed - } + if (channelDescField.getText().length() >= MAX_LENGTH) e.consume(); }); - channelDescField.textProperty().addListener((obs, oldText, newText) -> { if (newText.length() > MAX_LENGTH) { channelDescField.setText(newText.substring(0, MAX_LENGTH)); channelDescField.positionCaret(MAX_LENGTH); } - - // Current length out of max int current = channelDescField.getText().length(); descCounter.setText(current + " / " + MAX_LENGTH); - - // Style when limit reached - if (current == MAX_LENGTH) { - descCounter.setStyle("-fx-text-fill: red;"); - } else { - descCounter.setStyle(""); // fallback to CSS - } + descCounter.setStyle(current == MAX_LENGTH ? "-fx-text-fill: red;" : ""); }); + descCounter.setText("0 / 255"); - // Initial value - descCounter.setText("0 / " + MAX_LENGTH); - - // Reset error state when typing - channelNameField.textProperty().addListener((obs, oldVal, newVal) -> { - if (!newVal.trim().isEmpty()) { + // پاک کردن استایل خطا هنگام تایپ + channelNameField.textProperty().addListener((obs, ov, nv) -> { + if (!nv.trim().isEmpty()) { channelNameField.getStyleClass().remove("error"); channelNameLabel.getStyleClass().remove("error"); } }); - - // Reset error state when typing - channelIdField.textProperty().addListener((obs, oldVal, newVal) -> { - if (!newVal.trim().isEmpty()) { + channelIdField.textProperty().addListener((obs, ov, nv) -> { + if (!nv.trim().isEmpty()) { channelIdField.getStyleClass().remove("error"); channelIdLabel.getStyleClass().remove("error"); } }); - // Auto-focus channel name on open Platform.runLater(() -> channelNameField.requestFocus()); - // Register scene for ThemeManager → stylesheet swap will handle colors/icons Platform.runLater(() -> { if (newChannelCard.getScene() != null) { ThemeManager.getInstance().registerScene(newChannelCard.getScene()); } }); } + + private void onCreateChannel() { + String name = nz(channelNameField.getText()).trim(); + String dispId = nz(channelIdField.getText()).trim(); + String description = nz(channelDescField.getText()).trim(); + + boolean ok = true; + if (name.isEmpty()) { channelNameField.getStyleClass().add("error"); channelNameLabel.getStyleClass().add("error"); ok = false; } + if (dispId.isEmpty()) { channelIdField.getStyleClass().add("error"); channelIdLabel.getStyleClass().add("error"); ok = false; } + if (!ok) return; + + final String me = Session.getUserUUID(); + if (me == null || me.isBlank()) { + showToast("Cannot create channel: current user UUID missing."); + return; + } + + // TODO: اگر آپلود تصویر داری، فایل را آپلود کن و URL نهایی را اینجا بفرست + final String imageUrl = null; + + JSONObject req = new JSONObject() + .put("action", "create_channel") + .put("channel_id", dispId) // آیدی نمایشی/عمومی + .put("channel_name", name) + .put("user_id", me) // internal_uuid سازنده + .put("image_url", imageUrl) // اختیاری + .put("description", description); // اختیاری (سرور اگر ساپورت نکند نادیده می‌گیرد) + + new Thread(() -> { + JSONObject resp = ActionHandler.sendWithResponse(req); + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + Platform.runLater(() -> showToast("Create failed: " + + (resp == null ? "no response" : resp.optString("message","")))); + return; + } + + JSONObject data = resp.optJSONObject("data"); + if (data == null) { + Platform.runLater(() -> showToast("Create failed: empty data.")); + return; + } + + UUID internalId = UUID.fromString(data.optString("internal_id")); + String returnedName = data.optString("name", name); + String returnedDisp = data.optString("id", dispId); + String returnedImg = data.optString("image_url", ""); + + // 1) چت کانال تازه‌ساخته را به سایدبار اضافه کن و انتخاب کن + Platform.runLater(() -> { + ChatEntry entry = ChatEntry.fromServer( + internalId, + "channel", + returnedName, + returnedDisp, + returnedImg, + /*isOwner*/ true, + /*isAdmin*/ true + ); + MainController.getInstance().addChatAndSelect(entry); + }); + + // 2) اُورلی افزودن سابسکرایبر را باز کن و internal_id را پاس بده + Platform.runLater(() -> { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); + StackPane addSubsOverlay = loader.load(); + + AddSubscriberController controller = loader.getController(); + controller.setChannelInfo(internalId, returnedName, returnedDisp, channelImageFile, description); + + MainController.getInstance().showOverlay(addSubsOverlay); + MainController.getInstance().closeOverlay(overlayRoot); + } catch (IOException ex) { + ex.printStackTrace(); + showToast("Failed to open Add Subscribers."); + } + }); + }).start(); + } + + private void showToast(String msg) { + Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); + a.initOwner(overlayRoot.getScene().getWindow()); + a.show(); + } + + private static String nz(String s){ return s==null? "": s; } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java index 21c27fe..79ea078 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java @@ -10,8 +10,16 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.scene.layout.Pane; + +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; +import org.to.telegramfinalproject.Models.ChatEntry; + import java.io.File; import java.io.IOException; +import java.util.Optional; +import java.util.UUID; public class NewGroupController { @@ -31,12 +39,10 @@ public class NewGroupController { @FXML public void initialize() { - // Load default camera icon cameraIcon.setImage(new Image( getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/camera.png") )); - // Camera button action → choose image cameraButton.setOnAction(e -> { FileChooser chooser = new FileChooser(); chooser.setTitle("Choose Group Picture"); @@ -50,71 +56,121 @@ public class NewGroupController { } }); - cancelButton.setOnAction(e -> { - MainController.getInstance().closeOverlay(overlayRoot); - }); + cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayRoot)); + overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(overlayRoot)); - overlayBackground.setOnMouseClicked(e -> { - MainController.getInstance().closeOverlay(overlayRoot); - }); + nextButton.setOnAction(e -> onNext()); - nextButton.setOnAction(e -> { - String groupName = groupNameField.getText().trim(); - String groupId = groupIdField.getText().trim(); - - if (groupName.isEmpty()) { - // Apply error style - groupNameField.getStyleClass().add("error"); - groupNameLabel.getStyleClass().add("error"); - - return; - } - - if (groupId.isEmpty()) { - // Apply error style - groupIdField.getStyleClass().add("error"); - groupIdLabel.getStyleClass().add("error"); - - return; - } - - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource( - "/org/to/telegramfinalproject/Fxml/add_member.fxml")); - StackPane addMembersOverlay = loader.load(); - - // Optional: pass groupName and image to AddMembersController - AddMembersController controller = loader.getController(); - controller.setGroupInfo(groupName, groupId, groupImageFile); - - // Close the current "New Group" overlay - MainController.getInstance().closeOverlay(overlayRoot); - - // Then open the Add Members overlay - MainController.getInstance().showOverlay(addMembersOverlay); - - } catch (IOException ex) { - ex.printStackTrace(); - } - }); - - // Reset error state when typing - groupNameField.textProperty().addListener((obs, oldVal, newVal) -> { - if (!newVal.trim().isEmpty()) { + // clear errors on typing + groupNameField.textProperty().addListener((obs, ov, nv) -> { + if (!nv.trim().isEmpty()) { groupNameField.getStyleClass().remove("error"); groupNameLabel.getStyleClass().remove("error"); } }); - - // Reset error state when typing - groupIdField.textProperty().addListener((obs, oldVal, newVal) -> { - if (!newVal.trim().isEmpty()) { + groupIdField.textProperty().addListener((obs, ov, nv) -> { + if (!nv.trim().isEmpty()) { groupIdField.getStyleClass().remove("error"); groupIdLabel.getStyleClass().remove("error"); } }); - // Auto_focus search bar when overlay opens Platform.runLater(() -> groupNameField.requestFocus()); } + + private void onNext() { + String groupName = groupNameField.getText() == null ? "" : groupNameField.getText().trim(); + String groupId = groupIdField.getText() == null ? "" : groupIdField.getText().trim(); + + boolean ok = true; + if (groupName.isEmpty()) { + groupNameField.getStyleClass().add("error"); + groupNameLabel.getStyleClass().add("error"); + ok = false; + } + if (groupId.isEmpty()) { + groupIdField.getStyleClass().add("error"); + groupIdLabel.getStyleClass().add("error"); + ok = false; + } + if (!ok) return; + + // ساخت گروه روی سرور + final String me = Session.getUserUUID(); // internal_uuid + if (me == null || me.isBlank()) { + showToast("Cannot create group: current user UUID missing."); + return; + } + + // اگر آپلود تصویر داری، اینجا عکس رو آپلود کن و imageUrl واقعی بفرست (TODO) + final String imageUrl = null; + + JSONObject req = new JSONObject() + .put("action", "create_group") + .put("group_id", groupId) + .put("group_name", groupName) + .put("user_id", me) // ← internal_uuid سازنده + .put("image_url", imageUrl); // ← اختیاری + + new Thread(() -> { + JSONObject resp = ActionHandler.sendWithResponse(req); + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + Platform.runLater(() -> showToast("Create failed: " + + (resp == null ? "no response" : resp.optString("message","")))); + return; + } + + JSONObject data = resp.optJSONObject("data"); + if (data == null) { + Platform.runLater(() -> showToast("Create failed: empty data.")); + return; + } + + UUID internalId = UUID.fromString(data.optString("internal_id")); + String returnedName = data.optString("name", groupName); + String returnedDisp = data.optString("id", groupId); + String returnedImg = data.optString("image_url", ""); + + // 1) به‌صورت لوکال یک ChatEntry بساز و به سایدبار اضافه و سِلکت کن + Platform.runLater(() -> { + ChatEntry entry = ChatEntry.fromServer( + internalId, + "group", + returnedName, + returnedDisp, + returnedImg, + /*isOwner*/ true, + /*isAdmin*/ true + ); + MainController.getInstance().addChatAndSelect(entry); + }); + + // 2) پنجره‌ی Add Members را باز کن و internal_id گروه را پاس بده + Platform.runLater(() -> { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/to/telegramfinalproject/Fxml/add_member.fxml")); + StackPane addMembersOverlay = loader.load(); + AddMembersController controller = loader.getController(); + controller.setGroupInfo(internalId, returnedName, returnedDisp, groupImageFile); + + // این اُورلی را روی UI نشان بده + MainController.getInstance().showOverlay(addMembersOverlay); + + // این اُورلی NewGroup را ببند + MainController.getInstance().closeOverlay(overlayRoot); + } catch (IOException ex) { + ex.printStackTrace(); + showToast("Failed to open Add Members."); + } + }); + }).start(); + } + + private void showToast(String msg) { + // جایگزینش کن با سیستم نوتی شما + Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); + a.initOwner(overlayRoot.getScene().getWindow()); + a.show(); + } }