Profile on sidebar

This commit is contained in:
2025-09-04 22:05:59 +03:30
parent e3df396758
commit 9e3b816ef2
15 changed files with 173 additions and 13 deletions
@@ -13,21 +13,48 @@ public final class AvatarLocalResolver {
} }
public static String resolve(String serverValue) { public static String resolve(String serverValue) {
if (serverValue == null || serverValue.isBlank()) return null; if (serverValue == null) return null;
if (isHttp(serverValue) || serverValue.startsWith("file:")) return serverValue; 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(); 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); System.err.println("Avatar resolve: NOT FOUND -> " + p);
return null; 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); System.out.println("Avatar resolve: " + serverValue + " -> " + url);
return 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) { public static Image load(String serverValue) {
String url = resolve(serverValue); String url = resolve(serverValue);
if (url == null) return null; if (url == null) return null;
@@ -1811,7 +1811,7 @@ private void addBubble(
if (days > 30) { if (days > 30) {
return "Last seen long time ago"; return "Last seen long time ago";
} }
return "recently"; return "Last seen recently";
} }
private void setDefaultHeaderAvatarByType(String type){ private void setDefaultHeaderAvatarByType(String type){
@@ -23,9 +23,16 @@ import org.to.telegramfinalproject.Client.Session;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; 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 { public class EditProfileController {
private File pickedAvatarFile;
@FXML private Pane overlayBackground; @FXML private Pane overlayBackground;
@FXML private VBox editCard; @FXML private VBox editCard;
@@ -108,13 +115,15 @@ public class EditProfileController {
File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow()); File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow());
if (file != null) { if (file != null) {
profileImageView.setImage(new Image(file.toURI().toString())); pickedAvatarFile = file; // ← ذخیره کن برای ارسال هنگام خروج
// TODO: save this new image to DB or server profileImageView.setImage(new Image(file.toURI().toString())); // پیش‌نمایش
} }
} }
private void closeEdit() { 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) { public void setProfileData(String name, String status, String bio, String userId, Image profileImage) {
@@ -135,6 +144,55 @@ public class EditProfileController {
boolean anyError = false; 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 --- // --- Update bio ---
String oldBio = currentUser.optString("bio", ""); String oldBio = currentUser.optString("bio", "");
if (!newBio.equals(oldBio)) { if (!newBio.equals(oldBio)) {
@@ -181,8 +239,14 @@ public class EditProfileController {
currentUser.optString("bio"), currentUser.optString("bio"),
currentUser.optString("user_id"), currentUser.optString("user_id"),
currentUser.optString("image_url", "/org/to/telegramfinalproject/Avatars/default_user_profile.png") 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(); 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
}
} }
@@ -1141,4 +1141,12 @@ public class MainController {
getChatPageController().showChat(entry); getChatPageController().showChat(entry);
} }
} }
public void refreshSidebarUserFromSession() {
if (sidebarController != null && Session.currentUser != null) {
Platform.runLater(() -> sidebarController.setUserFromSession(Session.currentUser));
}
}
} }
@@ -14,6 +14,12 @@ import javafx.scene.shape.Circle;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; 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 { public class MyProfileController {
@@ -68,6 +74,7 @@ public class MyProfileController {
// === Open edit profile === // === Open edit profile ===
editButton.setOnAction(e -> openEditProfile()); editButton.setOnAction(e -> openEditProfile());
} }
private void closeProfile() { private void closeProfile() {
@@ -91,9 +98,18 @@ public class MyProfileController {
this.userId.setText("@" + userId); this.userId.setText("@" + userId);
userID = userId; userID = userId;
if (imageUrl != null && !imageUrl.isEmpty()) { if (imageUrl != null && !imageUrl.isBlank()) {
profileImage.setImage(new Image(imageUrl)); 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 === // === Theme-specific updates ===
@@ -133,4 +149,5 @@ public class MyProfileController {
ex.printStackTrace(); ex.printStackTrace();
} }
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB