Merge remote-tracking branch 'origin/Connection-UI' into Connection-UI

# Conflicts:
#	src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java
#	src/main/java/org/to/telegramfinalproject/UI/MainController.java
This commit is contained in:
Asal Lotfi
2025-08-31 01:13:47 +03:30
8 changed files with 615 additions and 114 deletions
@@ -42,10 +42,21 @@ public class ActionHandler {
// private void handleRealTime(JSONObject json) throws IOException {
// IncomingMessageListener listener = new IncomingMessageListener(this.in);
// listener.handleRealTimeEvent (json);
// }
// ActionHandler.java
private void handleRealTime(JSONObject json) throws IOException {
IncomingMessageListener listener = new IncomingMessageListener(this.in);
listener.handleRealTimeEvent (json);
IncomingMessageListener listener = TelegramClient.getInstance().getListener();
if (listener != null) {
listener.handleRealTimeEvent(json);
} else {
System.err.println("[RT] Listener not ready; dropping RT event: " + json);
}
}
public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
this.out = out;
this.in = in;
@@ -1,21 +1,36 @@
package org.to.telegramfinalproject.Client;
import javafx.application.Platform;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.UI.MainController;
import java.io.BufferedReader;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
public class IncomingMessageListener implements Runnable {
private final BufferedReader in;
public enum UIMode { CONSOLE, UI }
private final UIMode uiMode; // runtime mode
public IncomingMessageListener(BufferedReader in) {
private final java.util.Set<String> seenMessageIds =
java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>());
public IncomingMessageListener(BufferedReader in, UIMode uiMode) {
this.in = in;
this.uiMode = uiMode;
}
// public IncomingMessageListener(BufferedReader in) {
// this.in = in;
// }
@Override
public void run() {
try {
@@ -79,7 +94,7 @@ public class IncomingMessageListener implements Runnable {
"update_group_or_channel", "chat_deleted",
"blocked_by_user", "unblocked_by_user", "message_seen",
"removed_from_group", "removed_from_channel",
"became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true;
"became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true;
default -> false;
};
}
@@ -88,13 +103,15 @@ public class IncomingMessageListener implements Runnable {
String action = response.getString("action");
JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject();
JSONObject finalMsg = msg;
JSONObject finalMsg1 = msg;
switch (action) {
case "added_to_group", "added_to_channel",
"removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> {
"removed_from_group", "removed_from_channel",
"chat_deleted", "created_private_chat" -> {
// این قسمت مستقل از UI/کنسول است
System.out.println("🔄 Chat list changed. Updating...");
Session.forceRefreshChatList = true;
System.out.println("🧪 Calling requestChatList() after being added");
String chatId = msg.getString("chat_id");
String chatType = msg.getString("chat_type");
@@ -106,36 +123,67 @@ public class IncomingMessageListener implements Runnable {
}
}
case "chat_updated" -> {
System.out.println("\n🔄 Chat info updated.");
// case "chat_updated" -> {
// if (uiMode == UIMode.UI) {
// bumpChatListFromUpdate(msg); // برای UI (سایدبار و سورت)
// } else {
// updateLastMessageTime(msg); // برای کنسول (لیست‌های Session)
// }
// }
if (msg.has("last_message_time")) {
updateLastMessageTime(msg);
} else {
new Thread(() -> {
try {
handleAdminRoleChanged(msg);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> {
case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> {
System.out.println("🧩 Detected admin/owner role change. Calling handler...");
new Thread(() -> {
try {
handleAdminRoleChanged(msg); //new thread
handleAdminRoleChanged(finalMsg1);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
case "new_message" -> {
JSONObject data = response.optJSONObject("data");
if (data == null) break;
default -> displayRealTimeMessage(action, msg);
String mid = data.optString("id", data.optString("message_id",""));
if (mid.isEmpty()) break;
if (!seenMessageIds.add(mid)) break;
bumpChatListFromMessage(data);
JSONObject uiMsg = new JSONObject(data.toString());
if (!uiMsg.has("message_id") && uiMsg.has("id")) {
uiMsg.put("message_id", uiMsg.getString("id"));
}
Platform.runLater(() -> {
var mc = MainController.getInstance();
var chatCtl = (mc != null) ? mc.getChatPageController() : null;
if (chatCtl != null) {
chatCtl.onRealTimeNewMessage(uiMsg);
}
});
}
case "chat_updated" -> {
var data = response.getJSONObject("data");
bumpChatListFromUpdate(data);
}
case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted",
"user_status_changed", "blocked_by_user", "unblocked_by_user", "message_seen" -> {
displayRealTimeMessage(action, msg);
}
default -> {
System.out.println("\n❓ Unknown real-time action: " + action);
System.out.println(msg.toString(2));
}
}
System.out.print(">> ");
@@ -362,4 +410,58 @@ public class IncomingMessageListener implements Runnable {
}
}
}
private static LocalDateTime parseIsoFlexible(String iso) {
if (iso == null || iso.isBlank()) return null;
try { return LocalDateTime.parse(iso); } catch (Exception ignore) {}
try { return OffsetDateTime.parse(iso).toLocalDateTime(); } catch (Exception ignore) {}
return null;
}
private void bumpChatListFromUpdate(JSONObject data) {
try {
UUID chatId = UUID.fromString(data.optString("chat_id",""));
String chatType = data.optString("chat_type","");
LocalDateTime ts = parseIsoFlexible(data.optString("last_message_time", null));
Platform.runLater(() -> {
var mc = MainController.getInstance();
if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ false, null);
});
} catch (Exception e) { System.err.println("[RT] bumpChatListFromUpdate: " + e.getMessage()); }
}
private String previewOf(String type, String content) {
String t = type == null ? "" : type.trim().toUpperCase();
return switch (t) {
case "IMAGE" -> "[Image]";
case "AUDIO" -> "[Audio]";
case "VIDEO" -> "[Video]";
case "FILE" -> "[File]";
default -> (content == null ? "" : content);
};
}
private void bumpChatListFromMessage(JSONObject m) {
try {
UUID chatId = UUID.fromString(m.optString("receiver_id",""));
String chatType = m.optString("receiver_type","");
LocalDateTime ts = parseIsoFlexible(m.optString("send_at", null));
String preview = previewOf(m.optString("message_type","TEXT"),
m.optString("content",""));
Platform.runLater(() -> {
var mc = MainController.getInstance();
if (mc != null) mc.onChatUpdated(chatId, chatType, ts, /*isIncoming*/ true, preview);
});
} catch (Exception e) { System.err.println("[RT] bumpChatListFromMessage: " + e.getMessage()); }
}
}
@@ -171,11 +171,22 @@ public class TelegramClient {
return instance;
}
// public void startConsole() {
// try {
// connectIfNeeded();
// initHandlerIfNeeded();
// startListenerOnce();
// showMainMenu();
// } catch (IOException e) {
// System.err.println("❌ Error connecting to server: " + e.getMessage());
// }
// }
public void startConsole() {
try {
connectIfNeeded();
initHandlerIfNeeded();
startListenerOnce();
startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // کنسول
showMainMenu();
} catch (IOException e) {
System.err.println("❌ Error connecting to server: " + e.getMessage());
@@ -185,14 +196,23 @@ public class TelegramClient {
public void start() { startConsole(); }
//UI only
// public static synchronized TelegramClient getOrInitForUI() throws IOException {
// TelegramClient cli = getInstance();
// cli.connectIfNeeded();
// cli.initHandlerIfNeeded();
// cli.startListenerOnce();
// return cli;
// }
public static synchronized TelegramClient getOrInitForUI() throws IOException {
TelegramClient cli = getInstance();
cli.connectIfNeeded();
cli.initHandlerIfNeeded();
cli.startListenerOnce();
cli.startListenerOnce(IncomingMessageListener.UIMode.UI); // UI
return cli;
}
private synchronized void connectIfNeeded() throws IOException {
if (socket != null && socket.isConnected() && !socket.isClosed()) return;
@@ -208,10 +228,23 @@ public class TelegramClient {
}
}
private void startListenerOnce() {
// private void startListenerOnce() {
// if (listenerStarted) return;
// listenerStarted = true;
// Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener");
// listenerThread.setDaemon(true);
// listenerThread.start();
// }
private void startListenerOnce(IncomingMessageListener.UIMode mode) {
if (listenerStarted) return;
listenerStarted = true;
Thread listenerThread = new Thread(new IncomingMessageListener(in), "socket-listener");
Thread listenerThread = new Thread(
new IncomingMessageListener(in, mode),
"socket-listener"
);
listenerThread.setDaemon(true);
listenerThread.start();
}
@@ -269,5 +302,13 @@ public class TelegramClient {
public static void main(String[] args) {
new TelegramClient().startConsole();
}
// TelegramClient.java
private IncomingMessageListener listener;
public IncomingMessageListener getListener() {
return listener;
}
}
@@ -128,8 +128,6 @@ public class ChatEntry {
this.archived = archived;
}
// public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime);
// }
public void setLastMessageTime(String newTime) {
@@ -173,4 +171,13 @@ public class ChatEntry {
public UUID getLastMessageSenderId() { return lastMessageSenderId; }
public void setLastMessageSenderId(UUID lastMessageSenderId) { this.lastMessageSenderId = lastMessageSenderId; }
public void setUnread(int unreadCount){this.unreadCount = unreadCount;}
public int getUnread(){return unreadCount;}
public void setLastMessageTime(LocalDateTime t) {
this.lastMessageTime = t;
}
}
@@ -1616,21 +1616,16 @@ public class ClientHandler implements Runnable {
break;
}
// دریافت شناسه چت و نوع حذف (یک‌طرفه یا دوطرفه)
UUID targetId = UUID.fromString(requestJson.getString("chat_id"));
boolean both = requestJson.getBoolean("both");
// Real-Time Event Dispatch (اطلاع‌رسانی ریل تایم)
if (both) {
// حذف دوطرفه
RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid()));
RealTimeEventDispatcher.notifyChatDeleted("private", currentUser.getInternal_uuid(), List.of(targetId));
} else {
// حذف یک‌طرفه
RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid()));
}
// فراخوانی متد حذف چت
response = handleDeleteChat(requestJson);
break;
}
@@ -2910,6 +2905,8 @@ public class ClientHandler implements Runnable {
RealTimeEventDispatcher.sendToUser(receiver, chatPayload);
}
RealTimeEventDispatcher.sendToUser(senderId, chatPayload);
JSONObject data = new JSONObject();
data.put("message_id", messageId.toString());
return new ResponseModel("success", "Message sent successfully.", data);
@@ -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/";
@@ -332,13 +334,124 @@ 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() {
@@ -643,14 +756,12 @@ public class ChatPageController {
boolean edited,
org.json.JSONArray reactions
) {
// meta (فرستنده + زمان [+ edited])
String metaText = (displayName == null ? "" : displayName) + "" + formatWhen(sentAt);
if (edited) metaText += " (edited)";
Label meta = new Label(metaText);
meta.setStyle("-fx-font-size: 11; -fx-text-fill: #7e8a97;");
meta.setWrapText(true);
// نرمال‌سازی نوع پیام و متن
String t = type == null ? "" : type.trim().toUpperCase();
boolean isText = t.isEmpty() ? (content != null && !content.isBlank()) : "TEXT".equals(t);
String bodyText = isText ? (content == null ? "" : content) : bracketLabel(t);
@@ -700,7 +811,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);
@@ -793,6 +903,51 @@ 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,12 +9,14 @@ 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.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle;
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;
@@ -83,6 +85,76 @@ 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) {
// اگر هیچ‌کجا نیست، بی‌خیال
boolean exists =
(Session.chatList != null && Session.chatList.stream().anyMatch(c -> chatId.equals(c.getId()))) ||
(Session.activeChats != null && Session.activeChats.stream().anyMatch(c -> chatId.equals(c.getId()))) ||
(Session.archivedChats != null && Session.archivedChats.stream().anyMatch(c -> chatId.equals(c.getId())));
if (!exists) return;
boolean isOpen = Session.currentChatId != null &&
Session.currentChatId.equals(chatId.toString());
forEachChat(chatId, chat -> {
chat.setLastMessageTime(lastTs);
if (lastPreview != null) chat.setLastMessagePreview(lastPreview);
if (isIncoming && !isOpen) chat.setUnreadCount(chat.getUnreadCount() + 1); // یکدست با unreadCount
});
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);
};
if (Session.chatList != null) Session.chatList.sort(byTimeDesc);
if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc);
if (Session.archivedChats != null) Session.archivedChats.sort(byTimeDesc);
refreshChatListUI();
refreshBadgesIfAny();
}
// MainController
private void forEachChat(UUID chatId, java.util.function.Consumer<ChatEntry> fn) {
java.util.List<java.util.List<ChatEntry>> lists = java.util.List.of(
Session.chatList != null ? Session.chatList : java.util.List.of(),
Session.activeChats != null ? Session.activeChats : java.util.List.of(),
Session.archivedChats != null ? Session.archivedChats : java.util.List.of()
);
for (var lst : lists) {
for (var c : lst) {
if (chatId.equals(c.getId())) {
fn.accept(c);
}
}
}
}
private void refreshChatListUI() {
Platform.runLater(() -> {
chatListContainer.getChildren().clear();
itemControllers.clear();
populateChatListFromSession(); // همون متدی که نودها رو می‌سازه و می‌چسبونه
});
}
private void refreshBadgesIfAny() {
// آپدیت نشان‌ها اگر لازم است
}
@FXML
public void initialize() {
// Populate chat list
@@ -146,6 +218,13 @@ public class MainController {
}
});
chatSearchResults.setOnMouseClicked(e -> {
int idx = chatSearchResults.getSelectionModel().getSelectedIndex();
if (idx >= 0 && idx < searchBacking.size()) {
openSearchResult(searchBacking.get(idx));
}
});
// Smooth scroll feel for global search results
globalSearchScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
@@ -258,7 +337,6 @@ public class MainController {
if (list == null || list.isEmpty()) return;
// جدا کردن Saved از بقیه
ChatEntry saved = null;
java.util.List<ChatEntry> others = new java.util.ArrayList<>();
for (ChatEntry c : list) {
@@ -303,6 +381,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"));
@@ -310,20 +412,20 @@ public class MainController {
ChatItemController cc = fx.getController();
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
String timeText = formatChatTime(chat.getLastMessageTime());
String timeText = chat.getLastMessageTime() == null
? ""
: formatChatTime(chat.getLastMessageTime());
// If chat has a profile picture, pass it; otherwise null
String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty())
? chat.getImageUrl()
: null;
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType());
item.setOnMouseClicked(e -> openChat(chat));
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType()); item.setOnMouseClicked(e -> openChat(chat));
chatListContainer.getChildren().add(item);
itemControllers.put(chat.getId(), cc);
} catch (Exception ex) {
ex.printStackTrace();
}
@@ -361,10 +463,11 @@ public class MainController {
ChatPageController controller = loader.getController();
controller.showChat(chat);
currentChatId = UUID.fromString(chat.getId().toString());
this.chatPageController = controller;
Session.currentChatId = chat.getId().toString();
chatDisplayArea.getChildren().setAll(chatPage);
chat.setUnreadCount(0);
ChatItemController item = itemControllers.get(chat.getId());
if (item != null) item.setUnread(0);
@@ -503,6 +606,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 {
@@ -523,7 +637,6 @@ public class MainController {
}
String toDisplay() {
// رشته‌ای که داخل ListView نشان می‌دهیم
switch (type) {
case MESSAGE:
String left = (title == null || title.isBlank()) ? "Message" : title;
@@ -665,7 +778,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
));
@@ -752,11 +865,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;
@@ -769,13 +881,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) {
@@ -792,7 +903,6 @@ public class MainController {
}
case MESSAGE: {
// مثل کنسول: چتِ پیام را باز کن (اسکرول به پیام را بعداً اضافه کن)
org.to.telegramfinalproject.Models.ChatEntry existing =
findExistingChat(r.uuid, r.receiverType);
if (existing != null) {
@@ -801,7 +911,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);
}
@@ -824,14 +934,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) {}
}
@@ -858,7 +966,6 @@ public class MainController {
}
private String guessNameForReceiver(SearchResult r) {
// فقط برای زمانی که موجودیت در لیست نبود
if (r.title != null && !r.title.isBlank()) return r.title;
if (r.receiverType != null) {
switch (r.receiverType) {
@@ -40,18 +40,16 @@ public class RegisterController {
@FXML
public void initialize() {
// Sync password and confirm fields with their visible counterparts
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty());
try {
connection = new ClientConnection("localhost", 8000);
} catch (Exception e) {
System.out.println("Could not connect to server: " + e.getMessage());
org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI();
} catch (Exception ignored) {
}
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty());
}
@FXML
@FXML
private void togglePasswordVisibility() {
passwordVisible = !passwordVisible;
visiblePasswordField.setVisible(passwordVisible);
@@ -71,66 +69,149 @@ public class RegisterController {
toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁");
}
// @FXML
// private void handleRegister() {
// String userID = userIdField.getText().trim();
// String username = usernameField.getText().trim();
// String profileName = profileNameField.getText().trim();
// String password = passwordField.getText();
// String confirmPass = confirmPasswordField.getText();
//
// userDatabase userDb = new userDatabase();
// String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b";
//
// // 1. Empty fields
// if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) {
// showError("Please fill in all required fields.");
// return;
// }
//
// // 2. Username exists
// if (userDb.existsByUsername(username)) {
// showError("This username is already taken.");
// return;
// }
//
// // 3. User ID exists
// if (userDb.existsByUserId(userID)) {
// showError("This user ID is already taken.");
// return;
// }
//
// // 4. Password mismatch
// if (!password.equals(confirmPass)) {
// showError("Passwords do not match.");
// return;
// }
//
// // 5. Weak password
// if (!password.matches(passwordRegex)) {
// showError("Password must be at least 8 characters, include a capital letter, a number, and a special character.");
// return;
// }
//
// // 6. Attempt registration
// try {
// JSONObject request = new JSONObject();
// request.put("action", "register");
// request.put("user_id", userID);
// request.put("username", username);
// request.put("password", password);
// request.put("profile_name", profileName);
// connection.send(request.toString());
//
// // Simulate successful registration (since main.fxml isnt ready)
// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!");
// alert.show();
//
// } catch (Exception ex) {
// showError("Failed to register. Please try again later.");
// }
// }
@FXML
private void handleRegister() {
String userID = userIdField.getText().trim();
String username = usernameField.getText().trim();
String userID = userIdField.getText().trim();
String username = usernameField.getText().trim();
String profileName = profileNameField.getText().trim();
String password = passwordField.getText();
String password = passwordField.getText();
String confirmPass = confirmPasswordField.getText();
userDatabase userDb = new userDatabase();
String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b";
// 1. Empty fields
// اعتبارسنجی‌ها
if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) {
showError("Please fill in all required fields.");
return;
showError("Please fill in all required fields."); return;
}
if (userDb.existsByUsername(username)) { showError("This username is already taken."); return; }
if (userDb.existsByUserId(userID)) { showError("This user ID is already taken."); return; }
if (!password.equals(confirmPass)) { showError("Passwords do not match."); return; }
if (!password.matches(passwordRegex)) { showError("Password must be at least 8 characters, include a capital letter, a number, and a special character."); return; }
// 2. Username exists
if (userDb.existsByUsername(username)) {
showError("This username is already taken.");
return;
}
// UI را موقتاً disable کن (اختیاری)
setBusy(true);
// 3. User ID exists
if (userDb.existsByUserId(userID)) {
showError("This user ID is already taken.");
return;
}
new Thread(() -> {
try {
// 1) مطمئن شو کانکشن/لیسنر روشن است
var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI();
var handler = cli.getHandler();
// 4. Password mismatch
if (!password.equals(confirmPass)) {
showError("Passwords do not match.");
return;
}
// 2) Register
var regReq = new org.json.JSONObject()
.put("action", "register")
.put("user_id", userID)
.put("username", username)
.put("password", password)
.put("profile_name", profileName);
// 5. Weak password
if (!password.matches(passwordRegex)) {
showError("Password must be at least 8 characters, include a capital letter, a number, and a special character.");
return;
}
var regResp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(regReq);
if (regResp == null || !"success".equalsIgnoreCase(regResp.optString("status"))) {
javafx.application.Platform.runLater(() -> {
setBusy(false);
showError(regResp != null ? regResp.optString("message","Register failed.") : "Register failed.");
});
return;
}
// 6. Attempt registration
try {
JSONObject request = new JSONObject();
request.put("action", "register");
request.put("user_id", userID);
request.put("username", username);
request.put("password", password);
request.put("profile_name", profileName);
connection.send(request.toString());
// 3) Auto-Login با همان کانکشن
handler.login(username, password);
// Simulate successful registration (since main.fxml isnt ready)
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Registration successful!");
alert.show();
javafx.application.Platform.runLater(() -> {
setBusy(false);
if (org.to.telegramfinalproject.Client.Session.currentUser == null || !handler.wasSuccess()) {
showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage());
return;
}
org.to.telegramfinalproject.UI.AppRouter.showMain();
});
} catch (Exception ex) {
showError("Failed to register. Please try again later.");
}
} catch (Exception ex) {
javafx.application.Platform.runLater(() -> {
setBusy(false);
showError("Failed to register/login: " + ex.getMessage());
});
}
}, "register-thread").start();
}
private void setBusy(boolean b){
userIdField.setDisable(b);
usernameField.setDisable(b);
profileNameField.setDisable(b);
passwordField.setDisable(b);
visiblePasswordField.setDisable(b);
confirmPasswordField.setDisable(b);
visibleConfirmPasswordField.setDisable(b);
togglePasswordBtn.setDisable(b);
toggleConfirmBtn.setDisable(b);
}
@FXML
private void switchToLogin() throws IOException {
showLogin();