diff --git a/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java b/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java index 2458d99..1d36aa9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java +++ b/src/main/java/org/to/telegramfinalproject/Client/AvatarLocalResolver.java @@ -13,21 +13,48 @@ public final class AvatarLocalResolver { } public static String resolve(String serverValue) { - if (serverValue == null || serverValue.isBlank()) return null; - if (isHttp(serverValue) || serverValue.startsWith("file:")) return serverValue; + if (serverValue == null) return null; + serverValue = serverValue.trim(); + if (serverValue.isEmpty()) return null; - String rel = serverValue.startsWith("/") ? serverValue.substring(1) : serverValue; // "avatars/..." + // URL کامل یا file: → همون رو برگردون + if (isHttp(serverValue) || serverValue.startsWith("file:")) { + return serverValue; + } + + // ⬅️ مهم: برای مسیرهای لوکال، هر چیزی بعد از ? یا # را حذف کن + serverValue = stripQueryAndHash(serverValue); + + // حذف اسلش ابتدایی (اگر بود) + String rel = serverValue.startsWith("/") ? serverValue.substring(1) : serverValue; + + // ساخت مسیر امن داخل uploads Path p = UPLOADS_ROOT.resolve(rel).normalize(); - if (!p.startsWith(UPLOADS_ROOT)) return null; // امنیت مسیر - if (!Files.exists(p)) { // مهم: واقعاً وجود دارد؟ + + // جلوگیری از خروج از دایرکتوری uploads (path traversal) + if (!p.startsWith(UPLOADS_ROOT)) return null; + + // وجود واقعی فایل + if (!Files.exists(p)) { System.err.println("Avatar resolve: NOT FOUND -> " + p); return null; } - String url = p.toUri().toString(); // file:///.../uploads/avatars/.... + + // خروجی به صورت file:// URI که JavaFX Image می‌فهمد + String url = p.toUri().toString(); System.out.println("Avatar resolve: " + serverValue + " -> " + url); return url; } + private static String stripQueryAndHash(String s) { + int q = s.indexOf('?'); + if (q >= 0) s = s.substring(0, q); + int h = s.indexOf('#'); + if (h >= 0) s = s.substring(0, h); + return s; + } + + public static Image load(String serverValue) { String url = resolve(serverValue); if (url == null) return null; diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 5665701..dc0b29e 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -1811,7 +1811,7 @@ private void addBubble( if (days > 30) { return "Last seen long time ago"; } - return "recently"; + return "Last seen recently"; } private void setDefaultHeaderAvatarByType(String type){ diff --git a/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java b/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java index 8df3ad3..8d4fac0 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/EditProfileController.java @@ -23,9 +23,16 @@ import org.to.telegramfinalproject.Client.Session; import java.io.File; import java.io.IOException; import java.util.Objects; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; +import org.json.JSONObject; + +import java.io.File; public class EditProfileController { + private File pickedAvatarFile; + @FXML private Pane overlayBackground; @FXML private VBox editCard; @@ -108,13 +115,15 @@ public class EditProfileController { File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow()); if (file != null) { - profileImageView.setImage(new Image(file.toURI().toString())); - // TODO: save this new image to DB or server + pickedAvatarFile = file; // ← ذخیره کن برای ارسال هنگام خروج + profileImageView.setImage(new Image(file.toURI().toString())); // پیش‌نمایش } } private void closeEdit() { - MainController.getInstance().closeOverlay(editCard.getParent()); + //MainController.getInstance().closeOverlay(editCard.getParent()); + MainController.getInstance().goBack(overlayBackground); + } public void setProfileData(String name, String status, String bio, String userId, Image profileImage) { @@ -135,6 +144,55 @@ public class EditProfileController { boolean anyError = false; +// if (pickedAvatarFile != null) { +// ActionHandler.instance.uploadAvatarFor("user", null, pickedAvatarFile); +// +// if (ActionHandler.instance.wasSuccess()) { +// String url = ActionHandler.instance.getLastMessage(); +// if (url != null && !url.isBlank()) { +// currentUser.put("image_url", url); +// } +// } else { +// anyError = true; +// showAlert( +// "Avatar upload failed", +// ActionHandler.instance.getLastMessage() == null ? "Unknown error" : ActionHandler.instance.getLastMessage(), +// Alert.AlertType.ERROR +// ); +// } +// } + + + if (pickedAvatarFile != null) { + ActionHandler.instance.uploadAvatarFor("user", null, pickedAvatarFile); + + if (ActionHandler.instance.wasSuccess()) { + String url = ActionHandler.instance.getLastMessage(); + if (url != null && !url.isBlank()) { + // قبلاً اینجا مستقیم bust می‌زدیم → مشکل ایجاد می‌کرد + // currentUser.put("image_url", url); + + // ✔️ نسخه‌ی امن: + String extracted = extractImageUrl(url); // از helper پایین + if (isLikelyImageUrl(extracted)) { + String finalUrl = addCacheBusterIfHttp(extracted); // فقط برای http/https + Session.currentUser.put("image_url", finalUrl); + } else { + // اگر سرور چیز عجیبی مثل "Welcome Farid" برگردوند، نادیده بگیر + System.out.println("Upload avatar returned a non-image value: " + url); + } + } + } else { + anyError = true; + showAlert( + "Avatar upload failed", + ActionHandler.instance.getLastMessage() == null ? "Unknown error" : ActionHandler.instance.getLastMessage(), + Alert.AlertType.ERROR + ); + } + } + + // --- Update bio --- String oldBio = currentUser.optString("bio", ""); if (!newBio.equals(oldBio)) { @@ -181,8 +239,14 @@ public class EditProfileController { currentUser.optString("bio"), currentUser.optString("user_id"), currentUser.optString("image_url", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") + + ); - MainController.getInstance().closeOverlay(bioField.getParent().getParent()); +// MainController mc = MainController.getInstance(); + MainController.getInstance().refreshSidebarUserFromSession(); + MainController.getInstance().goBack(overlayBackground); + +// MainController.getInstance().closeOverlay(bioField.getParent().getParent()); } } @@ -199,4 +263,48 @@ public class EditProfileController { alert.showAndWait(); } + + + + private boolean isLikelyImageUrl(String s) { + if (s == null || s.isBlank()) return false; + // URL کامل، مسیر نسبی سرور، یا file: و پسوند تصویر + return s.startsWith("http://") || s.startsWith("https://") + || s.startsWith("file:") + || s.startsWith("/") + || s.matches("(?i).+\\.(png|jpe?g|gif|webp)$"); + } + + private String addCacheBusterIfHttp(String url) { + if (url == null) return null; + if (url.startsWith("http://") || url.startsWith("https://")) { + return url + (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis(); + } + // برای مسیر لوکال/نسبی اصلاً ?v اضافه نکن (روی ویندوز مشکل می‌دهد) + return url; + } + + /** اگر سرور به‌جای URL، JSON یا متن دیگری داد، اینجا URL را دربیار. */ + private String extractImageUrl(String raw) { + if (raw == null) return null; + String s = raw.trim(); + // اگر برگشتی JSON است (بعضی سرویس‌ها data.display_url برمی‌گردانند) + if (s.startsWith("{") && s.endsWith("}")) { + try { + org.json.JSONObject j = new org.json.JSONObject(s); + // هر کلیدی که سرویس‌ات استفاده می‌کند را امتحان کن + if (j.has("display_url")) return j.optString("display_url", null); + if (j.has("url")) return j.optString("url", null); + if (j.has("data")) { + var d = j.optJSONObject("data"); + if (d != null) { + if (d.has("display_url")) return d.optString("display_url", null); + if (d.has("url")) return d.optString("url", null); + } + } + } catch (Exception ignore) {} + } + return s; // در غیر این صورت همان raw + } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 94f4f35..de8886d 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -1141,4 +1141,12 @@ public class MainController { getChatPageController().showChat(entry); } } + + + public void refreshSidebarUserFromSession() { + if (sidebarController != null && Session.currentUser != null) { + Platform.runLater(() -> sidebarController.setUserFromSession(Session.currentUser)); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/MyProfileController.java b/src/main/java/org/to/telegramfinalproject/UI/MyProfileController.java index 52a82b8..5c74f58 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MyProfileController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MyProfileController.java @@ -14,6 +14,12 @@ import javafx.scene.shape.Circle; import java.io.IOException; import java.util.Objects; +import org.json.JSONObject; +import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.AvatarLocalResolver; +import org.to.telegramfinalproject.Client.Session; + +import java.io.File; public class MyProfileController { @@ -68,6 +74,7 @@ public class MyProfileController { // === Open edit profile === editButton.setOnAction(e -> openEditProfile()); + } private void closeProfile() { @@ -91,9 +98,18 @@ public class MyProfileController { this.userId.setText("@" + userId); userID = userId; - if (imageUrl != null && !imageUrl.isEmpty()) { - profileImage.setImage(new Image(imageUrl)); + if (imageUrl != null && !imageUrl.isBlank()) { + Image img = AvatarLocalResolver.load(imageUrl); + if (img != null) { + profileImage.setImage(img); + } else { + profileImage.setImage(new Image( + Objects.requireNonNull(getClass().getResourceAsStream( + "/org/to/telegramfinalproject/Avatars/default_user_profile.png")) + )); + } } + } // === Theme-specific updates === @@ -133,4 +149,5 @@ public class MyProfileController { ex.printStackTrace(); } } + } diff --git a/uploads/avatars/2025-09-04/0c49fdf9-8cec-4724-9e8c-eb2f22829e85.png b/uploads/avatars/2025-09-04/0c49fdf9-8cec-4724-9e8c-eb2f22829e85.png new file mode 100644 index 0000000..6f674aa Binary files /dev/null and b/uploads/avatars/2025-09-04/0c49fdf9-8cec-4724-9e8c-eb2f22829e85.png differ diff --git a/uploads/avatars/2025-09-04/165ff232-2d0b-49b3-8455-ab3f7d178800.png b/uploads/avatars/2025-09-04/165ff232-2d0b-49b3-8455-ab3f7d178800.png new file mode 100644 index 0000000..8f744ba Binary files /dev/null and b/uploads/avatars/2025-09-04/165ff232-2d0b-49b3-8455-ab3f7d178800.png differ diff --git a/uploads/avatars/2025-09-04/19dfd58c-cbcc-4e7d-93bd-693dfc1cdd7f.png b/uploads/avatars/2025-09-04/19dfd58c-cbcc-4e7d-93bd-693dfc1cdd7f.png new file mode 100644 index 0000000..19b7f82 Binary files /dev/null and b/uploads/avatars/2025-09-04/19dfd58c-cbcc-4e7d-93bd-693dfc1cdd7f.png differ diff --git a/uploads/avatars/2025-09-04/2b664f51-f42c-4435-a1ea-3bc63f128d37.png b/uploads/avatars/2025-09-04/2b664f51-f42c-4435-a1ea-3bc63f128d37.png new file mode 100644 index 0000000..19b7f82 Binary files /dev/null and b/uploads/avatars/2025-09-04/2b664f51-f42c-4435-a1ea-3bc63f128d37.png differ diff --git a/uploads/avatars/2025-09-04/37c58631-8766-4c70-ab18-8c3f4c7349ba.png b/uploads/avatars/2025-09-04/37c58631-8766-4c70-ab18-8c3f4c7349ba.png new file mode 100644 index 0000000..8f744ba Binary files /dev/null and b/uploads/avatars/2025-09-04/37c58631-8766-4c70-ab18-8c3f4c7349ba.png differ diff --git a/uploads/avatars/2025-09-04/3aa7cd3a-5f03-48f1-9d52-e4134e48d639.png b/uploads/avatars/2025-09-04/3aa7cd3a-5f03-48f1-9d52-e4134e48d639.png new file mode 100644 index 0000000..8f744ba Binary files /dev/null and b/uploads/avatars/2025-09-04/3aa7cd3a-5f03-48f1-9d52-e4134e48d639.png differ diff --git a/uploads/avatars/2025-09-04/4daa0186-964a-4d8f-ab04-540655ce4cfe.png b/uploads/avatars/2025-09-04/4daa0186-964a-4d8f-ab04-540655ce4cfe.png new file mode 100644 index 0000000..8f744ba Binary files /dev/null and b/uploads/avatars/2025-09-04/4daa0186-964a-4d8f-ab04-540655ce4cfe.png differ diff --git a/uploads/avatars/2025-09-04/6963902f-10ee-4411-afef-fd5bf9593d35.jpg b/uploads/avatars/2025-09-04/6963902f-10ee-4411-afef-fd5bf9593d35.jpg new file mode 100644 index 0000000..b6eac22 Binary files /dev/null and b/uploads/avatars/2025-09-04/6963902f-10ee-4411-afef-fd5bf9593d35.jpg differ diff --git a/uploads/avatars/2025-09-04/72e47bef-83fa-4d35-beac-cbf3594fd416.png b/uploads/avatars/2025-09-04/72e47bef-83fa-4d35-beac-cbf3594fd416.png new file mode 100644 index 0000000..8f744ba Binary files /dev/null and b/uploads/avatars/2025-09-04/72e47bef-83fa-4d35-beac-cbf3594fd416.png differ diff --git a/uploads/avatars/2025-09-04/75ab6cb9-da64-40a2-a8f1-de1a2d63bbcc.jpg b/uploads/avatars/2025-09-04/75ab6cb9-da64-40a2-a8f1-de1a2d63bbcc.jpg new file mode 100644 index 0000000..f768a20 Binary files /dev/null and b/uploads/avatars/2025-09-04/75ab6cb9-da64-40a2-a8f1-de1a2d63bbcc.jpg differ