From 9d1737a90f2c8a865f6067ac3f9590d699e8fb2d Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 28 Aug 2025 23:27:03 +0330 Subject: [PATCH] Time Formatter --- .../UI/ChatPageController.java | 125 ++++++++++++------ .../UI/MainController.java | 78 ++++++++--- .../UI/SidebarMenuController.java | 31 +++++ 3 files changed, 171 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index f63b5b7..54e05e9 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -13,11 +13,16 @@ import javafx.stage.FileChooser; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Client.ActionHandler; +import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Models.ChatEntry; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.UUID; public class ChatPageController { @@ -78,15 +83,47 @@ public class ChatPageController { private void initCurrentUserId() { try { - // از سشنی که سمت کلاینت داری: String meStr = org.to.telegramfinalproject.Client.Session .currentUser.getString("internal_uuid"); me = UUID.fromString(meStr); } catch (Exception ignore) { - me = null; // اگر به هر دلیلی نبود، خروجی‌ها رو ورودی فرض نکن + me = null; } } + //Time formatter for messages + + private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); + private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; + + private String formatWhen(LocalDateTime ts) { + if (ts == null) return ""; + LocalDate today = LocalDate.now(); + LocalDate d = ts.toLocalDate(); + + if (d.isEqual(today)) { + return FMT_HHMM.format(ts); + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL + " " + FMT_HHMM.format(ts); + } else { + return FMT_DATE_TIME.format(ts); + } + } + + /** ISO → LocalDateTime (با پشتیبانی از Offset/Z) */ + private LocalDateTime parseWhen(String iso) { + if (iso == null || iso.isEmpty()) return null; + try { + return OffsetDateTime.parse(iso).toLocalDateTime(); + } catch (Exception ignore) { + try { + return LocalDateTime.parse(iso); + } catch (Exception e) { + return null; + } + } + } @FXML public void initialize() { @@ -327,10 +364,10 @@ public class ChatPageController { // Header chatTitle.setText(entry.getName()); - chatStatus.setText(""); // اگر last seen داری اینجا بگذار + chatStatus.setText(""); if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { try { - userAvatar.setImage(new Image(entry.getImageUrl())); // یا لود از ریسورس خودت + userAvatar.setImage(new Image(entry.getImageUrl())); userAvatar.setClip(new Circle(18, 18, 18)); } catch (Exception ignored) { } @@ -339,10 +376,8 @@ public class ChatPageController { messageContainer.getChildren().clear(); loadMessages(entry); - // مارک به‌عنوان خوانده markAsRead(entry); - // فوکوس روی ورودی Platform.runLater(() -> messageInput.requestFocus()); } @@ -363,7 +398,6 @@ public class ChatPageController { } if (resp == null) return; - // بدون opt* : String status = ""; try { status = resp.getString("status"); @@ -391,49 +425,48 @@ public class ChatPageController { } - private void renderMessages(JSONArray arr) { + private void renderMessages(org.json.JSONArray arr) { messageContainer.getChildren().clear(); - for (int i = 0; i < arr.length(); i++) { - JSONObject m = arr.getJSONObject(i); - String senderId = m.optString("sender_id", ""); - String type = m.optString("message_type", "TEXT"); - String content = m.optString("content", ""); - boolean outgoing = false; - if (me != null && senderId != null && !senderId.isEmpty()) { - try { - outgoing = me.equals(UUID.fromString(senderId)); - } catch (Exception ignore) { - } - } + String myId = Session.currentUser != null + ? Session.currentUser.optString("internal_uuid", "") + : ""; + + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject m = arr.getJSONObject(i); + + String senderId = m.has("sender_id") && !m.isNull("sender_id") ? m.getString("sender_id") : ""; + String senderName = m.has("sender_name") && !m.isNull("sender_name") ? m.getString("sender_name") : ""; + String type = m.has("message_type") && !m.isNull("message_type") ? m.getString("message_type") : "TEXT"; + String content = m.has("content") && !m.isNull("content") ? m.getString("content") : ""; + String whenStr = m.has("send_at") && !m.isNull("send_at") ? m.getString("send_at") : null; + + boolean outgoing = senderId.equalsIgnoreCase(myId); + + String display = outgoing ? "You" + : (!senderName.isEmpty() ? senderName + : (senderId.isEmpty() ? "Unknown" + : senderId.substring(0, Math.min(8, senderId.length())))); String text; switch (type.toUpperCase()) { - case "TEXT": - text = content; - break; - case "IMAGE": - text = "[Image]"; - break; - case "AUDIO": - text = "[Audio]"; - break; - case "VIDEO": - text = "[Video]"; - break; - case "FILE": - text = "[File]"; - break; - default: - text = "[Message]"; + case "IMAGE": text = "[Image]"; break; + case "AUDIO": text = "[Audio]"; break; + case "VIDEO": text = "[Video]"; break; + case "FILE": text = "[File]"; break; + default: text = content; break; } - addBubble(outgoing, text); + + LocalDateTime ts = parseWhen(whenStr); + addBubble(outgoing, display, text, ts); } + messageScrollPane.layout(); messageScrollPane.setVvalue(1.0); } + private void markAsRead(ChatEntry entry) { JSONObject readReq = new JSONObject(); readReq.put("action", "mark_as_read"); @@ -442,14 +475,19 @@ public class ChatPageController { ActionHandler.sendWithResponse(readReq); } - private void addBubble(boolean outgoing, String content) { + private void addBubble(boolean outgoing, String displayName, String content, LocalDateTime sentAt) { + + Label meta = new Label(displayName + " • " + formatWhen(sentAt)); + meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;"); + meta.setWrapText(true); + Label msg = new Label(content); msg.setWrapText(true); boolean dark = themeManager.isDarkMode(); - String mine = dark ? "#2b7cff" : "#d8ecff"; + String mine = dark ? "#2b7cff" : "#d8ecff"; String theirs = dark ? "#2c333a" : "#ffffff"; - String bg = outgoing ? mine : theirs; + String bg = outgoing ? mine : theirs; msg.setStyle( "-fx-background-color:" + bg + ";" + @@ -459,7 +497,9 @@ public class ChatPageController { ); msg.setMinHeight(Region.USE_PREF_SIZE); - javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(msg); + VBox bubble = new VBox(4, meta, msg); + + javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(bubble); row.setFillHeight(true); row.setSpacing(6); row.setAlignment(outgoing @@ -470,4 +510,5 @@ public class ChatPageController { } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 18e3422..65b7ea5 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -48,6 +48,8 @@ public class MainController { // === Sidebar === @FXML private Pane overlay; @FXML private ImageView menuIcon; + private SidebarMenuController sidebarController; //save sidebar controller + // === STATE === private static MainController instance; @@ -173,29 +175,32 @@ public class MainController { // } - private final DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm"); - private final DateTimeFormatter dayFmt = DateTimeFormatter.ofPattern("dd/MM"); // همون سال - private final DateTimeFormatter fullDateFmt= DateTimeFormatter.ofPattern("yyyy/MM/dd"); // سال متفاوت + private static final java.time.format.DateTimeFormatter FMT_HHMM = + java.time.format.DateTimeFormatter.ofPattern("HH:mm"); + private static final java.time.format.DateTimeFormatter FMT_DATE_TIME = + java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + private static final String YESTERDAY_LABEL = "Yesterday"; - private String formatListTimestamp(LocalDateTime ts) { + // ... + private String formatChatTime(java.time.LocalDateTime ts) { if (ts == null) return ""; - LocalDate today = LocalDate.now(); - LocalDate d = ts.toLocalDate(); + var today = java.time.LocalDate.now(); + var d = ts.toLocalDate(); if (d.isEqual(today)) { - // امروز → فقط ساعت - return timeFmt.format(ts); + //today + return FMT_HHMM.format(ts); + //yesterday + } else if (d.isEqual(today.minusDays(1))) { + return YESTERDAY_LABEL; + } else { + return FMT_DATE_TIME.format(ts); } - // اگر همان سال است → تاریخ کوتاه + ساعت - if (d.getYear() == today.getYear()) { - return dayFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 15/08 13:28 - } - // سال متفاوت → تاریخ کامل + ساعت - return fullDateFmt.format(ts) + " " + timeFmt.format(ts); // مثال: 2024/12/31 21:10 } + private void populateChatListFromSession() { chatListContainer.getChildren().clear(); @@ -239,9 +244,9 @@ public class MainController { ChatItemController cc = fx.getController(); String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview(); - String time = formatListTimestamp(chat.getLastMessageTime()); + String timeText = formatChatTime(chat.getLastMessageTime()); - cc.setChatData(chat.getName(), preview, time, chat.getUnreadCount()); + cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount()); item.setOnMouseClicked(e -> openChat(chat)); chatListContainer.getChildren().add(item); @@ -284,11 +289,10 @@ public class MainController { Node chatPage = loader.load(); ChatPageController controller = loader.getController(); - controller.showChat(chat); // ✅ متد جدید در ChatPageController + controller.showChat(chat); chatDisplayArea.getChildren().setAll(chatPage); - // اختیاری: صفر کردن badge و مارک‌کردن به‌عنوان خوانده در UI ChatItemController item = itemControllers.get(chat.getId()); if (item != null) item.setUnread(0); @@ -307,30 +311,62 @@ public class MainController { } } +// @FXML +// private void openSidebar() { +// try { +// if (sidebarRoot == null) { +// sidebarRoot = FXMLLoader.load(getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); +// StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT); +// sidebarRoot.setTranslateX(-getSidebarWidth()); +// mainRoot.getChildren().add(sidebarRoot); +// } +// +// TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot); +// slideIn.setToX(0); +// slideIn.play(); +// +// overlay.setVisible(true); +// overlay.setMouseTransparent(false); +// +// isSidebarOpen = true; +// +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + @FXML private void openSidebar() { try { if (sidebarRoot == null) { - sidebarRoot = FXMLLoader.load(getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/to/telegramfinalproject/Fxml/Sidebar_menu.fxml")); + sidebarRoot = loader.load(); + sidebarController = loader.getController(); + StackPane.setAlignment(sidebarRoot, Pos.CENTER_LEFT); sidebarRoot.setTranslateX(-getSidebarWidth()); mainRoot.getChildren().add(sidebarRoot); } + if (sidebarController != null && org.to.telegramfinalproject.Client.Session.currentUser != null) { + sidebarController.setUserFromSession( + org.to.telegramfinalproject.Client.Session.currentUser); + } + TranslateTransition slideIn = new TranslateTransition(Duration.millis(250), sidebarRoot); slideIn.setToX(0); slideIn.play(); overlay.setVisible(true); overlay.setMouseTransparent(false); - isSidebarOpen = true; - } catch (IOException e) { e.printStackTrace(); } } + @FXML private void closeSidebar() { if (sidebarRoot != null && isSidebarOpen) { diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java index 6cfb82a..5c63f81 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -12,6 +12,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Duration; +import org.json.JSONObject; import java.net.URL; @@ -184,5 +185,35 @@ public class SidebarMenuController { + public void setUserFromSession(JSONObject user) { + if (user == null) return; + + // نام نمایشی (اول profile_name بعد username) + String displayName = user.optString("profile_name", + user.optString("username", "")); + usernameLabel.setText(displayName); + + // عکس پروفایل: هم URL اینترنتی هم مسیر ریسورس را پشتیبانی کن + String img = user.optString("image_url", ""); + Image pic = tryLoadImage(img); + if (pic == null) { + pic = loadImage("/org/to/telegramfinalproject/Avatars/profile.png"); + } + if (pic != null) profileImage.setImage(pic); + } + + // کمک‌کننده: هم URL و هم ریسورس کلاس‌پث را امتحان می‌کند + private Image tryLoadImage(String src) { + if (src == null || src.isBlank()) return null; + try { + // اگر ریسورس داخل پروژه است (با / شروع شود یا در resources موجود باشد) + var res = getClass().getResource(src); + if (res != null) return new Image(res.toExternalForm(), true); + // در غیر این صورت فرض کن URL است (http/https/file) + return new Image(src, true); + } catch (Exception ignored) { + return null; + } + } }