Work on send message Real time in UI

This commit is contained in:
2025-08-30 19:43:58 +03:30
parent 60338fec52
commit edc61ccb96
7 changed files with 481 additions and 60 deletions
@@ -80,6 +80,8 @@ public class ChatPageController {
// ===== state =====
private String chatName;
private final ThemeManager themeManager = ThemeManager.getInstance();
private final java.util.Set<String> pendingClientMsgIds =
java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>());
// Where your icons live
private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/";
@@ -97,6 +99,7 @@ public class ChatPageController {
}
private void initCurrentUserId() {
try {
String meStr = org.to.telegramfinalproject.Client.Session
@@ -332,15 +335,128 @@ public class ChatPageController {
// ----- actions -----
// private void sendMessage() {
// String text = messageInput.getText() == null ? "" : messageInput.getText().trim();
// if (!text.isEmpty()) {
// addMessage("You", text);
// messageInput.clear();
// // TODO: send to server
// }
// }
private void sendMessage() {
String text = messageInput.getText() == null ? "" : messageInput.getText().trim();
if (!text.isEmpty()) {
addMessage("You", text);
messageInput.clear();
// TODO: send to server
// 0) Read and validate input
String raw = messageInput.getText();
String text = (raw == null) ? "" : raw.trim();
if (text.isEmpty()) return;
if (currentChat == null) {
addSystemMessage("No chat is selected.");
return;
}
// 1) Clear input immediately for good UX
messageInput.clear();
// 2) Snapshot chat info (must be final for lambdas)
final UUID targetChatId = currentChat.getId();
final String targetType = currentChat.getType(); // "private" | "group" | "channel"
final String contentToSend = text; // effectively final
// 3) Build the SAME JSON as your console method (for TEXT only)
org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "send_message");
req.put("receiver_type", targetType);
req.put("receiver_id", targetChatId.toString());
req.put("content", contentToSend);
req.put("message_type", "TEXT");
// 4) Send on a background thread
new Thread(() -> {
org.json.JSONObject resp;
try {
resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req);
} catch (Exception ex) {
ex.printStackTrace();
Platform.runLater(() -> addSystemMessage("Send failed: " + ex.getMessage()));
return;
}
// 5) Check status like your console method
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
String err = (resp != null) ? resp.optString("message", "No response") : "No response";
Platform.runLater(() -> addSystemMessage("Send failed: " + err));
return;
}
// 6) Extract fields (your console reads data.message_id; handle both shapes)
org.json.JSONObject data = resp.optJSONObject("data");
String messageId = null;
String sendAtIso = null;
if (data != null) {
// If server returns { data: { message_id, send_at, ... } }
messageId = data.optString("message_id", null);
// Some servers nest: { data: { message: {...} } }
if (messageId == null) {
org.json.JSONObject msgObj = data.optJSONObject("message");
if (msgObj != null) {
messageId = msgObj.optString("message_id", null);
sendAtIso = msgObj.optString("send_at", null);
}
} else {
sendAtIso = data.optString("send_at", null);
}
}
if (messageId == null) messageId = java.util.UUID.randomUUID().toString();
final java.time.LocalDateTime ts =
(sendAtIso != null && !sendAtIso.isBlank()) ? parseWhen(sendAtIso)
: java.time.LocalDateTime.now();
final String fMessageId = messageId;
final java.time.LocalDateTime fTs = ts;
// 7) Update UI on FX thread (render outgoing bubble + index for reply previews)
Platform.runLater(() -> {
// If user switched chats while sending, dont render here
if (currentChat == null || !currentChat.getId().equals(targetChatId)) return;
addBubble(
true, // outgoing
"You", // display name
"TEXT", // message type
contentToSend, // content
fTs, // timestamp
fMessageId, // message_id
"", "", "", // forwarded_from, forwarded_by, reply_to_id
false, // edited
null // reactions
);
//Real time
var mc = MainController.getInstance();
if (mc != null) {
String preview = "You: " + (contentToSend.isBlank() ? "[Message]" : contentToSend);
mc.onChatUpdated(targetChatId, targetType, fTs, /*isIncoming*/ false, preview);
}
// Keep it in msgIndex for reply previews
org.json.JSONObject idx = new org.json.JSONObject();
idx.put("message_id", fMessageId);
idx.put("message_type", "TEXT");
idx.put("content", contentToSend);
idx.put("sender_name", "You");
idx.put("sender_id", (me != null) ? me.toString() : "");
idx.put("send_at", fTs.toString());
msgIndex.put(fMessageId, idx);
});
}).start();
}
private void openFileChooser() {
FileChooser fc = new FileChooser();
fc.setTitle("Select a file to send");
@@ -706,7 +822,6 @@ public class ChatPageController {
row.setAlignment(outgoing ? javafx.geometry.Pos.CENTER_RIGHT
: javafx.geometry.Pos.CENTER_LEFT);
// کمی padding اطراف هر پیام برای کاهش فاصله‌های ناخوشایند
row.setPadding(new javafx.geometry.Insets(2, 6, 2, 6));
messageContainer.getChildren().add(row);
@@ -799,9 +914,64 @@ public class ChatPageController {
}
private static boolean notBlank(String s) { return s != null && !s.isBlank(); }
private static String ellipsize(String s, int max) { return s.length() > max ? s.substring(0, max) + "" : s; }
public boolean isSameChat(UUID chatId, String type) {
return currentChat != null
&& currentChat.getId().equals(chatId)
&& currentChat.getType().equalsIgnoreCase(type);
}
public void onRealTimeNewMessage(JSONObject m) {
try {
String chatIdStr = str(m,"receiver_id");
String chatType = str(m,"receiver_type");
if (chatIdStr.isEmpty() || chatType.isEmpty()) return;
UUID chatId = UUID.fromString(chatIdStr);
if (!isSameChat(chatId, chatType)) {
System.out.println("[UI] RT msg for another chat: " + chatId);
return;
}
// id → message_id fallback
if (!m.has("message_id") && m.has("id")) {
m.put("message_id", m.getString("id"));
}
String senderName = hasVal(str(m,"sender_name")) ? str(m,"sender_name")
: (hasVal(str(m,"sender_id")) ? shortId(str(m,"sender_id")) : "Unknown");
String type = hasVal(str(m,"message_type")) ? str(m,"message_type") : "TEXT";
String content = str(m,"content");
String whenIso = str(m,"send_at");
String msgId = str(m,"message_id");
LocalDateTime ts = parseWhen(whenIso);
if (ts == null) ts = LocalDateTime.now();
addBubble(false, senderName, type, content, ts, msgId,
str(m,"forwarded_from"), str(m,"forwarded_by"), str(m,"reply_to_id"),
bool(m,"is_edited"), arr(m,"reactions"));
if (hasVal(msgId)) msgIndex.put(msgId, m);
if (currentChat != null) markAsRead(currentChat);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -9,10 +9,12 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.IOException;
@@ -75,6 +77,62 @@ public class MainController {
private final Map<UUID, ChatItemController> itemControllers = new HashMap<>();
//For realtime handling
public void onChatUpdated(UUID chatId,
String chatType,
LocalDateTime lastTs,
boolean isIncoming,
String lastPreview) {
var opt = java.util.stream.Stream
.of(Session.chatList, Session.activeChats, Session.archivedChats)
.flatMap(java.util.Collection::stream)
.filter(c -> c.getId().equals(chatId))
.findFirst();
if (opt.isEmpty()) return;
ChatEntry chat = opt.get();
if (lastTs != null) chat.setLastMessageTime(String.valueOf(lastTs)); // ← نوع: LocalDateTime
if (lastPreview != null) chat.setLastMessagePreview(lastPreview);
boolean isOpen = Session.currentChatId != null &&
Session.currentChatId.equals(chatId.toString());
if (isIncoming && !isOpen) {
chat.setUnread(chat.getUnread() + 1);
}
java.util.Comparator<ChatEntry> byTimeDesc = (c1, c2) -> {
var t1 = c1.getLastMessageTime();
var t2 = c2.getLastMessageTime();
if (t1 == null && t2 == null) return 0;
if (t1 == null) return 1;
if (t2 == null) return -1;
return t2.compareTo(t1);
};
Session.chatList.sort(byTimeDesc);
Session.activeChats.sort(byTimeDesc);
Session.archivedChats.sort(byTimeDesc);
refreshChatListUI();
refreshBadgesIfAny();
}
private void refreshChatListUI() {
Platform.runLater(() -> {
chatListContainer.getChildren().clear();
itemControllers.clear();
populateChatListFromSession(); // همون متدی که نودها رو می‌سازه و می‌چسبونه
});
}
private void refreshBadgesIfAny() {
// آپدیت نشان‌ها اگر لازم است
}
@FXML
public void initialize() {
// addSampleChats();
@@ -270,6 +328,30 @@ public class MainController {
// }
// }
// private void addChatNode(ChatEntry chat) {
// try {
// FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
// Node item = fx.load();
// ChatItemController cc = fx.getController();
//
// String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
// String timeText = formatChatTime(chat.getLastMessageTime());
// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
// Node chatItem = loader.load();
// ChatItemController controller = loader.getController();
//
// cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png");
// item.setOnMouseClicked(e -> openChat(chat));
// chatListContainer.getChildren().add(item);
//
// itemControllers.put(chat.getId(), cc);
//
// } catch (Exception ex) {
// ex.printStackTrace();
// }
// }
private void addChatNode(ChatEntry chat) {
try {
FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
@@ -277,22 +359,30 @@ public class MainController {
ChatItemController cc = fx.getController();
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
String timeText = formatChatTime(chat.getLastMessageTime());
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
Node chatItem = loader.load();
ChatItemController controller = loader.getController();
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), "/org/to/telegramfinalproject/Avatars/default_profile.png");
// اگر lastMessageTime از نوع LocalDateTime است:
String timeText = chat.getLastMessageTime() == null
? ""
: formatChatTime(chat.getLastMessageTime());
cc.setChatData(
chat.getName(),
preview,
timeText,
chat.getUnreadCount(),
"/org/to/telegramfinalproject/Avatars/default_profile.png"
);
item.setOnMouseClicked(e -> openChat(chat));
chatListContainer.getChildren().add(item);
itemControllers.put(chat.getId(), cc);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private String mapTypeToLabel(String t) {
switch (t.toUpperCase()) {
case "IMAGE": return "[Image]";
@@ -325,6 +415,9 @@ public class MainController {
ChatPageController controller = loader.getController();
controller.showChat(chat);
this.chatPageController = controller;
Session.currentChatId = chat.getId().toString();
chatDisplayArea.getChildren().setAll(chatPage);
ChatItemController item = itemControllers.get(chat.getId());
@@ -463,6 +556,17 @@ public class MainController {
// ===== Search UI state =====
private final java.util.List<SearchResult> searchBacking = new java.util.ArrayList<>();
@FXML private AnchorPane chatHost; // کانتینر مناسب در مین برای قرار دادن ChatPage
private ChatPageController chatPageController;
public ChatPageController getChatPageController() {
return chatPageController;
}
private enum SRType { USER, GROUP, CHANNEL, MESSAGE }
private static class SearchResult {
@@ -483,7 +587,6 @@ public class MainController {
}
String toDisplay() {
// رشته‌ای که داخل ListView نشان می‌دهیم
switch (type) {
case MESSAGE:
String left = (title == null || title.isBlank()) ? "Message" : title;
@@ -512,7 +615,6 @@ public class MainController {
showSearchPanel();
// درخواست به سرور (مثل کنسول)
org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "search");
req.put("keyword", keyword);
@@ -582,7 +684,7 @@ public class MainController {
String subtitle = (ctx.isBlank()? "" : ctx) + (content.isBlank()? "" : (subtitleSep(ctx)+content));
tmp.add(new SearchResult(
SRType.MESSAGE, senderName, subtitle, rType, rUuid,
it.optString("receiver_display_id", ""), // اگر داشتی
it.optString("receiver_display_id", ""),
it.optString("message_id", null),
time
));
@@ -612,11 +714,10 @@ public class MainController {
private void openSearchResult(SearchResult r) {
switch (r.type) {
case USER: {
// مثل کنسول: اگر چت private با این یوزر داریم بازش کن؛
// وگرنه chat_id را از سرور بگیر/بساز، بعد باز کن.
java.util.UUID chatId = findExistingPrivateChatId(r.uuid);
if (chatId == null) {
chatId = fetchOrCreatePrivateChat(r.uuid); // نیاز به API سمت سرور
chatId = fetchOrCreatePrivateChat(r.uuid);
if (chatId == null) {
System.out.println("❌ Failed to create/find private chat.");
return;
@@ -629,13 +730,12 @@ public class MainController {
ce.setName(r.title); // profile_name
ce.setType("private");
openChat(ce); // همون متد فعلی MainController
openChat(ce);
break;
}
case GROUP:
case CHANNEL: {
// اگر تو لیست هست بازش کن، وگرنه با همون uuid باز کن
org.to.telegramfinalproject.Models.ChatEntry existing =
findExistingChat(r.uuid, r.receiverType);
if (existing != null) {
@@ -652,7 +752,6 @@ public class MainController {
}
case MESSAGE: {
// مثل کنسول: چتِ پیام را باز کن (اسکرول به پیام را بعداً اضافه کن)
org.to.telegramfinalproject.Models.ChatEntry existing =
findExistingChat(r.uuid, r.receiverType);
if (existing != null) {
@@ -661,7 +760,7 @@ public class MainController {
org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry();
ce.setId(r.uuid.toString()); // receiver internal_uuid
ce.setType(r.receiverType);
ce.setName(guessNameForReceiver(r)); // اگر خواستی از subtitle استفاده کن
ce.setName(guessNameForReceiver(r));
ce.setDisplayId(r.displayId);
openChat(ce);
}
@@ -684,14 +783,12 @@ public class MainController {
}
private java.util.UUID findExistingPrivateChatId(java.util.UUID otherUserUuid) {
// اگر در لیست چت‌ها private با همین طرف داری و internal_id همان chat_id است، برش گردان
var list = (org.to.telegramfinalproject.Client.Session.chatList==null)
? java.util.Collections.<org.to.telegramfinalproject.Models.ChatEntry>emptyList()
: org.to.telegramfinalproject.Client.Session.chatList;
for (var c : list) {
if ("private".equalsIgnoreCase(c.getType())) {
// اگر مدل‌ات otherUserId در ChatEntry دارد، از آن استفاده کن
if (otherUserUuid.equals(c.getOtherUserId())) {
try { return java.util.UUID.fromString(c.getId().toString()); } catch (Exception ignored) {}
}