Merge pull request #19 from PartowRoshani/Main-UI

Main UI
This commit is contained in:
2025-09-08 17:46:26 +03:30
committed by GitHub
310 changed files with 35935 additions and 57 deletions
Generated
+1
View File
@@ -0,0 +1 @@
Telegram-Final-Project
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
+3
View File
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
+8 -1
View File
@@ -26,7 +26,7 @@ tasks.withType(JavaCompile) {
application {
mainModule = 'org.to.telegramfinalproject'
mainClass = 'org.to.telegramfinalproject.HelloApplication'
mainClass = 'org.to.telegramfinalproject.Server.MainServer'
}
javafx {
@@ -40,13 +40,20 @@ dependencies {
implementation('com.dlsc.formsfx:formsfx-core:11.6.0') {
exclude group: 'org.openjfx'
}
implementation 'org.postgresql:postgresql:42.7.3'
implementation('net.synedra:validatorfx:0.5.0') {
exclude group: 'org.openjfx'
}
//For upload files
implementation 'org.slf4j:slf4j-simple:2.0.13'
implementation 'com.sparkjava:spark-core:2.9.4'
implementation 'com.mpatric:mp3agic:0.9.1' //for mp3
implementation 'org.json:json:20231013'
implementation 'org.kordamp.ikonli:ikonli-javafx:12.3.1'
implementation 'org.kordamp.bootstrapfx:bootstrapfx-core:0.4.0'
implementation('eu.hansolo:tilesfx:21.0.3') {
exclude group: 'org.openjfx'
}
test {
+21 -3
View File
@@ -1,14 +1,32 @@
module org.to.telegramfinalproject {
requires javafx.controls;
requires javafx.fxml;
requires javafx.graphics; // بهتره اضافه باشه
requires javafx.media;
requires javafx.web;
requires org.json;
requires java.sql;
requires java.desktop;
// لایبرری‌های جانبی — اگر واقعاً در مسیر ماژول هستند
requires org.controlsfx.controls;
requires com.dlsc.formsfx;
requires net.synedra.validatorfx;
requires org.kordamp.ikonli.javafx;
requires org.kordamp.bootstrapfx.core;
requires eu.hansolo.tilesfx;
opens org.to.telegramfinalproject to javafx.fxml;
exports org.to.telegramfinalproject;
}
requires mp3agic;
requires spark.core; // فقط اگر واقعاً استفاده می‌کنی
requires javax.servlet.api; // فقط اگر واقعاً استفاده می‌کنی
// کنترلرها/کلاس‌هایی که FXML به آن‌ها دسترسی بازتابی دارد
opens org.to.telegramfinalproject.UI to javafx.fxml;
opens org.to.telegramfinalproject.Client to javafx.fxml;
opens org.to.telegramfinalproject.Models to javafx.fxml; // اگر مدل‌ها داخل FXML مصرف می‌شوند
// اگر پکیج‌هایی را می‌خواهی خارج از ماژول در دسترس قرار دهی
exports org.to.telegramfinalproject.UI;
exports org.to.telegramfinalproject.Client;
exports org.to.telegramfinalproject.Models;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
package org.to.telegramfinalproject.Client;
import javafx.scene.image.Image;
import java.nio.file.*;
public final class AvatarLocalResolver {
private static final Path UPLOADS_ROOT = Paths.get(
System.getProperty("app.uploads.root", "uploads")
).toAbsolutePath().normalize();
private static boolean isHttp(String s) {
return s.startsWith("http://") || s.startsWith("https://");
}
public static String resolve(String serverValue) {
if (serverValue == null) return null;
serverValue = serverValue.trim();
if (serverValue.isEmpty()) return null;
// 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();
// جلوگیری از خروج از دایرکتوری uploads (path traversal)
if (!p.startsWith(UPLOADS_ROOT)) return null;
// وجود واقعی فایل
if (!Files.exists(p)) {
System.err.println("Avatar resolve: NOT FOUND -> " + p);
return null;
}
// خروجی به صورت 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;
// فقط برای http/https کش‌بریکر
if (isHttp(url)) {
url += (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis();
}
Image img = new Image(url, false); // sync load تا خطا را همان‌جا بفهمیم
if (img.isError()) {
System.err.println("Avatar load failed: " + url + " -> " + img.getException());
return null;
}
return img;
}
}
@@ -0,0 +1,30 @@
package org.to.telegramfinalproject.Client;
import java.io.*;
import java.net.Socket;
public class ClientConnection {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public ClientConnection(String host, int port) throws IOException {
this.socket = new Socket(host, port);
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
this.out = new PrintWriter(socket.getOutputStream(), true);
}
public void send(String request) {
out.println(request);
}
public String receive() throws IOException {
return in.readLine();
}
public void close() throws IOException {
socket.close();
in.close();
out.close();
}
}
@@ -0,0 +1,30 @@
// DownloadIndexRegistry.java
package org.to.telegramfinalproject.Client;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class DownloadIndexRegistry {
private static final ConcurrentHashMap<UUID, DownloadsIndex> INSTANCES = new ConcurrentHashMap<>();
private static volatile boolean HOOK_REGISTERED = false;
private DownloadIndexRegistry() {}
public static DownloadsIndex forAccount(UUID accountId) {
registerHookOnce();
return INSTANCES.computeIfAbsent(accountId, DownloadsIndex::new);
}
public static void closeAccount(UUID accountId) {
DownloadsIndex idx = INSTANCES.remove(accountId);
if (idx != null) idx.saveQuietly();
}
private static synchronized void registerHookOnce() {
if (HOOK_REGISTERED) return;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
for (DownloadsIndex idx : INSTANCES.values()) idx.saveQuietly();
}));
HOOK_REGISTERED = true;
}
}
@@ -0,0 +1,116 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class DownloadsIndex {
private final UUID accountId;
private final Path indexFile;
private final Map<UUID, Entry> map = new ConcurrentHashMap<>();
public DownloadsIndex(UUID accountId) {
this.accountId = accountId;
this.indexFile = resolveIndexPath(accountId.toString());
load();
}
private static Path resolveIndexPath(String accountId) {
String os = System.getProperty("os.name", "").toLowerCase();
String home = System.getProperty("user.home");
Path dir;
if (os.contains("win")) {
String appData = System.getenv("APPDATA");
dir = (appData != null)
? Paths.get(appData, "TeleSock")
: Paths.get(home, "AppData", "Roaming", "TeleSock");
} else {
dir = Paths.get(home, ".telesock");
}
try { Files.createDirectories(dir); } catch (IOException ignored) {}
return dir.resolve("downloads-index-" + accountId + ".json");
}
private synchronized void load() {
map.clear();
try {
if (!Files.exists(indexFile)) return;
String json = Files.readString(indexFile, StandardCharsets.UTF_8);
if (json == null || json.isBlank()) return;
JSONObject root = new JSONObject(json);
JSONObject items = root.optJSONObject("items");
if (items == null) return;
for (String key : items.keySet()) {
JSONObject e = items.getJSONObject(key);
map.put(UUID.fromString(key), new Entry(
e.getString("path"),
e.optLong("size", 0L),
e.optLong("ts", System.currentTimeMillis())
));
}
} catch (Exception e) {
System.err.println("⚠️ DownloadsIndex load failed: " + e.getMessage());
}
}
private synchronized void save() throws IOException {
JSONObject items = new JSONObject();
for (Map.Entry<UUID, Entry> it : map.entrySet()) {
JSONObject e = new JSONObject();
e.put("path", it.getValue().path);
e.put("size", it.getValue().size);
e.put("ts", it.getValue().ts);
items.put(it.getKey().toString(), e);
}
byte[] data = new JSONObject().put("items", items).toString(2).getBytes(StandardCharsets.UTF_8);
Path tmp = indexFile.resolveSibling(indexFile.getFileName() + ".tmp");
Files.write(tmp, data);
try {
Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException ex) {
Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING);
}
}
public void saveQuietly() {
try { save(); } catch (Exception ignored) {}
}
public Path find(UUID mediaKey) {
Entry e = map.get(mediaKey);
if (e == null) return null;
Path p = Paths.get(e.path);
if (Files.exists(p)) return p;
map.remove(mediaKey);
saveQuietly();
return null;
}
public void put(UUID mediaKey, Path path, long size) {
map.put(mediaKey, new Entry(path.toString(), size, System.currentTimeMillis()));
saveQuietly();
}
public void remove(UUID mediaKey) {
map.remove(mediaKey);
saveQuietly();
}
private static final class Entry {
final String path; final long size; final long ts;
Entry(String path, long size, long ts) {
this.path = path; this.size = size; this.ts = ts;
}
}
}
@@ -0,0 +1,38 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONObject;
import java.io.BufferedReader;
//public class EventProcessorThread extends Thread {
// private final ActionHandler handler;
// private final BufferedReader in;
//
// public EventProcessorThread(ActionHandler handler, BufferedReader in) {
// this.handler = handler;
// this.in = in;
// setDaemon(true);
// }
//
// @Override
// public void run() {
// try {
// System.out.println("👂 Real-Time Listener started.");
// String line;
// while ((line = in.readLine()) != null) {
// JSONObject json = new JSONObject(line);
// System.out.println("📥 Received raw line: " + line);
//
// if (json.has("action")) {
// // پیام real-time
// handler.processIncomingEvent(json);
// } else {
// // پیام پاسخ معمولی
// TelegramClient.responseQueue.put(json);
// }
// }
// } catch (Exception e) {
// System.err.println("❌ Error in EventProcessorThread: " + e.getMessage());
// }
// }
//}
@@ -0,0 +1,658 @@
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
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 {
System.out.println("👂 Real-Time Listener started.");
while (true) {
if (TelegramClient.mediaBusy.get()) {
try { Thread.sleep(15); } catch (InterruptedException ignored) {}
continue;
}
String line = in.readLine();
if (line == null) break;
if (line.isBlank()) continue;
final JSONObject response;
try {
response = new JSONObject(line);
} catch (Exception badJson) {
System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line);
continue;
}
System.out.println("📥 Received raw line: " + line);
// 1) اول message_id را روت کن (برای ACK نهایی مدیا)
String mid = response.optString("message_id", "");
if (!mid.isEmpty()) {
BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid);
if (q != null) {
q.put(response);
continue; // مصرف شد
}
}
// 2) بعد request_id را روت کن (برای INIT و بقیه درخواست‌ها)
if (response.has("request_id")) {
String requestId = response.getString("request_id");
System.out.println("📬 Response with request_id: " + requestId);
System.out.println("📬 Full response: " + response.toString(2));
BlockingQueue<JSONObject> queue = TelegramClient.pendingResponses.get(requestId);
if (queue != null) {
queue.put(response);
} else {
System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue...");
TelegramClient.responseQueue.put(response);
}
continue;
}
// 3) رویدادهای real-time
if (response.has("action")) {
String action = response.getString("action");
System.out.println("🎯 [Listener] Action received: " + response.toString(2));
System.out.println("🎯 Received action: " + action);
if (isRealTimeEvent(action)) {
handleRealTimeEvent(response);
} else {
TelegramClient.responseQueue.put(response);
}
continue;
}
// 4) سایر پاسخ‌های عمومی
if (response.has("status") && response.has("message")) {
TelegramClient.responseQueue.put(response);
} else {
TelegramClient.responseQueue.put(response); // fallback
}
}
} catch (Exception e) {
System.out.println("🔴 [Listener] Crashed due to: " + e.getMessage());
e.printStackTrace();
}
}
private boolean isRealTimeEvent(String action) {
return switch (action) {
case "new_message",
"message_edited",
"message_deleted_global", "message_deleted_one_sided", "message_deleted",
"message_reacted", "message_unreacted",
"user_status_changed",
"added_to_group", "added_to_channel",
"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",
"chat_updated" -> true;
default -> false;
};
}
void handleRealTimeEvent(JSONObject response) throws IOException {
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" -> {
// // این قسمت مستقل از UI/کنسول است
// System.out.println("🔄 Chat list changed. Updating...");
// Session.forceRefreshChatList = true;
//
// String chatId = msg.getString("chat_id");
// String chatType = msg.getString("chat_type");
// ActionHandler.requestChatInfo(chatId, chatType);
//
// if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) {
// System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting...");
// ActionHandler.forceExitChat = true;
// }
// }
// case "chat_updated" -> {
// if (uiMode == UIMode.UI) {
// bumpChatListFromUpdate(msg); // برای UI (سایدبار و سورت)
// } else {
// updateLastMessageTime(msg); // برای کنسول (لیست‌های Session)
// }
// }
case "added_to_group":
case "added_to_channel":
case "created_private_chat": {
// داده‌ها
UUID chatId = UUID.fromString(msg.getString("chat_id"));
String type = msg.getString("chat_type"); // "group" | "channel" | "private"
String name = msg.optString("name", "");
String imgUrl = msg.optString("image_url", "");
// یک ChatEntry مینیمال بساز (تا UI سریع واکنش بده)
ChatEntry ce = new ChatEntry();
ce.setId(chatId.toString());
ce.setType(type);
ce.setName(name);
ce.setImageUrl(imgUrl);
Platform.runLater(() -> {
var mc = MainController.getInstance();
if (mc == null) return;
// به لیست‌ها اضافه و UI را رفرش می‌کند (متد خودت)
mc.onJoinedOrAdded(ce);
// اگر همین چت الان بازه، مود مناسب را اعمال کن
var cpc = mc.getChatPageController();
if (cpc != null && cpc.isSameChat(chatId, type)) {
// ❗ اگر applyMode در ChatPageController private است،
// یا publicش کن یا این دو خط را حذف کن.
// گروه → NORMAL ، کانال → READ_ONLY (مگر اینکه اجازه پست داشته باشی)
// cpc.applyMode("group".equalsIgnoreCase(type) ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY);
// cpc.fetchAndRenderHeader(ce); // اختیاری: هدر را تازه کن
}
});
break;
}
case "removed_from_group":
case "removed_from_channel": {
UUID chatId = UUID.fromString(msg.getString("chat_id"));
String type = msg.getString("chat_type");
Platform.runLater(() -> {
var mc = MainController.getInstance();
if (mc == null) return;
removeFromAllLists(chatId);
mc.refreshChatListUI();
// اگر همین چت باز است → به حالت نیاز به Join برگرد
var cpc = mc.getChatPageController();
if (cpc != null && cpc.isSameChat(chatId, type)) {
// اگر applyMode private است، این خط را کامنت کن یا publicش کن
// cpc.applyMode(ChatViewMode.NEEDS_JOIN);
}
});
break;
}
case "chat_deleted": {
UUID chatId = UUID.fromString(msg.getString("chat_id"));
String type = msg.getString("chat_type");
Platform.runLater(() -> {
var mc = MainController.getInstance();
if (mc == null) return;
removeFromAllLists(chatId);
mc.refreshChatListUI();
var cpc = mc.getChatPageController();
if (cpc != null && cpc.isSameChat(chatId, type)) {
// حداقل ورودی را ببندیم/غیرفعال کنیم
// اگر applyMode private است، این خط را کامنت کن یا publicش کن
// cpc.applyMode(ChatViewMode.READ_ONLY);
// و یک پیام سیستمی هم نشان بده
cpc.addSystemMessage("This chat was deleted.");
}
});
break;
}
case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" : {
System.out.println("🧩 Detected admin/owner role change. Calling handler...");
new Thread(() -> {
try {
handleAdminRoleChanged(finalMsg1);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
break;
}
case "new_message" :{
JSONObject data = response.optJSONObject("data");
if (data == null) break;
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);
}
});
break;
}
case "message_edited": {
JSONObject ui = normalizeMessageId(msg);
// (اختیاری) اگر ایونت زمان و چت را هم می‌دهد، می‌توانی چت‌لیست را آپدیت کنی
Platform.runLater(() -> {
var mc = MainController.getInstance();
var chatCtl = (mc != null) ? mc.getChatPageController() : null;
if (chatCtl != null) chatCtl.onRealTimeMessageEdited(ui);
});
break;
}
case "message_deleted_global", "message_deleted_one_sided", "message_deleted" : {
JSONObject ui = normalizeMessageId(msg);
Platform.runLater(() -> {
var mc = MainController.getInstance();
var chatCtl = (mc != null) ? mc.getChatPageController() : null;
if (chatCtl != null) chatCtl.onRealTimeMessageDeleted(ui);
});
break;
}
case "message_reacted", "message_unreacted" : {
JSONObject ui = normalizeMessageId(msg);
Platform.runLater(() -> {
var mc = MainController.getInstance();
var chatCtl = (mc != null) ? mc.getChatPageController() : null;
if (chatCtl != null) chatCtl.onRealTimeReaction(ui);
});
break;
}
case "chat_updated": {
var data = response.getJSONObject("data");
bumpChatListFromUpdate(data);
break;
}
case "user_status_changed" : {
displayRealTimeMessage(action, msg);
Platform.runLater(() -> {
var mc = org.to.telegramfinalproject.UI.MainController.getInstance();
var cp = (mc != null) ? mc.getChatPageController() : null;
if (cp != null) cp.onUserStatusChanged(
msg.optString("user_id",""),
msg.optString("status",""),
msg.optString("last_seen","")
);
});
break;
}
case "blocked_by_user", "unblocked_by_user", "message_seen" : {
displayRealTimeMessage(action, msg);
break;
}
default :{
System.out.println("\n❓ Unknown real-time action: " + action);
System.out.println(msg.toString(2));
}
}
System.out.print(">> ");
}
private void updateLastMessageTime(JSONObject msg) {
try {
UUID chatUUID = UUID.fromString(msg.getString("chat_id"));
String newTime = msg.optString("last_message_time", null);
Session.chatList.stream()
.filter(chat -> chat.getId().equals(chatUUID))
.findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.activeChats.stream()
.filter(chat -> chat.getId().equals(chatUUID))
.findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.archivedChats.stream()
.filter(chat -> chat.getId().equals(chatUUID))
.findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.chatList.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
Session.activeChats.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
Session.archivedChats.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
if (Session.inChatListMenu) {
ActionHandler.displayChatList();
System.out.print("Select a chat by number: ");
}
} catch (Exception e) {
System.out.println("❌ Failed to update last message time: " + e.getMessage());
}
}
private void handleAdminRoleChanged(JSONObject data) throws IOException {
String chatType = data.getString("chat_type");
String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null)));
if (chatId == null) {
System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2));
return;
}
System.out.println("\n🔄 Your admin status changed. Updating chat info...");
try {
// 1. get chat info
JSONObject chatInfoReq = new JSONObject();
chatInfoReq.put("action", "get_chat_info");
chatInfoReq.put("receiver_id", chatId);
chatInfoReq.put("receiver_type", chatType);
System.out.println("📤 Sending get_chat_info: " + chatInfoReq);
JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq);
JSONObject chatData = chatInfoResp.getJSONObject("data");
UUID chatUUID = UUID.fromString(chatData.getString("internal_id"));
Optional<ChatEntry> entry = Session.chatList.stream()
.filter(e -> e.getId().equals(chatUUID))
.findFirst();
if (entry.isEmpty()) {
System.out.println("❌ Chat not found in session.");
return;
}
entry.ifPresent(chat -> {
chat.setAdmin(chatData.optBoolean("is_admin", false));
chat.setOwner(chatData.optBoolean("is_owner", false));
chat.setName(chatData.optString("name", ""));
chat.setDisplayId(chatData.optString("id", ""));
chat.setImageUrl(chatData.optString("image_url", ""));
chat.setType(chatData.optString("type", ""));
Session.currentChatEntry = chat;
});
// 2. get permission
JSONObject permissionReq = new JSONObject();
if (chatType.equalsIgnoreCase("group")) {
permissionReq.put("action", "get_group_permissions");
permissionReq.put("group_id", chatId);
} else {
permissionReq.put("action", "get_channel_permissions");
permissionReq.put("channel_id", chatId);
}
JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq);
JSONObject perm = permissionResp.getJSONObject("data");
entry.ifPresent(chat -> chat.setPermissions(perm));
// 3. set currentChatId
Session.currentChatId = chatUUID.toString();
System.out.println("🧪 Checking refresh conditions...");
System.out.println("🔹 inChatMenu: " + Session.inChatMenu);
System.out.println("🔹 currentChatId: " + Session.currentChatId);
System.out.println("🔹 chatUUID: " + chatUUID);
if (Session.inChatMenu && Session.currentChatId != null && Session.currentChatId.equals(chatUUID.toString())) {
synchronized (Session.class) {
Session.refreshCurrentChatMenu = true;
}
System.out.println("✅ Admin status updated. Refreshing menu...");
} else {
System.out.println("❌ Refresh conditions not met.");
}
} catch (Exception e) {
System.out.println("❌ Exception while handling admin role change: " + e.getMessage());
e.printStackTrace();
}
}
private void displayRealTimeMessage(String action, JSONObject msg) {
switch (action) {
case "new_message" -> {
String senderName = msg.optString("sender_name","Unknown");
String content = msg.optString("content","(empty)");
String sendAt = msg.optString("send_at","-");
String chatId = msg.optString("receiver_id", msg.optString("chat_id",""));
String kind = msg.optString("kind","plain");
JSONObject meta = msg.optJSONObject("meta");
String prefix = "";
if ("reply".equals(kind) && meta != null) {
var rt = meta.optJSONObject("reply_to");
prefix = "[reply → " + (rt!=null?rt.optString("excerpt",""):"") + "] ";
} else if ("forward".equals(kind) && meta != null) {
var ff = meta.optJSONObject("forwarded_from");
prefix = "[forwarded from " + (ff!=null?ff.optString("sender_name","unknown"):"unknown") + "] ";
}
boolean isInCurrentChat = Session.inChatMenu &&
Session.currentChatId != null && Session.currentChatId.equals(chatId);
if (isInCurrentChat) {
System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")");
} else {
System.out.println("💬 Message from " + senderName + ": " + prefix + content);
Session.forceRefreshChatList = true;
}
}
case "message_edited" -> {
System.out.println("\n✏️ Message Edited:");
System.out.println("ID: " + msg.getString("message_id"));
System.out.println("New Content: " + msg.getString("new_content"));
System.out.println("Edit Time: " + msg.getString("edited_at"));
}
case "message_deleted_global" -> {
System.out.println("\n🗑️ Message Deleted:");
System.out.println("Message ID: " + msg.getString("message_id"));
}
case "message_reacted", "message_unreacted" -> {
String mid = msg.getString("message_id");
String emoji = msg.getString("emoji");
JSONObject counts = msg.optJSONObject("counts");
int n = msg.optInt("count_for_emoji", 0);
System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + "" + n);
}
case "user_status_changed" -> {
System.out.println("\n🔄 User Status Changed:");
System.out.println("User: " + msg.getString("user_id"));
System.out.println("Status: " + msg.getString("status"));
}
case "blocked_by_user" -> {
System.out.println("\n⛔ You were blocked by user: " + msg.getString("blocker_id"));
}
case "unblocked_by_user" -> {
System.out.println("\n✅ You were unblocked by user: " + msg.getString("unblocker_id"));
}
case "message_seen" -> {
System.out.println("\n👁️ Your message was seen:");
System.out.println("Message ID: " + msg.getString("message_id"));
System.out.println("Seen at: " + msg.getString("seen_at"));
}
default -> {
System.out.println("\n❓ Unknown real-time action: " + action);
System.out.println(msg.toString(2));
}
}
}
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()); }
}
// --- add this helper ---
private static JSONObject normalizeMessageId(JSONObject j) {
if (j == null) return new JSONObject();
if (!j.has("message_id") && j.has("id")) {
JSONObject copy = new JSONObject(j.toString());
copy.put("message_id", copy.optString("id", ""));
return copy;
}
return j;
}
private void removeFromAllLists(UUID chatId) {
if (Session.chatList != null) {
Session.chatList.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId())));
}
if (Session.activeChats != null) {
Session.activeChats.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId())));
}
if (Session.archivedChats != null) {
Session.archivedChats.removeIf(c -> chatId.toString().equals(String.valueOf(c.getId())));
}
}
}
@@ -0,0 +1,82 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONObject;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.util.UUID;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
class MediaSender {
public static void sendImageOrAudio(Socket socket,
UUID senderId,
String receiverType,
UUID receiverId,
File file,
String messageType, // "IMAGE" یا "AUDIO"
String captionOrEmpty) throws Exception {
if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType))
throw new IllegalArgumentException("Only IMAGE/AUDIO");
// 1) اعلام سوییچ به باینری
PrintWriter textOut = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
textOut.println("MEDIA");
// 2) متادیتا
String mime = Files.probeContentType(file.toPath());
if (mime == null) mime = "application/octet-stream";
Integer width = null, height = null;
if ("IMAGE".equals(messageType)) {
try {
BufferedImage img = ImageIO.read(file);
if (img != null) { width = img.getWidth(); height = img.getHeight(); }
} catch (Exception ignore) {}
}
UUID messageId = UUID.randomUUID();
JSONObject header = new JSONObject()
.put("message_id", messageId.toString())
.put("sender_id", senderId.toString())
.put("receiver_type", receiverType)
.put("receiver_id", receiverId.toString())
.put("message_type", messageType)
.put("file_name", file.getName())
.put("mime_type", mime)
.put("file_size", file.length())
.put("text", captionOrEmpty == null ? "" : captionOrEmpty);
if (width != null) header.put("width", width);
if (height != null) header.put("height", height);
byte[] headerBytes = header.toString().getBytes("UTF-8");
// 3) ارسال فریم باینری
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
dos.writeInt(0x4D444D31); // MAGIC
dos.writeInt(headerBytes.length); // headerLen
dos.write(headerBytes); // header
dos.writeLong(file.length()); // contentLen
try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) {
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
dos.write(buf, 0, n);
}
}
dos.flush();
// (اختیاری) Ack متنی
BufferedReader textIn = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String ack = textIn.readLine();
if (!"OK".equalsIgnoreCase(ack)) {
throw new IOException("Server did not ACK: " + ack);
}
}
}
@@ -0,0 +1,12 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONObject;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class RealTimeBuffer {
public static final BlockingQueue<JSONObject> incomingEvents = new LinkedBlockingQueue<>();
}
@@ -0,0 +1,211 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.Models.ContactEntry;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
// method for save data from server response
public class Session {
public static JSONObject currentUser;
public static List<ChatEntry> chatList = new ArrayList<>();
public static List<ChatEntry> archivedChats = new ArrayList<>();
public static List<ChatEntry> activeChats = new ArrayList<>();
public static UUID currentPrivateChatUserId = null;
public static volatile boolean forceRefreshChatList = false;
public static volatile boolean backToChatList = false;
public static boolean inChatListMenu = false;
public static String currentChatType = null;
public static volatile boolean inChatMenu = false;
public static volatile boolean refreshCurrentChatMenu = false;
public static String currentChatId = null;
public static ChatEntry currentChatEntry = null;
public static List<ContactEntry> contactEntries = new ArrayList<>();
public static boolean inContactListMenu = false;
public static DownloadsIndex downloadsIndex = null;
public static String getUserUUID() {
if (currentUser.has("uuid")) return currentUser.getString("uuid");
if (currentUser.has("internal_uuid")) return currentUser.getString("internal_uuid");
if (currentUser.has("internalUUID")) return currentUser.getString("internalUUID");
throw new RuntimeException("❌ No UUID found in currentUser!");
}
// public static void updateChatList(JSONArray chatArray) {
// chatList.clear();
// for (int i = 0; i < chatArray.length(); i++) {
// JSONObject obj = chatArray.getJSONObject(i);
// ChatEntry entry = new ChatEntry(
// UUID.fromString(obj.getString("internal_id")),
// obj.optString("id", ""), // displayId
// obj.optString("name", ""), // name
// obj.optString("image_url", ""),
// obj.getString("type"),
// null, // last message time (if needed, parse it)
// obj.optBoolean("is_owner", false),
// obj.optBoolean("is_admin", false)
// );
// entry.setPermissions(obj.optJSONObject("permissions"));
// chatList.add(entry);
// }
// }
public static void updateChatList(JSONArray chatArray) {
chatList.clear();
for (int i = 0; i < chatArray.length(); i++) {
JSONObject obj = chatArray.getJSONObject(i);
LocalDateTime lastMessageTime = null;
if (obj.has("last_message_time") && !obj.isNull("last_message_time")) {
String timeStr = obj.getString("last_message_time");
if (!timeStr.isBlank()) {
lastMessageTime = LocalDateTime.parse(timeStr);
}
}
ChatEntry entry = new ChatEntry(
UUID.fromString(obj.getString("internal_id")),
obj.optString("id", ""), // displayId
obj.optString("name", ""), // name
obj.optString("image_url", ""),
obj.getString("type"),
lastMessageTime,
obj.optBoolean("is_owner", false),
obj.optBoolean("is_admin", false)
);
entry.setPermissions(obj.optJSONObject("permissions"));
chatList.add(entry);
}
chatList.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
});
activeChats = chatList.stream().filter(c -> !c.isArchived()).toList();
archivedChats = chatList.stream().filter(ChatEntry::isArchived).toList();
}
public static List<ChatEntry> getChatList() {
return chatList;
}
public static void refreshChatLists() {
Session.activeChats = Session.chatList.stream()
.filter(c -> !c.isArchived())
.toList();
Session.archivedChats = Session.chatList.stream()
.filter(ChatEntry::isArchived)
.toList();
}
public void setDownloadIndex(DownloadsIndex idx){ this.downloadsIndex = idx; }
// ==== ADD to Session class ====
/** پیدا کردن چت با internal_id */
public static ChatEntry findChatByInternalId(UUID internalId) {
for (ChatEntry c : chatList) {
if (c.getId().equals(internalId)) return c;
}
return null;
}
/** درج در ابتدای لیست (با جلوگیری از دوبل) و سپس resort + refresh */
public static void prependChat(ChatEntry entry) {
// اگر قبلاً تو لیست هست، اول پاکش کن
chatList.removeIf(c -> c.getId().equals(entry.getId()));
// اول لیست بذار
chatList.add(0, entry);
resortAndRefresh();
}
public static void resortAndRefresh() {
chatList.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
});
refreshChatLists();
}
public static ChatEntry upsertSavedMessages(UUID chatId, String name, String chatType, String imageUrlIfAny) {
ChatEntry existing = findChatByInternalId(chatId);
if (existing != null) {
// برای اینکه بیاد بالا، می‌تونی زمان آخرین پیام رو الان بذاری (اختیاری)
if (existing.getLastMessageTime() == null) {
existing.setLastMessageTime(LocalDateTime.now());
resortAndRefresh();
}
return existing;
}
ChatEntry entry = new ChatEntry(
chatId,
"", // displayId برای Saved لازم نیست
(name == null || name.isBlank()) ? "Saved Messages" : name,
imageUrlIfAny == null ? "" : imageUrlIfAny,
(chatType == null || chatType.isBlank()) ? "private" : chatType,
LocalDateTime.now(), // بذار بالا بیاد
true, // owner? برای self-chat می‌تونه true باشه (اثری روی permission نداره)
true // admin? اختیاری—تأثیر خاصی نداره؛ می‌تونی false بذاری
);
// اگر توی UI برای Saved Messages آیکن خاص داری، اینجا ست کن:
// entry.setImageUrl("/org/to/telegramfinalproject/Icons/saved_messages_dark.png");
prependChat(entry);
return entry;
}
public static boolean inArchivedView = false;
public static boolean isArchived(UUID chatId) {
if (archivedChats == null) return false;
for (ChatEntry e : archivedChats) if (e.getId().equals(chatId)) return true;
return false;
}
public static void moveToArchived(ChatEntry e) {
if (e == null) return;
if (chatList != null) chatList.removeIf(x -> x.getId().equals(e.getId()));
if (activeChats != null) activeChats.removeIf(x -> x.getId().equals(e.getId()));
if (archivedChats != null) archivedChats.removeIf(x -> x.getId().equals(e.getId()));
if (archivedChats != null) archivedChats.add(e);
}
public static void moveToActive(ChatEntry e) {
if (e == null) return;
if (archivedChats != null) archivedChats.removeIf(x -> x.getId().equals(e.getId()));
if (activeChats != null) activeChats.removeIf(x -> x.getId().equals(e.getId()));
if (chatList != null) chatList.removeIf(x -> x.getId().equals(e.getId()));
if (activeChats != null) activeChats.add(e);
}
// مرتب‌سازی بر اساس آخرین پیام (در صورت داشتن فیلد)
public static void sortListsByLastMessage() {
java.util.Comparator<ChatEntry> cmp =
java.util.Comparator.comparing(ChatEntry::getLastMessageTime, java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder()))
.reversed();
if (activeChats != null) activeChats.sort(cmp);
if (archivedChats != null) archivedChats.sort(cmp);
}
}
@@ -0,0 +1,12 @@
package org.to.telegramfinalproject.Client;
public enum SidebarAction {
MY_PROFILE,
NEW_GROUP,
NEW_CHANNEL,
CONTACTS,
SAVED_MESSAGES,
SETTINGS,
FEATURES,
Q_AND_A
}
@@ -0,0 +1,580 @@
package org.to.telegramfinalproject.Client;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.Models.FileAttachment;
import org.to.telegramfinalproject.Models.Message;
import java.time.LocalDateTime;
import java.util.*;
import static org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse;
public class SidebarHandler {
private final Scanner scanner;
private ActionHandler actionHandler ;
private final String userUUID;
public SidebarHandler(Scanner scanner, ActionHandler actionHandler, ActionHandler action) {
this.scanner = scanner;
this.actionHandler = action;
this.userUUID = Session.getUserUUID();
}
public SidebarHandler(Scanner scanner, ActionHandler actionHandler) {
this.scanner = scanner;
this.actionHandler = actionHandler;
this.userUUID = Session.getUserUUID();
}
public void handleSidebarAction(SidebarAction action) {
switch (action) {
case MY_PROFILE:
openUserProfile();
break;
case NEW_GROUP:
createNewGroup();
break;
case NEW_CHANNEL:
createNewChannel();
break;
case CONTACTS:
showContacts();
break;
case SAVED_MESSAGES:
ensureSavedMessagesCreated();
break;
case SETTINGS:
openSettings();
break;
case FEATURES:
showTelegramFeatures();
break;
case Q_AND_A:
showTelegramQA();
break;
}
}
private void openUserProfile() {
// Step 1: Request profile info from the server
JSONObject request = new JSONObject();
request.put("action", "get_user_profile");
JSONObject response = sendWithResponse(request);
if (response == null || !response.optString("status", "fail").equals("success")) {
System.out.println("Failed to load profile information.");
return;
}
JSONObject profile;
try {
profile = response.getJSONObject("data");
} catch (JSONException e) {
System.out.println("Received malformed profile data from server.");
return;
}
String profileName = profile.optString("profile_name", "NOT-AVAILABLE");
String userId = profile.optString("user_id", "NOT-AVAILABLE");
String status = profile.optString("status", "NOT-AVAILABLE");
String bio = profile.optString("bio", "NOT-AVAILABLE");
String profilePictureUrl = profile.optString("profile_picture_url", "NOT-AVAILABLE");
// Step 2: Show profile info
System.out.println("===== My Profile =====");
System.out.println("Profile Picture URL : " + profilePictureUrl);
System.out.println("Profile Name : " + profileName);
System.out.println("User ID : @" + userId);
System.out.println("Status : " + status);
System.out.println("Bio : " + bio);
System.out.println("======================");
// Step 3: Ask if user wants to edit anything
System.out.println("Do you want to edit any of these? (yes/no)");
while (true) {
String choice = scanner.nextLine().trim().toLowerCase();
if (choice.equals("no")) return;
if (!choice.equals("yes")) {
System.out.println("Invalid input. Please enter 'yes' or 'no'.");
continue;
}
// Step 4: Show editable fields
System.out.println("Which field do you want to edit?");
System.out.println("1. Profile Name");
System.out.println("2. User ID");
System.out.println("3. Bio");
System.out.println("4. Profile Picture URL");
System.out.println("0. Cancel");
String option = scanner.nextLine().trim();
switch (option) {
case "1" -> editProfileName();
case "2" -> editUserId();
case "3" -> editBio();
case "4" -> editProfilePictureUrl();
case "0" -> { return; }
default -> System.out.println("Invalid choice. Try again.");
}
// Re-fetch and re-display profile after editing
openUserProfile();
return;
}
}
private void editProfileName() {
System.out.println("Enter your new profile name:");
String newProfileName = scanner.nextLine().trim();
// Step 1: Validate input
while (newProfileName.trim().isEmpty()) {
System.out.println("Profile name cannot be empty. Enter another name.");
newProfileName = scanner.nextLine().trim();
}
// Step 2: Create request
JSONObject request = new JSONObject();
request.put("action", "edit_profile_name");
request.put("new_profile_name", newProfileName);
// Step 3: Send request and receive response
JSONObject response = sendWithResponse(request);
// Step 4: Handle response
if (response == null || !response.optString("status", "fail").equals("success")) {
System.out.println("Failed to update profile name.");
} else {
System.out.println("Profile name updated successfully!");
}
}
private void editUserId() {
while (true) {
System.out.println("Enter your new user ID: ");
String newUserId = scanner.nextLine().trim();
if (newUserId.isEmpty()) {
System.out.println("User ID cannot be empty.");
continue;
}
if (newUserId.contains(" ")) {
System.out.println("User ID cannot contain spaces.");
continue;
}
if (!newUserId.matches("^[a-zA-Z0-9_]+$")) {
System.out.println("User ID can only contain letters, digits, and underscores.");
continue;
}
// Send to server
JSONObject request = new JSONObject();
request.put("action", "edit_user_id");
request.put("new_user_id", newUserId);
JSONObject response = sendWithResponse(request);
if (response.getString("status").equals("success")) {
System.out.println("User ID updated successfully.");
break;
} else {
// Server-side error message (e.g., ID already exists)
System.out.println(response.getString("message"));
}
}
}
private void editBio() {
System.out.println("Enter your new bio:");
String newBio = scanner.nextLine().trim();
// Limit the bio length
if (newBio.length() > 70) {
System.out.println("Bio cannot be more than 70 characters.");
return;
}
JSONObject request = new JSONObject();
request.put("action", "edit_bio");
request.put("new_bio", newBio);
JSONObject response = sendWithResponse(request);
if (response.getString("status").equals("success")) {
System.out.println("Bio updated successfully.");
} else {
System.out.println(response.getString("message"));
}
}
private void editProfilePictureUrl() {
while (true) {
System.out.println("Enter new profile picture URL (or leave empty to remove):");
String newImageUrl = scanner.nextLine().trim();
// URL validation
if (newImageUrl.contains(" ")) {
System.out.println("URL cannot contain spaces.");
continue;
}
if (!newImageUrl.isEmpty() && !newImageUrl.matches("^(http|https)://.*$")) {
System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link.");
continue;
}
JSONObject request = new JSONObject();
request.put("action", "edit_profile_picture");
request.put("new_image_url", newImageUrl);
JSONObject response = sendWithResponse(request);
if (response.getString("status").equals("success")) {
System.out.println("Bio updated successfully.");
} else {
System.out.println(response.getString("message"));
}
break;
}
}
private void createNewGroup() {
actionHandler.createGroup();
}
private void createNewChannel() {
actionHandler.createChannel();
}
private void showContacts() {
actionHandler.showContactList();
}
public void ensureSavedMessagesCreated() {
// 1) Client-side quick check
boolean alreadyExists = false;
if (Session.activeChats != null) {
for (ChatEntry e : Session.activeChats) {
if (e != null && e.isSavedMessages()) { alreadyExists = true; break; }
}
}
if (alreadyExists) {
System.out.println("️ Saved Messages already exists.");
return;
}
// 2) Ask server to get or create the self-chat
JSONObject res = sendWithResponse(new JSONObject().put("action", "get_or_create_saved_messages"));
if (res == null || !"success".equals(res.optString("status"))) {
System.out.println("❌ Failed to create/open Saved Messages: " + (res != null ? res.optString("message","") : ""));
return;
}
JSONObject data = res.optJSONObject("data");
if (data == null) {
System.out.println("❌ Invalid server response for Saved Messages.");
return;
}
boolean created = data.optBoolean("created", true); // if server sends it
String chatId = data.optString("chat_id", null);
if (created) {
System.out.println("✅ Saved Messages created." + (chatId != null ? " chat_id=" + chatId : ""));
try {
UUID cid = UUID.fromString(chatId);
ChatEntry saved = new ChatEntry(cid, "Saved Messages", "Saved Messages", "", "private", null, true, false);
saved.setSavedMessages(true);
if (Session.activeChats == null) Session.activeChats = new ArrayList<>();
Session.activeChats.add(0, saved);
} catch (Exception ignore) {}
} else {
System.out.println("️ Saved Messages already exists." + (chatId != null ? " chat_id=" + chatId : ""));
}
}
private void openSettings() {
while (true) {
System.out.println("⚙️ Settings\n");
String imageUrl = Session.currentUser.optString("image_url", "");
String profileName = Session.currentUser.optString("profile_name", "");
String userId = Session.currentUser.optString("user_id", "");
renderUserCard(imageUrl, profileName, userId);
System.out.println();
renderMenu();
System.out.print("\nChoose an option (0-4): ");
String pick = scanner.nextLine().trim();
switch (pick) {
case "1": openUserProfile(); break;
case "2": showPrivacySettings(); break;
case "3": showTelegramQA(); break;
case "4": showTelegramFeatures(); break;
case "0": return;
default:
System.out.println("❌ Invalid choice. Press Enter to continue...");
scanner.nextLine();
}
}
}
private void showPrivacySettings() {
while (true) {
System.out.println("🔒 Privacy\n");
System.out.println("1) Blocked users");
System.out.println("2) Change username / password");
System.out.println("0) Back");
System.out.print("\nChoose: ");
String pick = scanner.nextLine().trim();
switch (pick) {
case "1": viewBlockedUsers(); break;
case "2": changeCredentialsFlow(); break;
case "0": return;
default:
System.out.println("❌ Invalid choice. Press Enter...");
scanner.nextLine();
}
}
}
private void viewBlockedUsers() {
System.out.println("🚫 Blocked Users\n");
org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "get_blocked_users");
org.json.JSONObject res = sendWithResponse(req);
if (res == null || !res.optString("status","error").equals("success")) {
System.out.println("❌ Failed to fetch blocked users. Press Enter...");
scanner.nextLine();
return;
}
org.json.JSONArray arr = res.getJSONObject("data").optJSONArray("blocked_users");
if (arr == null || arr.isEmpty()) {
System.out.println("📭 No blocked users.");
System.out.println("\nPress Enter...");
scanner.nextLine();
return;
}
for (int i = 0; i < arr.length(); i++) {
org.json.JSONObject u = arr.getJSONObject(i);
String profileName = u.optString("profile_name", "");
String userId = u.optString("user_id", "");
System.out.printf("%d) %s (@%s)\n", i + 1, profileName, userId);
}
System.out.println("\n0) Back");
System.out.print("\nChoose a user to UNBLOCK (number): ");
String pick = scanner.nextLine().trim();
if (pick.equals("0")) return;
int idx;
try { idx = Integer.parseInt(pick) - 1; } catch (Exception e) { idx = -1; }
if (idx < 0 || idx >= arr.length()) {
System.out.println("❌ Invalid index. Press Enter...");
scanner.nextLine();
return;
}
org.json.JSONObject target = arr.getJSONObject(idx);
String targetDisplayId = target.optString("user_id", "");
java.util.UUID targetInternalId = java.util.UUID.fromString(target.getString("internal_uuid"));
System.out.printf("Unblock %s (@%s)? (yes/no): ", target.optString("profile_name",""), targetDisplayId);
if (!scanner.nextLine().trim().equalsIgnoreCase("yes")) return;
org.json.JSONObject unReq = new org.json.JSONObject();
unReq.put("action", "toggle_block");
unReq.put("user_id",Session.getUserUUID());
unReq.put("target_id", targetInternalId.toString());
org.json.JSONObject unRes = sendWithResponse(unReq);
if (unRes != null && unRes.optString("status","error").equals("success")) {
System.out.println("✅ User unblocked.");
} else {
System.out.println("❌ Failed to unblock.");
}
System.out.println("Press Enter...");
scanner.nextLine();
}
private void changeCredentialsFlow() {
System.out.println("🛡️ Change Username / Password\n");
System.out.print("Enter current password: ");
String currentPassword = scanner.nextLine();
org.json.JSONObject verReq = new org.json.JSONObject();
verReq.put("action", "verify_password");
verReq.put("current_password", currentPassword);
org.json.JSONObject verRes = sendWithResponse(verReq);
if (verRes == null || !verRes.optString("status","error").equals("success")) {
System.out.println("❌ Current password is incorrect.");
System.out.println("Press Enter...");
scanner.nextLine();
return;
}
String currentUsername = Session.currentUser.optString("username", "");
System.out.println("\n✅ Verified.");
System.out.println("Current username: " + currentUsername);
System.out.println("Current password: ******** (hidden)");
System.out.println("\nWhat do you want to change?");
System.out.println("1) Username");
System.out.println("2) Password");
System.out.println("3) Both");
System.out.println("0) Back");
System.out.print("\nChoose: ");
String pick = scanner.nextLine().trim();
switch (pick) {
case "1":
changeUsername(currentPassword);
break;
case "2":
changePassword(currentPassword);
break;
case "3":
boolean uOk = changeUsername(currentPassword);
boolean pOk = changePassword(currentPassword);
if (uOk && pOk) System.out.println("✅ Username and password updated.");
System.out.println("Press Enter...");
scanner.nextLine();
break;
case "0":
return;
default:
System.out.println("❌ Invalid choice. Press Enter...");
scanner.nextLine();
}
}
private boolean changeUsername(String currentPassword) {
System.out.print("\nNew username: ");
String newUsername = scanner.nextLine().trim();
if (!isValidUsername(newUsername)) {
System.out.println("❌ Invalid username. Use 432 chars: letters, digits, underscore.");
return false;
}
org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "update_username");
req.put("current_password", currentPassword);
req.put("new_username", newUsername);
org.json.JSONObject res = sendWithResponse(req);
if (res != null && res.optString("status","error").equals("success")) {
Session.currentUser.put("username", newUsername);
System.out.println("✅ Username updated.");
return true;
} else {
String msg = (res == null) ? "No response." : res.optString("message","Update failed.");
System.out.println("" + msg);
return false;
}
}
private boolean isValidUsername(String s) {
//4-32 char
return s != null && s.matches("^[A-Za-z0-9_]{4,32}$");
}
private boolean changePassword(String currentPassword) {
System.out.print("\nNew password: ");
String newPassword = scanner.nextLine();
System.out.print("Repeat new password: ");
String repeat = scanner.nextLine();
if (!newPassword.equals(repeat)) {
System.out.println("❌ Passwords do not match.");
return false;
}
if (!isStrongPassword(newPassword)) {
System.out.println("❌ Weak password. Min 8 chars, include letters and digits.");
return false;
}
org.json.JSONObject req = new org.json.JSONObject();
req.put("action", "update_password");
req.put("current_password", currentPassword);
req.put("new_password", newPassword);
org.json.JSONObject res = sendWithResponse(req);
if (res != null && res.optString("status","error").equals("success")) {
System.out.println("✅ Password updated.");
return true;
} else {
String msg = (res == null) ? "No response." : res.optString("message","Update failed.");
System.out.println("" + msg);
return false;
}
}
private void renderUserCard(String imageUrl, String profileName, String userId) {
int w = 60;
String top = "" + "".repeat(w - 2) + "";
String bot = "" + "".repeat(w - 2) + "";
System.out.println(top);
System.out.println(padBoxLine("Profile", w));
System.out.println("" + "".repeat(w - 2) + "");
System.out.println(padBoxLine("Image URL: " + imageUrl, w));
System.out.println(padBoxLine("Profile Name: "+ profileName, w));
System.out.println(padBoxLine("User ID: " + userId, w));
System.out.println(bot);
}
private String padBoxLine(String text, int width) {
final int inner = width - 2;
if (text.length() > inner) {
text = text.substring(0, inner - 1) + "";
}
int spaces = inner - text.length();
return "" + text + " ".repeat(Math.max(0, spaces)) + "";
}
private void renderMenu() {
System.out.println("1) My Account");
System.out.println("2) Privacy");
System.out.println("3) Telegram Q&A");
System.out.println("4) Telegram Features");
System.out.println("0) Back");
}
private void showTelegramFeatures() {
System.out.println("🌟 Showing Telegram features...");
}
private void showTelegramQA() {
System.out.println("❓ Showing Q&A...");
}
private boolean isStrongPassword(String s) {
boolean approved = s.matches("\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b");
return approved ;
}
}
@@ -0,0 +1,75 @@
package org.to.telegramfinalproject.Client;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.PrintWriter;
public final class SocketMediaDownloader {
private static final int MAGIC_DL = 0x4D444D32;
private final PrintWriter outText; // NEW
private final DataInputStream inBin;
private final DataOutputStream outBin;
public SocketMediaDownloader(PrintWriter outText, DataInputStream inBin, DataOutputStream outBin) {
this.outText = outText;
this.inBin = inBin;
this.outBin = outBin;
}
public java.nio.file.Path download(java.util.UUID mediaKey, java.nio.file.Path saveDir, String fileNameHint) throws Exception {
outText.print("MEDIA_DL\n");
outText.flush();
org.json.JSONObject req = new org.json.JSONObject()
.put("op","download")
.put("media_key", mediaKey.toString())
.put("offset", 0);
byte[] hb = req.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
outBin.writeInt(MAGIC_DL);
outBin.writeInt(hb.length);
outBin.write(hb);
outBin.flush();
int magic = inBin.readInt();
if (magic != MAGIC_DL) throw new java.io.IOException("bad magic");
int hlen = inBin.readInt();
byte[] hbytes = inBin.readNBytes(hlen);
org.json.JSONObject hdr = new org.json.JSONObject(new String(hbytes, java.nio.charset.StandardCharsets.UTF_8));
if (!"success".equalsIgnoreCase(hdr.optString("status"))) {
throw new java.io.IOException("download error: " + hdr.optString("message"));
}
long contentLen = inBin.readLong();
String serverName = hdr.optString("file_name", fileNameHint != null ? fileNameHint : mediaKey.toString());
java.nio.file.Files.createDirectories(saveDir);
java.nio.file.Path dest = uniquePath(saveDir, serverName);
try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(dest)) {
byte[] buf = new byte[8192];
long remain = contentLen;
while (remain > 0) {
int toRead = (int) Math.min(buf.length, remain);
int n = inBin.read(buf, 0, toRead);
if (n == -1) throw new java.io.EOFException("unexpected EOF");
os.write(buf, 0, n);
remain -= n;
}
}
return dest;
}
private static java.nio.file.Path uniquePath(java.nio.file.Path dir, String name) throws java.io.IOException {
java.nio.file.Path p = dir.resolve(name);
if (!java.nio.file.Files.exists(p)) return p;
String base = name, ext = "";
int dot = name.lastIndexOf('.');
if (dot >= 0) { base = name.substring(0, dot); ext = name.substring(dot); }
int i = 1;
while (java.nio.file.Files.exists(dir.resolve(base + " (" + i + ")" + ext))) i++;
return dir.resolve(base + " (" + i + ")" + ext);
}
}
@@ -0,0 +1,339 @@
//package org.to.telegramfinalproject.Client;
//
//import org.json.JSONObject;
//
//import java.io.BufferedReader;
//import java.io.IOException;
//import java.io.InputStreamReader;
//import java.io.PrintWriter;
//import java.net.Socket;
//import java.util.Map;
//import java.util.Scanner;
//import java.util.UUID;
//import java.util.concurrent.BlockingQueue;
//import java.util.concurrent.ConcurrentHashMap;
//import java.util.concurrent.LinkedBlockingQueue;
//
//public class TelegramClient {
// private static final String SERVER_HOST = "localhost";
// private static final int SERVER_PORT = 8000;
// private static Socket socket;
// private BufferedReader in;
// private PrintWriter out;
// private final Scanner scanner;
// private ActionHandler handler;
// public static BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
// public static UUID loggedInUserId = null;
// public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
//
//
// private static TelegramClient instance;
//
// public TelegramClient() {
// this.scanner = new Scanner(System.in);
// instance = this;
// }
//
// public static TelegramClient getInstance() {
// return instance;
// }
//
// public void start() {
// try {
// socket = new Socket(SERVER_HOST, SERVER_PORT);
// in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// out = new PrintWriter(socket.getOutputStream(), true);
// System.out.println("✅ Connected to Telegram Server");
// handler = new ActionHandler(out, in, scanner);
//
// Thread listenerThread = new Thread(new IncomingMessageListener(in));
// listenerThread.setDaemon(true);
// listenerThread.start();
//
// showMainMenu();
//
// } catch (IOException e) {
// System.err.println("❌ Error connecting to server: " + e.getMessage());
// }
// }
//
// private void showMainMenu() throws IOException {
// while (true) {
// System.out.println("Main Menu:");
// System.out.println("1. Register");
// System.out.println("2. Login");
// System.out.println("3. Exit");
// System.out.print("Choose an option: ");
// String choice = scanner.nextLine();
//
// switch (choice) {
// case "1" -> handler.register();
// case "2" -> {
// handler.loginHandler();
// if (Session.currentUser != null) {
// System.out.println("✅ Login successful.");
// new Thread(new ActionHandler.ChatStateMonitor(out)).start();
//// new Thread(new ActionHandler.CurrentChatMenuRefresher(this.handler)).start();
//
//
// UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
// loggedInUserId = internalId;
//
// handler.userMenu(internalId);
// } else {
// System.out.println("❌ Login failed.");
// }
// }
// case "3" -> {
// System.out.println("Exiting...");
// return;
// }
// default -> System.out.println("Invalid choice.");
// }
// }
// }
//
// public static void send(JSONObject req) {
// try {
// responseQueue.clear(); // optional: clear old responses
// getInstance().out.println(req.toString());
// System.out.println("📤 [SEND] " + req.toString(2));
//
// } catch (Exception e) {
// System.err.println("❌ Error sending request: " + e.getMessage());
// }
// }
//
//
//
//
// public static Socket getSocket() {
// return socket;
// }
//
// public static void main(String[] args) {
// new TelegramClient().start();
// }
//
// public PrintWriter getOut() {
// return out;
// }
//
//
//}
package org.to.telegramfinalproject.Client;
import org.json.JSONObject;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
public class TelegramClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8080;
private static TelegramClient instance;
private static Socket socket;
private BufferedReader in;
private PrintWriter out;
private final Scanner scanner;
private ActionHandler handler;
public static final BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
public static UUID loggedInUserId = null;
private DataInputStream inBin; // NEW
private DataOutputStream outBin; // NEW
private static SocketMediaDownloader downloader; // NEW
public static final java.util.concurrent.atomic.AtomicBoolean mediaBusy = new java.util.concurrent.atomic.AtomicBoolean(false); // NEW
public static SocketMediaDownloader getDownloader() { return downloader; }
public DataInputStream getInBin() { return inBin; }
public DataOutputStream getOutBin() { return outBin; }
private volatile boolean listenerStarted = false;
public TelegramClient() {
this.scanner = new Scanner(System.in);
instance = this;
}
public static synchronized TelegramClient getInstance() {
if (instance == null) instance = new 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(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول
showMainMenu();
} catch (IOException e) {
System.err.println("❌ Error connecting to server: " + e.getMessage());
}
}
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(IncomingMessageListener.UIMode.UI); // ← UI
return cli;
}
private synchronized void connectIfNeeded() throws IOException {
if (socket != null && socket.isConnected() && !socket.isClosed()) return;
socket = new Socket(SERVER_HOST, SERVER_PORT);
InputStream rawIn = socket.getInputStream();
OutputStream rawOut = socket.getOutputStream();
in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8));
out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true);
// NEW: binary streams
inBin = new DataInputStream(rawIn);
outBin = new DataOutputStream(rawOut);
// NEW: media downloader helper
downloader = new SocketMediaDownloader(out, inBin, outBin);
System.out.println("✅ Connected to Telegram Server");
}
private synchronized void initHandlerIfNeeded() {
if (handler == null) {
handler = new ActionHandler(out, in, outBin, scanner);
}
}
// 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, mode),
// "socket-listener"
// );
// listenerThread.setDaemon(true);
// listenerThread.start();
// }
private void startListenerOnce(IncomingMessageListener.UIMode mode) {
if (listenerStarted) return;
listenerStarted = true;
listener = new IncomingMessageListener(in, mode);
Thread listenerThread = new Thread(listener, "socket-listener");
listenerThread.setDaemon(true);
listenerThread.start();
}
//console
private void showMainMenu() throws IOException {
while (true) {
System.out.println("Main Menu:");
System.out.println("1. Register");
System.out.println("2. Login");
System.out.println("3. Exit");
System.out.print("Choose an option: ");
String choice = scanner.nextLine();
switch (choice) {
case "1" -> handler.register();
case "2" -> {
handler.loginHandler();
if (Session.currentUser != null) {
System.out.println("✅ Login successful.");
new Thread(new ActionHandler.ChatStateMonitor(out)).start();
UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
loggedInUserId = internalId;
handler.userMenu(internalId);
} else {
System.out.println("❌ Login failed.");
}
}
case "3" -> {
System.out.println("Exiting...");
return;
}
default -> System.out.println("Invalid choice.");
}
}
}
public static void send(JSONObject req) {
try {
responseQueue.clear();
getInstance().out.println(req.toString());
System.out.println("📤 [SEND] " + req.toString(2));
} catch (Exception e) {
System.err.println("❌ Error sending request: " + e.getMessage());
}
}
public BufferedReader getIn() { return in; }
public PrintWriter getOut() { return out; }
public ActionHandler getHandler() { return handler; }
public static Socket getSocket() { return socket; }
public static void main(String[] args) {
new TelegramClient().startConsole();
}
// TelegramClient.java
private IncomingMessageListener listener;
public IncomingMessageListener getListener() {
return listener;
}
}
@@ -0,0 +1,72 @@
package org.to.telegramfinalproject.Database;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ArchivedChatDatabase {
public static boolean archiveChat(UUID userId, UUID chatId, String chatType) {
String sql = "INSERT INTO archived_chats (user_id, chat_id, chat_type) VALUES (?, ?, ?) ON CONFLICT DO NOTHING";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, chatId);
ps.setString(3, chatType);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean unarchiveChat(UUID userId, UUID chatId) {
String sql = "DELETE FROM archived_chats WHERE user_id = ? AND chat_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, chatId);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean isChatArchived(UUID userId, UUID chatId) {
String sql = "SELECT 1 FROM archived_chats WHERE user_id = ? AND chat_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, chatId);
ResultSet rs = ps.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<UUID> getArchivedChats(UUID userId) {
List<UUID> list = new ArrayList<>();
String sql = "SELECT chat_id FROM archived_chats WHERE user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
list.add(UUID.fromString(rs.getString("chat_id")));
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
@@ -0,0 +1,773 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.Channel;
import org.to.telegramfinalproject.Models.Group;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ChannelDatabase {
public static List<Channel> getChannelsByUser(UUID internalUuid) {
List<Channel> channels = new ArrayList<>();
String sql = """
SELECT c.* FROM channels c
JOIN channel_subscribers cs ON c.internal_uuid = cs.channel_id
WHERE cs.user_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUuid);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Channel channel = new Channel();
channel.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
channel.setChannel_id(rs.getString("channel_id"));
channel.setChannel_name(rs.getString("channel_name"));
channel.setCreator_id(UUID.fromString(rs.getString("creator_id")));
channel.setImage_url(rs.getString("image_url"));
channel.setDescription(rs.getString("description"));
channel.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
channels.add(channel);
}
} catch (SQLException e) {
e.printStackTrace();
}
return channels;
}
public static List<Channel> searchChannels(String keyword) {
List<Channel> result = new ArrayList<>();
String sql = "SELECT * FROM channels WHERE channel_name ILIKE ? OR channel_id ILIKE ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, "%" + keyword + "%");
stmt.setString(2, keyword);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Channel channel = new Channel(
UUID.fromString(rs.getString("internal_uuid")),
rs.getString("channel_name"),
UUID.fromString(rs.getString("creator_id")),
rs.getTimestamp("created_at").toLocalDateTime()
);
channel.setChannel_id(rs.getString("channel_id"));
channel.setImage_url(rs.getString("image_url"));
channel.setDescription(rs.getString("description"));
result.add(channel);
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
public static List<UUID> getSubscriberUUIDs(UUID channelInternalUUID) {
List<UUID> subscriberIds = new ArrayList<>();
String sql = "SELECT user_id FROM channel_subscribers WHERE channel_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelInternalUUID);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
subscriberIds.add((UUID) rs.getObject("user_id"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return subscriberIds;
}
public static Channel findByChannelId(String channelId) {
String sql = "SELECT * FROM channels WHERE channel_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, channelId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Channel channel = new Channel();
channel.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
channel.setChannel_id(rs.getString("channel_id"));
channel.setChannel_name(rs.getString("channel_name"));
channel.setImage_url(rs.getString("image_url"));
channel.setCreator_id(UUID.fromString(rs.getString("creator_id")));
channel.setDescription(rs.getString("description"));
channel.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
return channel;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean isUserSubscribed(UUID userId, UUID channelInternalId) {
String sql = "SELECT * FROM channel_subscribers WHERE user_id = ? AND channel_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, channelInternalId);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static UUID findInternalUUIDByChannelId(String channelId) {
String sql = "SELECT internal_uuid FROM channels WHERE channel_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, channelId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("internal_uuid");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean addSubscriberToChannel(UUID userId, UUID channelUUID) {
String sql = """
INSERT INTO channel_subscribers (channel_id, user_id)
VALUES (?, ?)
ON CONFLICT DO NOTHING
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelUUID);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean createChannel(Channel channel, UUID creatorId) {
String sql = """
INSERT INTO channels (
internal_uuid, channel_id, channel_name,
creator_id, image_url, description, created_at
)
VALUES (gen_random_uuid(), ?, ?, ?, ?, ?, ?)
RETURNING internal_uuid
""";
String subscriberSql = """
INSERT INTO channel_subscribers (channel_id, user_id) VALUES (?, ?)
""";
try (Connection conn = ConnectionDb.connect()) {
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, channel.getChannel_id());
stmt.setString(2, channel.getChannel_name());
stmt.setObject(3, creatorId);
stmt.setString(4, channel.getImage_url());
stmt.setString(5, channel.getDescription());
stmt.setObject(6, channel.getCreated_at());
ResultSet rs = stmt.executeQuery();
if (!rs.next()) return false;
UUID internalUUID = (UUID) rs.getObject("internal_uuid");
channel.setInternal_uuid(internalUUID);
PreparedStatement subStmt = conn.prepareStatement(subscriberSql);
subStmt.setObject(1, internalUUID);
subStmt.setObject(2, creatorId);
subStmt.executeUpdate();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// public static boolean insertChannel(UUID internalUUID, String channelId, String channelName, UUID creatorId, String imageUrl, LocalDateTime createdAt) {
// String sql = "INSERT INTO channels (internal_uuid, channel_id, channel_name, creator_id, image_url, created_at) VALUES (?, ?, ?, ?, ?, ?)";
//
// try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
// stmt.setObject(1, internalUUID);
// stmt.setString(2, channelId);
// stmt.setString(3, channelName);
// stmt.setObject(4, creatorId);
// stmt.setString(5, imageUrl);
// stmt.setObject(6, createdAt);
// stmt.executeUpdate();
// return true;
// } catch (SQLException e) {
// e.printStackTrace();
// return false;
// }
// }
public static boolean insertChannel(UUID internalUUID,
String channelId,
String channelName,
UUID creatorId,
String imageUrl,
String description,
LocalDateTime createdAt) {
String sql = "INSERT INTO channels " +
"(internal_uuid, channel_id, channel_name, creator_id, image_url, description, created_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUUID);
stmt.setString(2, channelId);
stmt.setString(3, channelName);
stmt.setObject(4, creatorId);
if (imageUrl != null && !imageUrl.isBlank()) {
stmt.setString(5, imageUrl);
} else {
stmt.setNull(5, java.sql.Types.VARCHAR);
}
if (description != null && !description.isBlank()) {
// اگر می‌خوای مطمئن بشی 255 نشکنه:
stmt.setString(6, description.length() > 255 ? description.substring(0, 255) : description);
} else {
stmt.setNull(6, java.sql.Types.VARCHAR);
}
stmt.setObject(7, createdAt);
stmt.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static void addSubscriber(UUID channelId, UUID userId, String role) {
String sql = "INSERT INTO channel_subscribers (channel_id, user_id, role) VALUES (?, ?, ?) ON CONFLICT DO NOTHING";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
stmt.setString(3, role);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static Channel findByInternalUUID(UUID internalUUID) {
String sql = "SELECT * FROM channels WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUUID);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Channel channel = new Channel();
channel.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
channel.setChannel_id(rs.getString("channel_id"));
channel.setChannel_name(rs.getString("channel_name"));
channel.setImage_url(rs.getString("image_url"));
channel.setCreator_id(UUID.fromString(rs.getString("creator_id")));
channel.setDescription(rs.getString("description"));
channel.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
return channel;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean addOwnerToChannel(UUID channelId, UUID userId) {
String sql = "INSERT INTO channel_subscribers (channel_id, user_id, role) VALUES (?, ?, 'owner')";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean addAdminToChannel(UUID channelId, UUID userId, JSONObject permissions) {
String sql = "UPDATE channel_subscribers SET role = 'admin', permissions = ?::jsonb WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, channelId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static String getChannelRole(UUID channelId, UUID userId) {
String sql = "SELECT role FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("role");
}
} catch (SQLException e) {
e.printStackTrace();
}
return "subscriber"; // پیش‌فرض
}
public static JSONObject getChannelPermissions(UUID channelId, UUID userId) {
String sql = "SELECT permissions FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new JSONObject(rs.getString("permissions"));
}
} catch (Exception e) {
e.printStackTrace();
}
return new JSONObject();
}
public static boolean updateChannelAdminPermissions(UUID channelId, UUID userId, JSONObject permissions) {
String sql = "UPDATE channel_subscribers SET permissions = ?::jsonb WHERE channel_id = ? AND user_id = ? AND role = 'admin'";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, channelId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<JSONObject> getChannelAdminsAndOwner(UUID channelId) {
List<JSONObject> admins = new ArrayList<>();
String sql = """
SELECT u.internal_uuid, u.profile_name, u.user_id, cs.role, cs.permissions
FROM channel_subscribers cs
JOIN users u ON cs.user_id = u.internal_uuid
WHERE cs.channel_id = ? AND (cs.role = 'owner' OR cs.role = 'admin')
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
JSONObject obj = new JSONObject();
obj.put("internal_uuid", rs.getObject("internal_uuid").toString());
obj.put("profile_name", rs.getString("profile_name"));
obj.put("user_id", rs.getString("user_id"));
obj.put("role", rs.getString("role"));
obj.put("permissions", new JSONObject(rs.getString("permissions")));
admins.add(obj);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return admins;
}
public static boolean isOwner(UUID channelId, UUID userId) {
String sql = "SELECT role FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return "owner".equals(rs.getString("role"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean isAdmin(UUID channelId, UUID userId) {
String sql = "SELECT role FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String role = rs.getString("role");
return "admin".equals(role) || "owner".equals(role); // owner هم admin هست
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean isUserInChannel(UUID userId, UUID channelId) {
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(
"SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ?")) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean removeSubscriberFromChannel(UUID channelId, UUID userId) {
String sql = "DELETE FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn =ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
int affectedRows = stmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
System.err.println("Error removing subscriber from channel: " + e.getMessage());
return false;
}
}
public static JSONArray getChannelSubscribers(UUID channelId) {
JSONArray subscribers = new JSONArray();
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(
"SELECT u.internal_uuid, u.user_id, u.profile_name, " +
"CASE WHEN cs.role = 'owner' THEN 'owner' " +
" WHEN cs.role = 'admin' THEN 'admin' " +
" ELSE 'subscriber' END AS role " +
"FROM channel_subscribers cs " +
"JOIN users u ON cs.user_id = u.internal_uuid " +
"WHERE cs.channel_id = ?")) {
stmt.setObject(1, channelId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
JSONObject obj = new JSONObject();
obj.put("internal_uuid", rs.getObject("internal_uuid").toString());
obj.put("user_id", rs.getString("user_id"));
obj.put("profile_name", rs.getString("profile_name"));
obj.put("role", rs.getString("role"));
subscribers.put(obj);
}
} catch (SQLException e) {
e.printStackTrace();
}
return subscribers;
}
public static boolean updateChannelInfo(UUID channelId, String newId, String name, String description, String imageUrl) {
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(
"UPDATE channels SET channel_id = ?, channel_name = ?, description = ?, image_url = ? WHERE internal_uuid = ?")) {
stmt.setString(1, newId);
stmt.setString(2, name);
stmt.setString(3, description);
stmt.setString(4, imageUrl);
stmt.setObject(5, channelId);
int rows = stmt.executeUpdate();
return rows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean isChannelIdUnique(String channelId, UUID excludeChannelUUID) {
String query = "SELECT COUNT(*) FROM channels WHERE channel_id = ? AND internal_uuid != ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, channelId);
stmt.setObject(2, excludeChannelUUID);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
int count = rs.getInt(1);
return count == 0;
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean demoteAdminToSubscriber(UUID channelId, UUID userId) {
String sql = "UPDATE channel_subscribers SET role = 'member', permissions = '{}'::jsonb WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
int affected = stmt.executeUpdate();
return affected > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean deleteChannel(UUID channelId) {
String sql = "DELETE FROM channels WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
int affected = stmt.executeUpdate();
return affected > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean transferOwnership(UUID channelId, UUID newOwnerUUID) {
String updateRoles = """
UPDATE channel_subscribers
SET role = CASE
WHEN user_id = ? THEN 'owner'
WHEN role = 'owner' THEN 'admin'
ELSE role
END
WHERE channel_id = ?
""";
String clearPermissions = """
UPDATE channel_subscribers
SET permissions = '{}'::jsonb
WHERE channel_id = ? AND user_id = ?
""";
try (Connection conn = ConnectionDb.connect()) {
conn.setAutoCommit(false);
try (PreparedStatement roleStmt = conn.prepareStatement(updateRoles);
PreparedStatement clearPermsStmt = conn.prepareStatement(clearPermissions)) {
roleStmt.setObject(1, newOwnerUUID);
roleStmt.setObject(2, channelId);
roleStmt.executeUpdate();
clearPermsStmt.setObject(1, channelId);
clearPermsStmt.setObject(2, newOwnerUUID);
clearPermsStmt.executeUpdate();
conn.commit();
return true;
} catch (SQLException e) {
conn.rollback();
e.printStackTrace();
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static List<UUID> getChannelSubscriberUUIDs(UUID channelId) {
List<UUID> subscriberIds = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement("SELECT user_id FROM channel_subscribers WHERE channel_id = ?")) {
stmt.setObject(1, channelId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
subscriberIds.add(UUID.fromString(rs.getString("user_id")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return subscriberIds;
}
public static boolean updateAdminPermissions(UUID channelId, UUID userId, JSONObject permissions) {
String sql = "UPDATE channel_subscribers SET permissions = ?::jsonb WHERE channel_id = ? AND user_id = ? AND role = 'admin'";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, channelId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static JSONObject getChannelInfo(UUID channelId, UUID viewerUuid) throws SQLException {
JSONObject result = new JSONObject();
try (Connection conn = ConnectionDb.connect()) {
// === Channel header (name, subscriber count, etc.)
String channelQuery = """
SELECT c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description,
COUNT(s.user_id) as subscriber_count
FROM channels c
LEFT JOIN channel_subscribers s ON c.internal_uuid = s.channel_id
WHERE c.internal_uuid = ?
GROUP BY c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description
""";
try (PreparedStatement ps = conn.prepareStatement(channelQuery)) {
ps.setObject(1, channelId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("internal_uuid", rs.getString("internal_uuid"));
result.put("channel_id", rs.getString("channel_id")); // varchar if needed
result.put("channel_name", rs.getString("channel_name"));
result.put("subscriber_count", rs.getInt("subscriber_count"));
result.put("image_url", rs.getString("image_url"));
result.put("description", rs.getString("description")); // ✅ here
} else {
return null; // no such channel
}
}
// === Subscribers (id, name, role, status, image)
JSONArray subscribersArr = new JSONArray();
String subscribersQuery = """
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
cs.role, u.status, u.last_seen
FROM channel_subscribers cs
JOIN users u ON cs.user_id = u.internal_uuid
WHERE cs.channel_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(subscribersQuery)) {
ps.setObject(1, channelId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
JSONObject sub = new JSONObject();
sub.put("user_id", rs.getString("internal_uuid"));
sub.put("profile_name", rs.getString("profile_name"));
sub.put("username", rs.getString("user_id"));
sub.put("image_url", rs.getString("image_url"));
sub.put("role", rs.getString("role"));
sub.put("status", rs.getString("status"));
sub.put("last_seen", rs.getString("last_seen"));
subscribersArr.put(sub);
}
}
result.put("subscribers", subscribersArr);
// === Viewer role (so UI knows if viewer can manage/delete)
String roleQuery = """
SELECT role
FROM channel_subscribers
WHERE channel_id = ? AND user_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
ps.setObject(1, channelId);
ps.setObject(2, viewerUuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("my_role", rs.getString("role"));
} else {
result.put("my_role", ""); // not a subscriber
}
}
}
return result;
}
}
@@ -0,0 +1,130 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONObject;
import java.sql.*;
import java.time.OffsetDateTime;
import java.util.UUID;
public class ChatInfoDatabase {
/**
* هدر چت را بر اساس نوع آن برمی‌گرداند.
* private: name,image_url, online,last_seen
* group : name,image_url, member_count
* channel: name,image_url, member_count
*/
public static JSONObject getHeaderInfo(String type, UUID receiverId, UUID viewerId) throws SQLException {
try (Connection conn = ConnectionDb.connect()) {
switch (type.toLowerCase()) {
case "private":
return getPrivateHeader(conn, receiverId, viewerId);
case "group":
return getGroupHeader(conn, receiverId);
case "channel":
return getChannelHeader(conn, receiverId);
default:
throw new IllegalArgumentException("Unsupported receiver_type: " + type);
}
}
}
/** هدر چت خصوصی: other-user نسبت به viewerId را پیدا می‌کنیم */
private static JSONObject getPrivateHeader(Connection conn, UUID chatId, UUID viewerId) throws SQLException {
if (viewerId == null) throw new IllegalArgumentException("viewerId is required for private header.");
final String sql = """
SELECT
u.profile_name AS name,
COALESCE(u.image_url, '') AS image_url,
(u.status = 'online') AS online,
u.status AS status,
u.last_seen AS last_seen
FROM private_chat pc
JOIN users u
ON u.internal_uuid = CASE WHEN pc.user1_id = ? THEN pc.user2_id ELSE pc.user1_id END
WHERE pc.chat_id = ?
AND (pc.user1_id = ? OR pc.user2_id = ?)
LIMIT 1
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
int i = 1;
ps.setObject(i++, viewerId);
ps.setObject(i++, chatId);
ps.setObject(i++, viewerId);
ps.setObject(i++, viewerId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) throw new SQLException("Private chat not found or viewer is not a participant.");
JSONObject j = new JSONObject();
j.put("type", "private");
j.put("name", rs.getString("name"));
j.put("image_url", rs.getString("image_url"));
// خواندن بولینِ online با سازگاری DB
Object onlineObj = rs.getObject("online");
boolean online = (onlineObj instanceof Boolean) ? (Boolean) onlineObj
: rs.getInt("online") == 1;
j.put("online", online);
j.put("status", rs.getString("status")); // رشته‌ی وضعیت هم می‌آید
Timestamp ts = rs.getTimestamp("last_seen");
j.put("last_seen", ts == null ? JSONObject.NULL : ts.toInstant().toString());
return j;
}
}
}
/** هدر گروه: نام، عکس و تعداد اعضا */
private static JSONObject getGroupHeader(Connection conn, UUID groupInternalId) throws SQLException {
String sql = """
SELECT
g.group_name AS name,
COALESCE(g.image_url, '') AS image_url,
(SELECT COUNT(*) FROM group_members gm WHERE gm.group_id = g.internal_uuid) AS member_count
FROM groups g
WHERE g.internal_uuid = ?
LIMIT 1
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, groupInternalId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) throw new SQLException("Group not found.");
JSONObject j = new JSONObject();
j.put("type", "group");
j.put("name", rs.getString("name"));
j.put("image_url", rs.getString("image_url"));
j.put("member_count", rs.getInt("member_count"));
return j;
}
}
}
/** هدر کانال: نام، عکس و تعداد سابسکرایبر */
private static JSONObject getChannelHeader(Connection conn, UUID channelInternalId) throws SQLException {
String sql = """
SELECT
c.channel_name AS name,
COALESCE(c.image_url, '') AS image_url,
(SELECT COUNT(*) FROM channel_subscribers cs WHERE cs.channel_id = c.internal_uuid) AS member_count
FROM channels c
WHERE c.internal_uuid = ?
LIMIT 1
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, channelInternalId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) throw new SQLException("Channel not found.");
JSONObject j = new JSONObject();
j.put("type", "channel");
j.put("name", rs.getString("name"));
j.put("image_url", rs.getString("image_url"));
j.put("member_count", rs.getInt("member_count"));
return j;
}
}
}
}
@@ -0,0 +1,18 @@
package org.to.telegramfinalproject.Database;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionDb {
private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/Telegram";
private static final String USERNAME = "postgres";
private static final String PASSWORD = "Partow@1384";
public ConnectionDb() {
}
public static Connection connect() throws SQLException {
return DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
}
}
@@ -0,0 +1,385 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.Contact;
import org.to.telegramfinalproject.Models.ContactEntry;
import org.to.telegramfinalproject.Models.User;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ContactDatabase {
public ContactDatabase(){}
private static Connection getConnection() throws SQLException {
return ConnectionDb.connect();
}
public static boolean addContact(UUID userId, UUID contactId) {
String sql = """
INSERT INTO contacts (user_id, contact_id)
VALUES (?, ?)
ON CONFLICT DO NOTHING
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, contactId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<UUID> getContactUUIDs(UUID userId) {
List<Contact> contacts = getContacts(userId);
List<UUID> contactIds = new ArrayList<>();
for (Contact c : contacts) {
contactIds.add(c.getContact_id());
}
return contactIds;
}
public static boolean removeContact(UUID user_id, UUID contact_id) {
String sql = "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, user_id);
stmt.setObject(2, contact_id);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean toggleBlock(UUID userId, UUID targetId) {
String selectSql = "SELECT is_blocked FROM contacts WHERE user_id = ? AND contact_id = ?";
String updateSql = "UPDATE contacts SET is_blocked = ? WHERE user_id = ? AND contact_id = ?";
try (Connection conn = getConnection();
PreparedStatement selectStmt = conn.prepareStatement(selectSql)) {
selectStmt.setObject(1, userId);
selectStmt.setObject(2, targetId);
ResultSet rs = selectStmt.executeQuery();
if (rs.next()) {
boolean currentlyBlocked = rs.getBoolean("is_blocked");
try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) {
updateStmt.setBoolean(1, !currentlyBlocked);
updateStmt.setObject(2, userId);
updateStmt.setObject(3, targetId);
updateStmt.executeUpdate();
}
return !currentlyBlocked;
} else {
String insertSql = "INSERT INTO contacts (user_id, contact_id, is_blocked) VALUES (?, ?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {
insertStmt.setObject(1, userId);
insertStmt.setObject(2, targetId);
insertStmt.setBoolean(3, true);
insertStmt.executeUpdate();
}
return true;
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static List<JSONObject> getBlockedUsers(UUID userId) {
String sql = """
SELECT u.internal_uuid, u.user_id, u.profile_name
FROM contacts c
JOIN users u ON u.internal_uuid = c.contact_id
WHERE c.user_id = ? AND c.is_blocked = TRUE
ORDER BY u.profile_name NULLS LAST, u.user_id
""";
List<JSONObject> out = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
JSONObject j = new JSONObject();
j.put("internal_uuid", rs.getObject("internal_uuid").toString());
j.put("user_id", rs.getString("user_id"));
j.put("profile_name", rs.getString("profile_name"));
out.add(j);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return out;
}
public static UUID findOtherUserInPrivateChat(UUID chatId, UUID viewerId) {
if (chatId == null || viewerId == null) return null;
final String sql = """
SELECT user1_id, user2_id
FROM private_chat
WHERE chat_id = ?
""";
try (Connection cn = getConnection();
PreparedStatement ps = cn.prepareStatement(sql)) {
ps.setObject(1, chatId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
UUID u1 = (UUID) rs.getObject("user1_id");
UUID u2 = (UUID) rs.getObject("user2_id");
if (viewerId.equals(u1)) return u2;
if (viewerId.equals(u2)) return u1;
// اگر viewer عضو این چت نیست
return null;
}
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public boolean unblockContact(UUID user_id, UUID contact_id) {
String sql = "UPDATE contacts SET is_blocked = FALSE WHERE user_id = ? AND contact_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, user_id);
stmt.setObject(2, contact_id);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<Contact> getContacts(UUID user_id) {
List<Contact> contacts = new ArrayList<>();
String sql = "SELECT * FROM contacts WHERE user_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, user_id);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
UUID contactId = (UUID) rs.getObject("contact_id");
boolean isBlocked = rs.getBoolean("is_blocked");
Timestamp addedAt = rs.getTimestamp("added_at");
Contact contact = new Contact(user_id, contactId);
contact.setIs_blocked(isBlocked);
contact.setAdd_at(addedAt.toLocalDateTime());
contacts.add(contact);
}
} catch (SQLException e) {
e.printStackTrace();
}
return contacts;
}
public static boolean existsContact(UUID user_id, UUID contact_id) {
String sql = "SELECT 1 FROM contacts WHERE user_id = ? AND contact_id = ? LIMIT 1"; // stop searching when find the first item in DB(LIMIT 1)
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, user_id);
stmt.setObject(2, contact_id);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean eitherBlocks(UUID userA, UUID userB) {
return isBlocked(userA, userB) || isBlocked(userB, userA);
}
public static boolean isBlocked(UUID user_id, UUID contact_id) {
String sql = "SELECT is_blocked FROM contacts WHERE user_id = ? AND contact_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, user_id);
stmt.setObject(2, contact_id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getBoolean("is_blocked");
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean deleteChatOneSide(UUID currentUserId, UUID otherUserId) {
String sql = """
UPDATE private_chat
SET user1_deleted = CASE WHEN user1_id = ? THEN TRUE ELSE user1_deleted END,
user2_deleted = CASE WHEN user2_id = ? THEN TRUE ELSE user2_deleted END
WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?)
""";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, currentUserId);
stmt.setObject(2, currentUserId);
stmt.setObject(3, currentUserId);
stmt.setObject(4, otherUserId);
stmt.setObject(5, otherUserId);
stmt.setObject(6, currentUserId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean deleteChatBoth(UUID currentUserId, UUID otherUserId) {
String sqlDeleteMessages = """
DELETE FROM messages
WHERE receiver_type = 'private' AND (
(sender_id = ? AND receiver_id = ?) OR
(sender_id = ? AND receiver_id = ?)
)
""";
String sqlDeleteChat = """
DELETE FROM private_chat
WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?)
""";
try (Connection conn = getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement stmtMsg = conn.prepareStatement(sqlDeleteMessages);
PreparedStatement stmtChat = conn.prepareStatement(sqlDeleteChat)) {
stmtMsg.setObject(1, currentUserId);
stmtMsg.setObject(2, otherUserId);
stmtMsg.setObject(3, otherUserId);
stmtMsg.setObject(4, currentUserId);
stmtMsg.executeUpdate();
stmtChat.setObject(1, currentUserId);
stmtChat.setObject(2, otherUserId);
stmtChat.setObject(3, otherUserId);
stmtChat.setObject(4, currentUserId);
stmtChat.executeUpdate();
conn.commit();
return true;
} catch (SQLException e) {
conn.rollback();
e.printStackTrace();
return false;
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<ContactEntry> getContactEntries(UUID userId) {
List<ContactEntry> entries = new ArrayList<>();
String sql = """
SELECT c.contact_id, c.is_blocked, u.user_id, u.profile_name, u.image_url
FROM contacts c
JOIN users u ON c.contact_id = u.internal_uuid
WHERE c.user_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
UUID contactId = UUID.fromString(rs.getString("contact_id"));
boolean isBlocked = rs.getBoolean("is_blocked");
String userIdStr = rs.getString("user_id");
String profileName = rs.getString("profile_name");
String imageUrl = rs.getString("image_url");
ContactEntry entry = new ContactEntry(contactId, userIdStr, profileName, imageUrl, isBlocked);
entries.add(entry);
}
} catch (SQLException e) {
e.printStackTrace();
}
return entries;
}
public static List<ContactEntry> searchContacts(UUID userId, String searchTerm) {
List<ContactEntry> results = new ArrayList<>();
String sql = """
SELECT u.internal_uuid, u.user_id, c.is_blocked
FROM contacts c
JOIN users u ON c.contact_id = u.internal_uuid
WHERE c.user_id = ?
AND (u.user_id ILIKE ? OR u.profile_name ILIKE ?)
""";
try (Connection connection = getConnection();
PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setString(2, "%" + searchTerm + "%");
stmt.setString(3, "%" + searchTerm + "%");
ResultSet rs = stmt.executeQuery();
userDatabase userDB = new userDatabase();
while (rs.next()) {
UUID contactId = (UUID) rs.getObject("internal_uuid");
String displayId = rs.getString("user_id");
String contact_displayId = userDB.getUserId(contactId);
String profileName = userDB.getProfileName(contactId);
String imageUrl = userDB.getProfilePicture(contactId);
boolean isBlocked = rs.getBoolean("is_blocked");
String lastSeenString = userDB.getLastSeen(contactId);
// Convert to LocalDateTime
LocalDateTime lastSeen = null;
if (!"Unknown".equals(lastSeenString)) {
lastSeen = LocalDateTime.parse(lastSeenString);
}
results.add(new ContactEntry(contactId, displayId, contact_displayId, profileName, imageUrl, isBlocked, lastSeen));
}
} catch (SQLException e) {
e.printStackTrace();
}
return results;
}
}
@@ -0,0 +1,784 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.Group;
import org.to.telegramfinalproject.Models.User;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class GroupDatabase {
public static List<Group> getGroupsByUser(UUID internalUuid) {
List<Group> groups = new ArrayList<>();
String sql = """
SELECT g.* FROM groups g
JOIN group_members gm ON g.internal_uuid = gm.group_id
WHERE gm.user_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUuid);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Group group = new Group();
group.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
group.setGroup_id(rs.getString("group_id"));
group.setGroup_name(rs.getString("group_name"));
group.setCreator_id(UUID.fromString(rs.getString("creator_id")));
group.setImage_url(rs.getString("image_url"));
group.setDescription(rs.getString("description"));
group.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
groups.add(group);
}
} catch (SQLException e) {
e.printStackTrace();
}
return groups;
}
public static List<Group> searchGroups(String keyword) {
List<Group> result = new ArrayList<>();
String sql = "SELECT * FROM groups WHERE group_name ILIKE ? OR group_id ILIKE ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, "%" + keyword + "%");
stmt.setString(2, keyword);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Group group = new Group(
UUID.fromString(rs.getString("internal_uuid")),
rs.getString("group_name"),
UUID.fromString(rs.getString("creator_id")),
rs.getTimestamp("created_at").toLocalDateTime()
);
group.setGroup_id(rs.getString("group_id"));
group.setImage_url(rs.getString("image_url"));
group.setDescription(rs.getString("description"));
result.add(group);
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
public static List<UUID> getMemberUUIDs(UUID groupInternalUUID) {
List<UUID> memberIds = new ArrayList<>();
String sql = "SELECT user_id FROM group_members WHERE group_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupInternalUUID);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
memberIds.add((UUID) rs.getObject("user_id"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return memberIds;
}
public static Group findByGroupId(String groupId) {
String sql = "SELECT * FROM groups WHERE group_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, groupId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Group group = new Group();
group.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
group.setGroup_id(rs.getString("group_id"));
group.setGroup_name(rs.getString("group_name"));
group.setImage_url(rs.getString("image_url"));
group.setCreator_id(UUID.fromString(rs.getString("creator_id")));
group.setDescription(rs.getString("description"));
group.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
return group;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean isUserInGroup(UUID userId, UUID groupInternalId) {
String sql = "SELECT * FROM group_members WHERE user_id = ? AND group_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, groupInternalId);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean updateGroupInfo(UUID internalUUID, String newGroupId, String name, String description, String imageUrl) {
String sql = "UPDATE groups SET group_id = ?, group_name = ?, description = ?, image_url = ? WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, newGroupId);
stmt.setString(2, name);
stmt.setString(3, description);
if (imageUrl == null) {
stmt.setNull(4, Types.VARCHAR);
} else {
stmt.setString(4, imageUrl);
}
stmt.setObject(5, internalUUID);
int affectedRows = stmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean isGroupIdUnique(String groupId, UUID excludeUUID) {
String sql = "SELECT COUNT(*) FROM groups WHERE group_id = ? AND internal_uuid != ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, groupId);
stmt.setObject(2, excludeUUID);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getInt(1) == 0;
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static void addMember(UUID groupInternalId, UUID userId) {
String sql = "INSERT INTO group_members (group_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupInternalId);
stmt.setObject(2, userId);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static UUID findInternalUUIDByGroupId(String groupId) {
String sql = "SELECT internal_uuid FROM groups WHERE group_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, groupId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("internal_uuid");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean addMemberToGroup(UUID userId, UUID groupUUID) {
String sql = """
INSERT INTO group_members (group_id, user_id)
VALUES (?, ?)
ON CONFLICT DO NOTHING
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupUUID);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean createGroup(Group group, UUID creatorId) {
String sql = """
INSERT INTO groups (
internal_uuid, group_id, group_name,
creator_id, image_url, description, created_at
)
VALUES (gen_random_uuid(), ?, ?, ?, ?, ?, ?)
RETURNING internal_uuid
""";
String memberSql = """
INSERT INTO group_members (group_id, user_id) VALUES (?, ?)
""";
try (Connection conn = ConnectionDb.connect()) {
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, group.getGroup_id());
stmt.setString(2, group.getGroup_name());
stmt.setObject(3, creatorId);
stmt.setString(4, group.getImage_url());
stmt.setString(5, group.getDescription());
stmt.setObject(6, group.getCreated_at());
ResultSet rs = stmt.executeQuery();
if (!rs.next()) return false;
UUID internalUUID = (UUID) rs.getObject("internal_uuid");
group.setInternal_uuid(internalUUID);
PreparedStatement memberStmt = conn.prepareStatement(memberSql);
memberStmt.setObject(1, internalUUID);
memberStmt.setObject(2, creatorId);
memberStmt.executeUpdate();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static boolean insertGroup(UUID internalUUID, String groupId, String groupName, UUID creatorId, String imageUrl, LocalDateTime createdAt) {
String sql = "INSERT INTO groups (internal_uuid, group_id, group_name, creator_id, image_url, created_at) VALUES (?, ?, ?, ?, ?, ?)";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUUID);
stmt.setString(2, groupId);
stmt.setString(3, groupName);
stmt.setObject(4, creatorId);
stmt.setString(5, imageUrl);
stmt.setObject(6, createdAt);
stmt.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static void addMember(UUID groupId, UUID userId, String role) {
String sql = "INSERT INTO group_members (group_id, user_id, role) VALUES (?, ?, ?) ON CONFLICT DO NOTHING";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
stmt.setString(3, role);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static Group findByInternalUUID(UUID internalUUID) {
String sql = "SELECT * FROM groups WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUUID);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Group group = new Group();
group.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid")));
group.setGroup_id(rs.getString("group_id"));
group.setGroup_name(rs.getString("group_name"));
group.setImage_url(rs.getString("image_url"));
group.setCreator_id(UUID.fromString(rs.getString("creator_id")));
group.setDescription(rs.getString("description"));
group.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime());
return group;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean addOwnerToGroup(UUID groupId, UUID userId) {
String sql = "INSERT INTO group_members (group_id, user_id, role) VALUES (?, ?, 'owner')";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean addAdminToGroup(UUID groupId, UUID userId, JSONObject permissions) {
String sql = """
UPDATE group_members
SET role = 'admin',
permissions = ?::jsonb
WHERE group_id = ? AND user_id = ? AND role = 'member'
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, groupId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static String getGroupRole(UUID groupId, UUID userId) {
String sql = "SELECT role FROM group_members WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("role");
}
} catch (SQLException e) {
e.printStackTrace();
}
return "member";
}
public static boolean updateGroupAdminPermissions(UUID groupId, UUID userId, JSONObject permissions) {
String sql = """
UPDATE group_members
SET permissions = ?::jsonb
WHERE group_id = ? AND user_id = ? AND role = 'admin'
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, groupId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<JSONObject> getGroupAdminsAndOwner(UUID groupId) {
List<JSONObject> admins = new ArrayList<>();
String sql = "SELECT gm.user_id, gm.role, gm.permissions, u.profile_name " +
"FROM group_members gm " +
"JOIN users u ON gm.user_id = u.internal_uuid " +
"WHERE gm.group_id = ? AND gm.role IN ('owner', 'admin')";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
JSONObject obj = new JSONObject();
obj.put("user_id", rs.getObject("user_id").toString());
obj.put("role", rs.getString("role"));
obj.put("permissions", new JSONObject(rs.getString("permissions")));
obj.put("profile_name", rs.getString("profile_name"));
admins.add(obj);
}
} catch (SQLException e) {
e.printStackTrace();
}
return admins;
}
public static boolean isOwner(UUID groupId, UUID userId) {
String sql = "SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ? AND role = 'owner'";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean isAdmin(UUID groupId, UUID userId) {
String sql = "SELECT role FROM group_members WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String role = rs.getString("role");
return "admin".equals(role) || "owner".equals(role);
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static JSONObject getGroupPermissions(UUID groupId, UUID userId) {
String sql = "SELECT permissions FROM group_members WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String permissions = rs.getString("permissions");
if (permissions != null && !permissions.isBlank()) {
return new JSONObject(permissions);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return new JSONObject();
}
public static JSONArray getGroupMembers(UUID groupId) {
String sql = """
SELECT u.profile_name, u.user_id, u.internal_uuid, gm.role, gm.permissions
FROM group_members gm
JOIN users u ON gm.user_id = u.internal_uuid
WHERE gm.group_id = ?
""";
JSONArray members = new JSONArray();
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
JSONObject member = new JSONObject();
member.put("profile_name", rs.getString("profile_name"));
member.put("user_id", rs.getString("user_id"));
member.put("internal_uuid", rs.getObject("internal_uuid").toString());
member.put("role", rs.getString("role"));
String permissions = rs.getString("permissions");
if (permissions != null && !permissions.isBlank()) {
member.put("permissions", new JSONObject(permissions));
}
members.put(member);
}
return members;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public static boolean demoteAdminToMember(UUID groupId, UUID userId) {
String sql = "UPDATE group_members SET role = 'member', permissions = '{}'::jsonb WHERE group_id = ? AND user_id = ? AND role = 'admin'";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean removeMemberFromGroup(UUID groupId, UUID userId) {
String sql = "DELETE FROM group_members WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean transferOwnership(UUID groupId, UUID newOwnerId) {
String demoteOldOwner = "UPDATE group_members SET role = 'admin' WHERE group_id = ? AND role = 'owner'";
String promoteNewOwner = "UPDATE group_members SET role = 'owner', permissions = '{}'::jsonb WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect()) {
conn.setAutoCommit(false);
try (PreparedStatement demoteStmt = conn.prepareStatement(demoteOldOwner);
PreparedStatement promoteStmt = conn.prepareStatement(promoteNewOwner)) {
demoteStmt.setObject(1, groupId);
demoteStmt.executeUpdate();
promoteStmt.setObject(1, groupId);
promoteStmt.setObject(2, newOwnerId);
promoteStmt.executeUpdate();
conn.commit();
return true;
} catch (SQLException e) {
conn.rollback();
e.printStackTrace();
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean deleteGroup(UUID groupId) {
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement("DELETE FROM groups WHERE internal_uuid = ?")) {
stmt.setObject(1, groupId);
int affectedRows = stmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<UUID> getGroupMemberUUIDs(UUID groupId) {
List<UUID> memberIds = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement("SELECT user_id FROM group_members WHERE group_id = ?")) {
stmt.setObject(1, groupId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
memberIds.add(UUID.fromString(rs.getString("user_id")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return memberIds;
}
public static boolean updateAdminPermissions(UUID groupId, UUID userId, JSONObject permissions) {
String sql = "UPDATE group_members SET permissions = ?::jsonb WHERE group_id = ? AND user_id = ? AND role = 'admin'";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, permissions.toString());
stmt.setObject(2, groupId);
stmt.setObject(3, userId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean isMember(UUID groupId, UUID userId) {
String sql = "SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
return rs.next();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
// public static List<User> searchGroupMembers(UUID groupId, String keyword) {
// List<User> result = new ArrayList<>();
// String sql = """
// SELECT u.*
// FROM users u
// JOIN group_members gm ON gm.user_id = u.internal_uuid
// WHERE gm.group_id = ?
// AND (
// LOWER(u.profile_name) LIKE ?
// OR LOWER(u.username) LIKE ?
// OR LOWER(u.user_id) LIKE ?
// )
// """;
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
//
// stmt.setObject(1, groupId);
// String likePattern = "%" + keyword.toLowerCase() + "%";
// stmt.setString(2, likePattern);
// stmt.setString(3, likePattern);
// stmt.setString(4, likePattern);
//
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// User user = User.fromResultSet(rs);
// result.add(user);
// }
//
// } catch (SQLException e) {
// e.printStackTrace();
// }
// return result;
// }
public static JSONObject getGroupInfo(UUID groupId, UUID viewerUuid) throws SQLException {
JSONObject result = new JSONObject();
try (Connection conn = ConnectionDb.connect()) {
// === Group header (name, member count, etc.)
String groupQuery = """
SELECT g.internal_uuid, g.group_id, g.group_name, g.image_url,
COUNT(m.user_id) as member_count
FROM groups g
LEFT JOIN group_members m ON g.internal_uuid = m.group_id
WHERE g.internal_uuid = ?
GROUP BY g.internal_uuid, g.group_id, g.group_name, g.image_url
""";
try (PreparedStatement ps = conn.prepareStatement(groupQuery)) {
ps.setObject(1, groupId); // ✅ compare UUID with UUID
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("internal_uuid", rs.getString("internal_uuid"));
result.put("group_id", rs.getString("group_id")); // varchar handle
result.put("group_name", rs.getString("group_name"));
result.put("member_count", rs.getInt("member_count"));
result.put("image_url", rs.getString("image_url"));
} else {
return null; // no such group
}
}
// === Members (id, name, role, status, image)
JSONArray membersArr = new JSONArray();
String membersQuery = """
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
gm.role, u.status, u.last_seen
FROM group_members gm
JOIN users u ON gm.user_id = u.internal_uuid
WHERE gm.group_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(membersQuery)) {
ps.setObject(1, groupId); // ✅ gm.group_id is UUID
ResultSet rs = ps.executeQuery();
while (rs.next()) {
JSONObject member = new JSONObject();
member.put("user_id", rs.getString("internal_uuid"));
member.put("profile_name", rs.getString("profile_name"));
member.put("username", rs.getString("user_id"));
member.put("image_url", rs.getString("image_url"));
member.put("role", rs.getString("role"));
member.put("status", rs.getString("status"));
member.put("last_seen", rs.getString("last_seen"));
membersArr.put(member);
}
}
result.put("members", membersArr);
// === Viewer role (to decide if they can delete group, etc.)
String roleQuery = """
SELECT role
FROM group_members
WHERE group_id = ? AND user_id = ?
""";
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
ps.setObject(1, groupId);
ps.setObject(2, viewerUuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("my_role", rs.getString("role"));
} else {
result.put("my_role", ""); // not a member
}
}
}
return result;
}
public static JSONObject getAdminPermissions(UUID groupId, UUID adminId) throws SQLException {
String sql = """
SELECT permissions
FROM group_members
WHERE group_id = ? AND user_id = ? AND role = 'admin'
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, groupId);
ps.setObject(2, adminId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String jsonStr = rs.getString("permissions");
if (jsonStr != null && !jsonStr.isBlank()) {
return new JSONObject(jsonStr);
}
}
}
return null; // not found or no permissions
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.MediaRow;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class MessageReactionDatabase {
public static boolean saveOrUpdateReaction(UUID messageId, UUID userId, String reaction) {
String sql = """
INSERT INTO message_reactions (message_id, user_id, emoji)
VALUES (?, ?, ?)
ON CONFLICT (message_id, user_id)
DO UPDATE SET emoji = EXCLUDED.emoji, reacted_at = CURRENT_TIMESTAMP
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ps.setObject(2, userId);
ps.setString(3, reaction);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<String> getReactions(UUID messageId) {
String sql = "SELECT emoji FROM message_reactions WHERE message_id = ?";
List<String> reactions = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
reactions.add(rs.getString("emoji"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return reactions;
}
public static JSONObject getCountsAsJson(UUID messageId) {
String sql = """
SELECT emoji, COUNT(*) AS c
FROM message_reactions
WHERE message_id = ?
GROUP BY emoji
""";
JSONObject counts = new JSONObject();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String emoji = rs.getString("emoji");
int c = rs.getInt("c");
counts.put(emoji, c);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return counts; // مثال: {"❤️":2,"👍":1}
}
}
@@ -0,0 +1,413 @@
package org.to.telegramfinalproject.Database;
import org.to.telegramfinalproject.Models.PrivateChat;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
public class PrivateChatDatabase {
public static List<UUID> getMembers(UUID privateChatId) {
String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, privateChatId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
UUID user1 = (UUID) rs.getObject("user1_id");
UUID user2 = (UUID) rs.getObject("user2_id");
return new ArrayList<>(Arrays.asList(user1, user2));
}
} catch (SQLException e) {
e.printStackTrace();
}
return new ArrayList<>();
}
public static UUID getOrCreateSavedMessagesChat(UUID userId) {
String query = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?";
try (Connection conn = ConnectionDb.connect()) {
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setObject(1, userId);
stmt.setObject(2, userId); // Saved Messages is self-chat
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("chat_id"); // Chat already exists
} else {
// Create a new chat
UUID chatId = UUID.randomUUID();
String insert = "INSERT INTO private_chat (chat_id, user1_id, user2_id) VALUES (?, ?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insert)) {
insertStmt.setObject(1, chatId);
insertStmt.setObject(2, userId);
insertStmt.setObject(3, userId);
insertStmt.executeUpdate();
return chatId;
}
}
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public static UUID findChatIdByUsers(UUID user1, UUID user2) {
UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2;
UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1;
String sql = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, u1);
ps.setObject(2, u2);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("chat_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static UUID getOrCreateChat(UUID user1, UUID user2) {
UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2;
UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1;
String select = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(select)) {
ps.setObject(1, u1);
ps.setObject(2, u2);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return UUID.fromString(rs.getString("chat_id"));
}
} catch (SQLException e) {
e.printStackTrace();
}
UUID newChatId = UUID.randomUUID();
String insert = "INSERT INTO private_chat(chat_id, user1_id, user2_id) VALUES (?, ?, ?)";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(insert)) {
ps.setObject(1, newChatId);
ps.setObject(2, u1);
ps.setObject(3, u2);
ps.executeUpdate();
return newChatId;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public static List<PrivateChat> findChatsOfUser(UUID userId) {
List<PrivateChat> chats = new ArrayList<>();
String sql = "SELECT * FROM private_chat WHERE user1_id = ? OR user2_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, userId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
UUID chatId = (UUID) rs.getObject("chat_id");
UUID user1 = (UUID) rs.getObject("user1_id");
UUID user2 = (UUID) rs.getObject("user2_id");
chats.add(new PrivateChat(chatId, user1, user2));
}
} catch (SQLException e) {
e.printStackTrace();
}
return chats;
}
public static UUID getOtherUserInChat(UUID chatId, UUID currentUserId) {
String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, chatId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
UUID user1 = (UUID) rs.getObject("user1_id");
UUID user2 = (UUID) rs.getObject("user2_id");
return currentUserId.equals(user1) ? user2 : user1;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
private static PrivateChat extractPrivateChat(ResultSet rs) throws SQLException {
UUID chatId = UUID.fromString(rs.getString("chat_id"));
UUID user1 = rs.getObject("user1_id", UUID.class);
UUID user2 = rs.getObject("user2_id", UUID.class);
boolean user1Deleted = rs.getBoolean("user1_deleted");
boolean user2Deleted = rs.getBoolean("user2_deleted");
LocalDateTime createdAt = rs.getTimestamp("created_at").toLocalDateTime();
return new PrivateChat(chatId, user1, user2, user1Deleted, user2Deleted, createdAt);
}
public static void markChatDeleted(UUID userId, UUID chatId) {
String sql = """
UPDATE private_chat
SET user1_deleted = CASE WHEN user1_id = ? THEN true ELSE user1_deleted END,
user2_deleted = CASE WHEN user2_id = ? THEN true ELSE user2_deleted END
WHERE chat_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, userId);
stmt.setObject(3, chatId);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void unmarkChatDeleted(UUID userId, UUID chatId) {
String sql = """
UPDATE private_chat
SET user1_deleted = CASE WHEN user1_id = ? THEN false ELSE user1_deleted END,
user2_deleted = CASE WHEN user2_id = ? THEN false ELSE user2_deleted END
WHERE chat_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, userId);
stmt.setObject(3, chatId);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void markUser1Deleted(UUID chatId) {
updateBoolField(chatId, "user1_deleted", true);
}
public static void markUser2Deleted(UUID chatId) {
updateBoolField(chatId, "user2_deleted", true);
}
public static void markBothDeleted(UUID chatId) {
String sql = "UPDATE private_chat SET user1_deleted = true, user2_deleted = true WHERE chat_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, chatId);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void updateBoolField(UUID chatId, String field, boolean value) {
String sql = "UPDATE private_chat SET " + field + " = ? WHERE chat_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setBoolean(1, value);
ps.setObject(2, chatId);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static PrivateChat findById(UUID chatId) {
String sql = "SELECT * FROM private_chat WHERE chat_id = ?";
try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, chatId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
UUID user1_id = (UUID) rs.getObject("user1_id");
UUID user2_id = (UUID) rs.getObject("user2_id");
boolean user1_deleted = rs.getBoolean("user1_deleted");
boolean user2_deleted = rs.getBoolean("user2_deleted");
LocalDateTime created_at = rs.getTimestamp("created_at").toLocalDateTime();
return new PrivateChat(chatId, user1_id, user2_id, user1_deleted, user2_deleted, created_at);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static void clearDeletedFlag(UUID senderId, UUID chatId) {
String sql = """
UPDATE private_chat
SET user1_deleted = CASE WHEN user1_id = ? THEN FALSE ELSE user1_deleted END,
user2_deleted = CASE WHEN user2_id = ? THEN FALSE ELSE user2_deleted END
WHERE chat_id = ?
""";
try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, senderId);
ps.setObject(2, senderId);
ps.setObject(3, chatId);
int rows = ps.executeUpdate();
System.out.println("✅ clearDeletedFlag updated rows = " + rows);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static UUID findChatBetween(UUID user1, UUID user2) {
String sql = """
SELECT chat_id FROM private_chat
WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?)
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, user1);
ps.setObject(2, user2);
ps.setObject(3, user2);
ps.setObject(4, user1);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("chat_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
// public static UUID getOtherParticipant(UUID chatId, UUID me) {
// List<UUID> members = getMembers(chatId);
// for (UUID u : members) {
// if (!u.equals(me)) return u;
// }
// return null;
// }
public static UUID getOtherParticipant(UUID chatId, UUID me) {
List<UUID> members = getMembers(chatId); // باید [user1_id, user2_id] بده
if (members == null || members.isEmpty()) return null;
boolean isMember = false;
UUID other = null;
for (UUID u : members) {
if (u == null) continue;
if (u.equals(me)) {
isMember = true;
} else {
other = u;
}
}
if (!isMember) return null;
return (other != null) ? other
: me;
}
public static PrivateChat findSelfChat(UUID userId) {
String sql = """
SELECT chat_id, user1_id, user2_id, user1_deleted, user2_deleted
FROM private_chat
WHERE user1_id = ? AND user2_id = ?
LIMIT 1
""";
try (Connection c = ConnectionDb.connect();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return map(rs);
}
} catch (SQLException e) { e.printStackTrace(); }
return null;
}
public static UUID createSelfChat(UUID userId) {
String sql = """
INSERT INTO private_chat (chat_id, user1_id, user2_id, user1_deleted, user2_deleted, created_at)
VALUES (gen_random_uuid(), ?, ?, FALSE, FALSE, NOW())
RETURNING chat_id
""";
try (Connection c = ConnectionDb.connect();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setObject(1, userId);
ps.setObject(2, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return (UUID) rs.getObject("chat_id");
}
} catch (SQLException e) { e.printStackTrace(); }
return null;
}
private static PrivateChat map(ResultSet rs) throws SQLException {
return new PrivateChat(
(UUID) rs.getObject("chat_id"),
(UUID) rs.getObject("user1_id"),
(UUID) rs.getObject("user2_id"),
rs.getBoolean("user1_deleted"),
rs.getBoolean("user2_deleted")
);
}
public static boolean isParticipant(java.util.UUID chatId, java.util.UUID userId) {
String sql = """
SELECT 1
FROM private_chat
WHERE chat_id = ?
AND (user1_id = ? OR user2_id = ?)
LIMIT 1
""";
try (var c = ConnectionDb.connect();
var ps = c.prepareStatement(sql)) {
ps.setObject(1, chatId);
ps.setObject(2, userId);
ps.setObject(3, userId);
try (var rs = ps.executeQuery()) {
return rs.next();
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
@@ -0,0 +1,450 @@
package org.to.telegramfinalproject.Database;
import org.to.telegramfinalproject.Models.User;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class userDatabase {
public userDatabase() {
}
public static boolean isUserOnline(UUID userId) {
String sql = "SELECT status FROM users WHERE internal_uuid = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String status = rs.getString("status");
return "online".equalsIgnoreCase(status);
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
private static Connection getConnection() throws SQLException {
return ConnectionDb.connect();
}
public static String getLastSeen(UUID userId) {
String sql = "SELECT last_seen FROM users WHERE internal_uuid = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Timestamp lastSeen = rs.getTimestamp("last_seen");
if (lastSeen != null) {
return lastSeen.toLocalDateTime().toString();
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return "Unknown";
}
public User findByUserId(String userId) {
String query = "SELECT * FROM users WHERE user_id = ?";
try {
User var6;
try (Connection conn = ConnectionDb.connect()) {
try (PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, userId);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
return null;
}
var6 = this.extractUser(rs);
}
}
return var6;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public User findByUsername(String username) {
String query = "SELECT * FROM users WHERE username = ?";
try {
User var6;
try (Connection conn = this.getConnection()) {
try (PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
return null;
}
var6 = this.extractUser(rs);
}
}
return var6;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
public boolean existsByUsername(String username) {
String query = "SELECT 1 FROM users WHERE username = ?";
try {
boolean var6;
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
) {
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
var6 = rs.next();
}
return var6;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean existsByUserId(String user_id) {
String query = "SELECT 1 FROM users WHERE user_id = ?";
try {
boolean var6;
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
) {
stmt.setString(1, user_id);
ResultSet rs = stmt.executeQuery();
var6 = rs.next();
}
return var6;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean save(User user) {
String query = "INSERT INTO users (user_id, internal_uuid, username, password, profile_name, bio, image_url) VALUES (?, ?, ?, ?, ?, ?, ?)";
try {
boolean var5;
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
) {
stmt.setString(1, user.getUser_id());
stmt.setObject(2, user.getInternal_uuid());
stmt.setString(3, user.getUsername());
stmt.setString(4, user.getPassword());
stmt.setString(5, user.getProfile_name());
stmt.setString(6, user.getBio());
stmt.setString(7, user.getImage_url());
var5 = stmt.executeUpdate() > 0;
}
return var5;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean updateByUUID(UUID uuid, User user) {
String query = "UPDATE users SET user_id=?, username=?, password=?, profile_name=?, bio=?, image_url=?, status=?, last_seen=? WHERE internal_uuid=?";
try {
boolean var6;
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
) {
stmt.setString(1, user.getUser_id());
stmt.setString(2, user.getUsername());
stmt.setString(3, user.getPassword());
stmt.setString(4, user.getProfile_name());
stmt.setString(5, user.getBio());
stmt.setString(6, user.getImage_url());
stmt.setString(7, user.getStatus());
stmt.setObject(8, user.getLast_seen());
stmt.setObject(9, uuid);
var6 = stmt.executeUpdate() > 0;
}
return var6;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public List<User> getAll() {
List<User> users = new ArrayList();
String query = "SELECT * FROM users";
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
) {
while(rs.next()) {
users.add(this.extractUser(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
private User extractUser(ResultSet rs) throws SQLException {
return new User(rs.getString("user_id"), UUID.fromString(rs.getString("internal_uuid")), rs.getString("username"), rs.getString("password"), rs.getString("profile_name"), rs.getString("bio"), rs.getString("image_url"));
}
public boolean deleteByUUID(UUID uuid) {
String query = "DELETE FROM users WHERE internal_uuid = ?";
try {
boolean var5;
try (
Connection conn = this.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
) {
stmt.setObject(1, uuid);
var5 = stmt.executeUpdate() > 0;
}
return var5;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static void updateUserStatus(UUID uuid, String status) {
String sql = "UPDATE users SET status = ? WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, status);
stmt.setObject(2, uuid);
int rows = stmt.executeUpdate();
System.out.println("🔁 updateUserStatus: set '" + status + "' for " + uuid + " → affected rows = " + rows);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void updateLastSeen(UUID uuid) {
String sql = "UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, uuid);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static User findByInternalUUID(UUID internalUuid) {
String sql = "SELECT * FROM users WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, internalUuid);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
User user = new User(
rs.getString("user_id"),
UUID.fromString(rs.getString("internal_uuid")),
rs.getString("username"),
rs.getString("password"),
rs.getString("profile_name"),
rs.getString("bio"),
rs.getString("image_url")
);
user.setBio(rs.getString("bio"));
user.setImage_url(rs.getString("image_url"));
user.setStatus(rs.getString("status"));
Timestamp lastSeenTs = rs.getTimestamp("last_seen");
if (lastSeenTs != null) {
user.setLast_seen(lastSeenTs.toLocalDateTime());
}
return user;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<User> searchUsers(String keyword, UUID currentUserId) {
String query = """
SELECT * FROM users
WHERE (user_id ILIKE ? OR profile_name ILIKE ?)
AND internal_uuid <> ?
""";
List<User> result = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, "%" + keyword + "%");
stmt.setString(2, "%" + keyword + "%");
stmt.setObject(3, currentUserId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractUser(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
public static void setAllUsersOffline() {
String sql = "UPDATE users SET status = 'offline'";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
int affected = stmt.executeUpdate();
System.out.println("🔁 All users set to offline. Rows affected: " + affected);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static String getProfileName(UUID userId) {
String sql = "SELECT profile_name FROM users WHERE internal_uuid = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String profileName = rs.getString("profile_name");
return profileName;
}
} catch (SQLException e) {
e.printStackTrace();
}
return "Unknown";
}
public static String getProfilePicture(UUID userId) {
String sql = "SELECT image_url FROM users WHERE internal_uuid = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String imageUrl = rs.getString("image_url");
return imageUrl;
}
} catch (SQLException e) {
e.printStackTrace();
}
return "Unknown";
}
public static String getUserId(UUID userId) {
String sql = "SELECT user_id FROM users WHERE internal_uuid = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
String user_id = rs.getString("user_id");
return user_id;
}
} catch (SQLException e) {
e.printStackTrace();
}
return "Unknown";
}
public static String getPasswordHash(UUID userId) {
String sql = "SELECT password FROM users WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return rs.getString("password");
}
} catch (SQLException e) { e.printStackTrace(); }
return null;
}
public static boolean updateUsername(UUID userId, String newUsername) throws SQLException {
String sql = "UPDATE users SET username = ? WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newUsername);
ps.setObject(2, userId);
ps.executeUpdate();
return true;
} catch (SQLException e) {
// 23505 = unique_violation در PostgreSQL
if ("23505".equals(e.getSQLState())) throw e;
throw e;
}
}
public static boolean updatePasswordHash(UUID userId, String newHash) {
String sql = "UPDATE users SET password = ? WHERE internal_uuid = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newHash);
ps.setObject(2, userId);
return ps.executeUpdate() > 0;
} catch (SQLException e) { e.printStackTrace(); return false; }
}
}
@@ -1,23 +0,0 @@
package org.to.telegramfinalproject;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
@@ -1,14 +0,0 @@
package org.to.telegramfinalproject;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class HelloController {
@FXML
private Label welcomeText;
@FXML
protected void onHelloButtonClick() {
welcomeText.setText("Welcome to JavaFX Application!");
}
}
@@ -0,0 +1,49 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class Channel {
private UUID internal_uuid;
private String channel_id;
private String channel_name;
private UUID creator_id;
private String image_url;
private String description;
private LocalDateTime created_at;
private List<ChannelSubscribe> members;
public Channel(UUID internal_uuid, String channel_name, UUID creator_id, LocalDateTime created_at){
this.internal_uuid = internal_uuid;
this.channel_name = channel_name;
this.creator_id = creator_id;
this.created_at = created_at;
}
public Channel() {
}
public void setChannel_id(String Channel_id){this.channel_id = Channel_id;}
public void setCreator_id(UUID creator_id){this.creator_id = creator_id;}
public void setChannel_name(String channel_name){this.channel_name = channel_name;}
public void setImage_url(String image_url){this.image_url = image_url;}
public void setCreated_at(LocalDateTime created_at){this.created_at = created_at;}
public void setDescription(String description){this.description =description;}
public void setMembers(List<ChannelSubscribe> members){this.members = members;}
public String getChannel_id(){return this.channel_id;}
public UUID getCreator_id(){return this.creator_id;}
public String getChannel_name(){return this.channel_name;}
public String getImage_url(){return this.image_url;}
public LocalDateTime getCreated_at(){return this.created_at;}
public String getDescription(){return this.description;}
public List<ChannelSubscribe> getMembers(){return this.members;}
public UUID getInternal_uuid() {
return this.internal_uuid;
}
public void setInternal_uuid(UUID internalUuid) { this.internal_uuid = internalUuid;
}
}
@@ -0,0 +1,136 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.UUID;
public class ChannelAdmin {
private String channelId;
private UUID userId;
private LocalDateTime addedAt;
private UUID addedBy;
private String role; // e.g., "owner", "admin"
private boolean canPostMessage;
private boolean canEditSetting;
private boolean canDeleteMessages;
private boolean canDeleteMembers;
private boolean canAddMembers;
public ChannelAdmin(String channelId, UUID userId, LocalDateTime addedAt, UUID addedBy,
String role, boolean canPostMessage, boolean canEditSetting, boolean canDeleteMessages,
boolean canDeleteMembers, boolean canAddMembers) {
this.channelId = channelId;
this.userId = userId;
this.addedAt = addedAt;
this.addedBy = addedBy;
this.role = role;
this.canPostMessage = canPostMessage;
this.canEditSetting = canEditSetting;
this.canDeleteMessages = canDeleteMessages;
this.canDeleteMembers = canDeleteMembers;
this.canAddMembers = canAddMembers;
}
// Overloaded constructor with default values from SQL
public ChannelAdmin(String channelId, UUID userId, UUID addedBy) {
this.channelId = channelId;
this.userId = userId;
this.addedBy = addedBy;
}
// Getters and setters
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public LocalDateTime getAddedAt() {
return addedAt;
}
public void setAddedAt(LocalDateTime addedAt) {
this.addedAt = addedAt;
}
public UUID getAddedBy() {
return addedBy;
}
public void setAddedBy(UUID addedBy) {
this.addedBy = addedBy;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public boolean getCanPostMessage() {
return canPostMessage;
}
public void setCanPostMessage(boolean canPostMessage) {
this.canPostMessage = canPostMessage;
}
public boolean getCanEditSetting() {
return canEditSetting;
}
public void setCanEditSetting(boolean canEditSetting) {
this.canEditSetting = canEditSetting;
}
public boolean getCanDeleteMessages() {
return canDeleteMessages;
}
public void setCanDeleteMessages(boolean canDeleteMessages) {
this.canDeleteMessages = canDeleteMessages;
}
public boolean getCanDeleteMembers() {
return canDeleteMembers;
}
public void setCanDeleteMembers(boolean canDeleteMembers) {
this.canDeleteMembers = canDeleteMembers;
}
public boolean getCanAddMembers() {
return canAddMembers;
}
public void setCanAddMembers(boolean canAddMembers) {
this.canAddMembers = canAddMembers;
}
@Override
public String toString() {
return "ChannelAdmin{" +
"channelId='" + channelId + '\'' +
", userId=" + userId +
", addedAt=" + addedAt +
", addedBy=" + addedBy +
", role='" + role + '\'' +
", canPost=" + canPostMessage +
", canEdit=" + canEditSetting +
", canDeleteMessages=" + canDeleteMessages +
", canDeleteMembers=" + canDeleteMembers +
", canAddMembers=" + canAddMembers +
'}';
}
}
@@ -0,0 +1,29 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.UUID;
public class ChannelSubscribe{
private UUID channel_id;
private UUID user_id;
private LocalDateTime Subscribed_at;
public ChannelSubscribe(UUID channel_id, UUID user_id, String role){
this.channel_id = channel_id;
this.user_id = user_id;
this.Subscribed_at = LocalDateTime.now();
}
public void setChannel_id(UUID group_id){this.channel_id = group_id;}
public void setUser_id(UUID user_id){this.user_id = user_id;}
public void setJoin_at(LocalDateTime join_at){this.Subscribed_at = join_at;}
public UUID getChannel_id(){return this.channel_id;}
public UUID getUser_id(){return this.user_id;}
public LocalDateTime getJoin_at(){return this.Subscribed_at;}
}
@@ -0,0 +1,213 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
import java.time.LocalDateTime;
import java.util.UUID;
public class ChatEntry {
private UUID internalId;
private String displayId;
private String name;
private String imageUrl;
private String type;
private LocalDateTime lastMessageTime;
private boolean archived = false;
private UUID otherUser;
private boolean savedMessages = false;
private boolean isOwner = false;
private boolean isAdmin = false;
private JSONObject permissions;
// private UUID otherUserId;
private int unreadCount;
private String lastMessagePreview;
private String lastMessageType; // TEXT / IMAGE / AUDIO / VIDEO / ...
private UUID lastMessageSenderId;
public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime) {
this.internalId = internalId;
this.displayId = displayId;
this.name = name;
this.imageUrl = imageUrl;
this.type = type;
this.lastMessageTime = lastMessageTime;
}
public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime, boolean isOwner, boolean isAdmin) {
this(internalId, displayId, name, imageUrl, type, lastMessageTime);
this.isOwner = isOwner;
this.isAdmin = isAdmin;
this.permissions = permissions;
}
public ChatEntry() {
}
public ChatEntry(UUID internalId, String type, String name, String displayId) {
this.internalId = internalId;
this.type = type;
this.displayId = displayId;
this.name = name;
}
public boolean isOwner() {
return isOwner;
}
public void setOwner(boolean owner) {
isOwner = owner;
}
public boolean isAdmin() {
return isAdmin;
}
public void setAdmin(boolean admin) {
isAdmin = admin;
}
public UUID getId() {
return internalId;
}
public String getDisplayId() {
return displayId;
}
public String getName() {
return name;
}
public String getImageUrl() {
return imageUrl;
}
public String getType() {
return type;
}
public LocalDateTime getLastMessageTime() {
return lastMessageTime;
}
public JSONObject getPermissions() {
return permissions;
}
public void setPermissions(JSONObject permissions) {
this.permissions = permissions;
}
public void setName(String name) {this.name = name;
}
public void setDisplayId(String id) {this.displayId = id;
}
public void setImageUrl(String image_url) {this.imageUrl = image_url;
}
public void setType(String type) {this.type =type;
}
public void setId(String internalId) {this.internalId = UUID.fromString(internalId);
}
public boolean isArchived() {
return archived;
}
public void setArchived(boolean archived) {
this.archived = archived;
}
public void setLastMessageTime(String newTime) {
if (newTime == null || newTime.isBlank()) {
this.lastMessageTime = null;
return;
}
try {
this.lastMessageTime = LocalDateTime.parse(newTime);
} catch (Exception e) {
System.out.println("❌ Failed to parse lastMessageTime: " + newTime);
this.lastMessageTime = null;
}
}
public void setOtherUserId(UUID otherId) {this.otherUser = otherId;
}
public UUID getOtherUserId(){
return otherUser;
}
public boolean isSavedMessages() {
return savedMessages;
}
public void setSavedMessages(boolean savedMessages) {
this.savedMessages = savedMessages;
}
public int getUnreadCount() { return unreadCount; }
public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; }
public String getLastMessagePreview() { return lastMessagePreview; }
public void setLastMessagePreview(String lastMessagePreview) { this.lastMessagePreview = lastMessagePreview; }
public String getLastMessageType() { return lastMessageType; }
public void setLastMessageType(String lastMessageType) { this.lastMessageType = lastMessageType; }
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;
}
public static ChatEntry fromServer(UUID internalId,
String type,
String name,
String displayId,
String imageUrl,
boolean isOwner,
boolean isAdmin) {
ChatEntry e = new ChatEntry(internalId, type, name, displayId); // اگر سازنده‌ات فرق دارد، مطابق آن بساز
e.setImageUrl(imageUrl);
e.setOwner(isOwner);
e.setAdmin(isAdmin);
return e;
}
}
@@ -0,0 +1,44 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONArray;
import org.json.JSONObject;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class Contact {
private UUID user_id;
private UUID contact_id;
private LocalDateTime added_at;
private Boolean is_blocked;
public Contact(UUID user_id, UUID contact_id){
this.user_id = user_id;
this.contact_id = contact_id;
this.added_at = LocalDateTime.now();
}
public void setUser_id(UUID user_id){this.user_id = user_id;}
public void setContact_id(UUID contact_id){this.contact_id = contact_id;}
public void setAdd_at(LocalDateTime add_at){this.added_at =add_at;}
public void setIs_blocked(Boolean is_blocked){this.is_blocked =is_blocked;}
public UUID getUser_id(){
return this.user_id;
}
public UUID getContact_id(){return this.contact_id;}
public LocalDateTime getAdd_at() {
return added_at;
}
public Boolean getIs_blocked(){
return is_blocked;
}
}
@@ -0,0 +1,87 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import org.json.JSONObject;
import java.util.UUID;
public class ContactEntry {
private UUID contactId; // internal UUID
private String userId; // public ID
private String profileName;
private String imageUrl;
private boolean isBlocked;
private LocalDateTime lastSeenTime;
private String contact_displayId;
public ContactEntry(UUID contactId, String userId, String profileName, String imageUrl, boolean isBlocked) {
this.contactId = contactId;
this.userId = userId;
this.profileName = profileName;
this.imageUrl = imageUrl;
this.isBlocked = isBlocked;
}
public ContactEntry(UUID contactId, String userId,String contact_displayId , String profileName, String imageUrl, boolean isBlocked, LocalDateTime lastSeenTime){
this.contactId = contactId;
this.userId = userId;
this.contact_displayId = contact_displayId;
this.profileName = profileName;
this.imageUrl = imageUrl;
this.isBlocked = isBlocked;
this.lastSeenTime = lastSeenTime;
}
public UUID getContactId() {
return contactId;
}
public String getUserId() {
return userId;
}
public String getProfileName() {
return profileName;
}
public String getImageUrl() {
return imageUrl;
}
public boolean isBlocked() {
return isBlocked;
}
public LocalDateTime getLastSeenTime() {
return lastSeenTime;
}
public void setLastSeenTime(LocalDateTime lastSeenTime) {
this.lastSeenTime = lastSeenTime;
}
public String getContact_displayId() {
return contact_displayId;
}
public void setContact_displayId(String contact_displayId) {
this.contact_displayId = contact_displayId;
}
@Override
public String toString() {
return profileName + " (@" + contact_displayId + ")" + (isBlocked ? " [Blocked]" : "") + " Last Seen:" + (lastSeenTime != null ? " " + lastSeenTime.toString() : "");
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
obj.put("contact_id", contactId.toString());
obj.put("user_id", userId);
obj.put("profile_name", profileName);
obj.put("image_url", imageUrl);
obj.put("is_blocked", isBlocked);
return obj;
}
}
@@ -0,0 +1,23 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
public class ContactRequestModel {
private String event;
private String contactId;
private String userId;
public ContactRequestModel(String event, String contactId, String userId) {
this.event = event;
this.contactId = contactId;
this.userId = userId;
}
public JSONObject toJson() {
JSONObject json = new JSONObject();
json.put("event", event);
json.put("contact_id", contactId);
json.put("user_id", userId);
return json;
}
}
@@ -0,0 +1,162 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
import java.util.Objects;
import java.util.UUID;
public class FileAttachment {
private UUID attachmentId; // اختیاری؛ اگر null بود، تولید می‌کنیم
private UUID mediaKey;
private String fileUrl;
private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER
private String fileName;
private Long fileSize;
private String mimeType; // e.g., image/png
private Integer width;
private Integer height;
private Integer durationSeconds; // for audio/video
private String thumbnailUrl;
private String storagePath;
public FileAttachment(String fileUrl,
String fileType,
String fileName,
Long fileSize,
String mimeType,
Integer width,
Integer height,
Integer durationSeconds,
String thumbnailUrl) {
this.fileUrl = fileUrl;
this.fileType = fileType;
this.fileName = fileName;
this.fileSize = fileSize;
this.mimeType = mimeType;
this.width = width;
this.height = height;
this.durationSeconds = durationSeconds;
this.thumbnailUrl = thumbnailUrl;
}
public FileAttachment(String fileUrl, String fileType) {
this(fileUrl, fileType, null, null, null, null, null, null, null);
}
public FileAttachment() {
}
// ساخت از JSON /upload
public static FileAttachment fromUploadJson(JSONObject j) {
return new FileAttachment(
j.optString("file_url", ""),
j.optString("file_type", "FILE"),
emptyToNull(j.optString("file_name", null)),
j.has("file_size") && !j.isNull("file_size") ? j.getLong("file_size") : null,
emptyToNull(j.optString("mime_type", null)),
j.has("width") && !j.isNull("width") ? j.getInt("width") : null,
j.has("height") && !j.isNull("height") ? j.getInt("height") : null,
j.has("duration_seconds") && !j.isNull("duration_seconds") ? j.getInt("duration_seconds") : null,
j.isNull("thumbnail_url") ? null : emptyToNull(j.optString("thumbnail_url", null))
);
}
public JSONObject toJson() {
JSONObject out = new JSONObject()
.put("file_url", fileUrl)
.put("file_type", fileType);
out.put("file_name", fileName == null ? JSONObject.NULL : fileName);
out.put("file_size", fileSize == null ? JSONObject.NULL : fileSize);
out.put("mime_type", mimeType == null ? JSONObject.NULL : mimeType);
out.put("width", width == null ? JSONObject.NULL : width);
out.put("height", height == null ? JSONObject.NULL : height);
out.put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds);
out.put("thumbnail_url", thumbnailUrl == null ? JSONObject.NULL : thumbnailUrl);
return out;
}
// Helpers
public boolean isImage() { return "IMAGE".equalsIgnoreCase(fileType) || "GIF".equalsIgnoreCase(fileType); }
public boolean isAudio() { return "AUDIO".equalsIgnoreCase(fileType); }
public boolean hasDimensions() { return width != null && height != null; }
private static String emptyToNull(String s) {
return (s == null || s.isBlank()) ? null : s;
}
// Getters
public String getFileUrl() { return fileUrl; }
public String getFileType() { return fileType; }
public String getFileName() { return fileName; }
public Long getFileSize() { return fileSize; }
public String getMimeType() { return mimeType; }
public Integer getWidth() { return width; }
public Integer getHeight() { return height; }
public Integer getDurationSeconds() { return durationSeconds; }
public String getThumbnailUrl() { return thumbnailUrl; }
// equals/hashCode/toString
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FileAttachment)) return false;
FileAttachment that = (FileAttachment) o;
return Objects.equals(fileUrl, that.fileUrl) &&
Objects.equals(fileType, that.fileType) &&
Objects.equals(fileName, that.fileName) &&
Objects.equals(fileSize, that.fileSize) &&
Objects.equals(mimeType, that.mimeType) &&
Objects.equals(width, that.width) &&
Objects.equals(height, that.height) &&
Objects.equals(durationSeconds, that.durationSeconds) &&
Objects.equals(thumbnailUrl, that.thumbnailUrl);
}
@Override public int hashCode() {
return Objects.hash(fileUrl, fileType, fileName, fileSize, mimeType, width, height, durationSeconds, thumbnailUrl);
}
@Override public String toString() {
return "FileAttachment{" +
"fileUrl='" + fileUrl + '\'' +
", fileType='" + fileType + '\'' +
", fileName='" + fileName + '\'' +
", fileSize=" + fileSize +
", mimeType='" + mimeType + '\'' +
", width=" + width +
", height=" + height +
", durationSeconds=" + durationSeconds +
", thumbnailUrl='" + thumbnailUrl + '\'' +
'}';
}
public UUID getAttachmentId() {return attachmentId;
}
public UUID getMediaKey() {return mediaKey;
}
public String getStoragePath() {return storagePath;
}
public void setAttachmentId(UUID attachmentId) {this.attachmentId = attachmentId;
}
public void setMediaKey(UUID mediaKey) {this.mediaKey = mediaKey;
}
public void setFileUrl(String fileUrl) {this.fileUrl = fileUrl;
}
public void setFileType(String fileType){this.fileType = fileType;}
public void setFileName(String fileName){this.fileName = fileName;}
public void setFileSize(Long fileSize){this.fileSize = fileSize;}
public void setMimeType(String mimeType){this.mimeType = mimeType;}
public void setWidth(int width){this.width = width;}
public void setHeight(int height){this.height = height;}
public void setDurationSeconds(Integer durationSeconds){this.durationSeconds = durationSeconds;}
public void setThumbnailUrl(String thumbnailUrl){this.thumbnailUrl = thumbnailUrl;}
public void setStoragePath(String storagePath) {this.storagePath = storagePath;
}
}
@@ -0,0 +1,49 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class Group {
private UUID internal_uuid;
private String group_id;
private String group_name;
private UUID creator_id;
private String image_url;
private String description;
private LocalDateTime created_at;
private List<GroupMember> members;
public Group(UUID internal_uuid, String group_name, UUID creator_id, LocalDateTime created_at){
this.internal_uuid = internal_uuid;
this.group_name = group_name;
this.creator_id = creator_id;
this.created_at = created_at;
}
public Group() {
}
public void setGroup_id(String group_id){this.group_id = group_id;}
public void setCreator_id(UUID creator_id){this.creator_id = creator_id;}
public void setGroup_name(String group_name){this.group_name = group_name;}
public void setImage_url(String image_url){this.image_url = image_url;}
public void setCreated_at(LocalDateTime created_at){this.created_at = created_at;}
public void setDescription(String description){this.description =description;}
public void setMembers(List<GroupMember> members){this.members = members;}
public String getGroup_id(){return this.group_id;}
public UUID getCreator_id(){return this.creator_id;}
public String getGroup_name(){return this.group_name;}
public String getImage_url(){return this.image_url;}
public LocalDateTime getCreated_at(){return this.created_at;}
public String getDescription(){return this.description;}
public List<GroupMember> getMembers(){return this.members;}
public UUID getInternal_uuid() {
return this.internal_uuid;
}
public void setInternal_uuid(UUID internalUuid) { this.internal_uuid = internalUuid;
}
}
@@ -0,0 +1,32 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.UUID;
public class GroupMember {
private UUID group_id;
private UUID user_id;
private LocalDateTime join_at;
private String role;
public GroupMember(UUID group_id, UUID user_id, String role){
this.group_id = group_id;
this.user_id = user_id;
this.join_at = LocalDateTime.now();
this.role = role;
}
public void setGroup_id(UUID group_id){this.group_id = group_id;}
public void setUser_id(UUID user_id){this.user_id = user_id;}
public void setJoin_at(LocalDateTime join_at){this.join_at = join_at;}
public void setRole(String role){this.role = role;}
public UUID getGroup_id(){return this.group_id;}
public UUID getUser_id(){return this.user_id;}
public LocalDateTime getJoin_at(){return this.join_at;}
public String getRole(){return this.role;}
}
@@ -0,0 +1,215 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.userDatabase;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class JsonUtil {
public static JSONArray contactListToJson(List<Contact> contacts) {
JSONArray array = new JSONArray();
for (Contact contact : contacts) {
UUID contactId = contact.getContact_id();
User contactUser = userDatabase.findByInternalUUID(contactId);
if (contactUser == null) continue;
JSONObject obj = new JSONObject();
obj.put("contact_id", contactId.toString()); // UUID
obj.put("user_id", contactUser.getUser_id()); // public ID
obj.put("profile_name", contactUser.getProfile_name());
obj.put("image_url", contactUser.getImage_url());
obj.put("is_blocked", contact.getIs_blocked());
array.put(obj);
}
return array;
}
public static JSONArray contactEntryListToJson(List<ContactEntry> contactEntries) {
JSONArray array = new JSONArray();
for (ContactEntry entry : contactEntries) {
JSONObject obj = new JSONObject();
obj.put("contact_id", entry.getContactId().toString());
obj.put("user_id", entry.getUserId());
obj.put("profile_name", entry.getProfileName());
obj.put("image_url", entry.getImageUrl());
obj.put("is_blocked", entry.isBlocked());
array.put(obj);
}
return array;
}
public static JSONObject userToJson(User user) {
JSONObject obj = new JSONObject();
obj.put("internal_uuid", user.getInternal_uuid().toString());
obj.put("user_id", user.getUser_id() != null ?user.getUser_id().toString() :JSONObject.NULL);
obj.put("username", user.getUsername());
obj.put("profile_name", user.getProfile_name());
obj.put("bio", user.getBio()!= null ?user.getBio().toString() :JSONObject.NULL);
obj.put("image_url", user.getImage_url() != null ?user.getImage_url().toString() :JSONObject.NULL);
obj.put("status", user.getStatus());
obj.put("last_seen", user.getLast_seen() != null ? user.getLast_seen().toString() : JSONObject.NULL);
obj.put("contactList", JsonUtil.contactListToJson(user.getContactList()));
obj.put("channelList", JsonUtil.channelListToJson(user.getChannelList()));
obj.put("groupList", JsonUtil.groupListToJson(user.getGroupList()));
obj.put("unreadMessages", JsonUtil.messageListToJson(user.getUnreadMessages()));
return obj;
}
public static JSONArray messageListToJson(List<Message> messages) {
JSONArray array = new JSONArray();
for (Message message : messages) {
JSONObject obj = new JSONObject();
obj.put("message_id", message.getMessage_id().toString());
obj.put("sender_id", message.getSender_id() != null ? message.getSender_id().toString() : JSONObject.NULL);
obj.put("receiver_type", message.getReceiver_type());
obj.put("receiver_id", message.getReceiver_id().toString());
obj.put("content", message.getContent());
obj.put("message_type", message.getMessage_type());
obj.put("send_at", message.getSend_at().toString());
obj.put("status", message.getStatus());
obj.put("reply_to_id", message.getReply_to_id() != null ? message.getReply_to_id().toString() : JSONObject.NULL);
obj.put("is_edited", message.isIs_edited());
obj.put("original_message_id", message.getOriginal_message_id() != null ? message.getOriginal_message_id().toString() : JSONObject.NULL);
obj.put("forwarded_by", message.getForwarded_by() != null ? message.getForwarded_by().toString() : JSONObject.NULL);
obj.put("forwarded_from", message.getForwarded_from() != null ? message.getForwarded_from().toString() : JSONObject.NULL);
array.put(obj);
}
return array;
}
public static JSONArray groupListToJson(List<Group> groups) {
JSONArray array = new JSONArray();
for (Group group : groups) {
JSONObject obj = new JSONObject();
obj.put("internal_uuid", group.getInternal_uuid().toString());
obj.put("group_id", group.getGroup_id() != null ?group.getGroup_id().toString() :JSONObject.NULL);
obj.put("group_name", group.getGroup_name());
obj.put("creator_id", group.getCreator_id().toString());
obj.put("image_url",group.getImage_url()!= null ?group.getImage_url().toString() : JSONObject.NULL );
obj.put("description", group.getDescription() != null ?group.getDescription().toString() : JSONObject.NULL );
obj.put("created_at", group.getCreated_at().toString());
obj.put("members", JsonUtil.groupMemberListToJson(group.getMembers()));
array.put(obj);
}
return array;
}
public static JSONArray channelListToJson(List<Channel> channels) {
JSONArray array = new JSONArray();
for (Channel channel : channels) {
JSONObject obj = new JSONObject();
obj.put("internal_uuid",channel.getInternal_uuid().toString());
obj.put("channel_id", channel.getChannel_id() != null ?channel.getChannel_id().toString() :JSONObject.NULL);
obj.put("channel_name", channel.getChannel_name());
obj.put("creator_id", channel.getCreator_id().toString());
obj.put("image_url",channel.getImage_url()!= null ?channel.getImage_url().toString() : JSONObject.NULL );
obj.put("description",channel.getDescription() != null ?channel.getDescription().toString() : JSONObject.NULL );
obj.put("created_at",channel.getCreated_at().toString());
obj.put("members", JsonUtil.channelSubscribeToJson(channel.getMembers()));
array.put(obj);
}
return array;
}
public static JSONArray groupMemberListToJson(List<GroupMember> members) {
JSONArray array = new JSONArray();
if (members == null) {
return array;
}
for (GroupMember m : members) {
JSONObject obj = new JSONObject();
obj.put("group_id", m.getGroup_id().toString());
obj.put("user_id", m.getUser_id().toString());
obj.put("joined_at", m.getJoin_at().toString());
obj.put("role", m.getRole());
array.put(obj);
}
return array;
}
public static JSONArray channelSubscribeToJson(List<ChannelSubscribe> subscribes) {
JSONArray array = new JSONArray();
if (subscribes == null) {
return array;
}
for (ChannelSubscribe s : subscribes) {
JSONObject obj = new JSONObject();
obj.put("channel_id", s.getChannel_id().toString());
obj.put("user_id", s.getUser_id().toString());
obj.put("Subscribed_at", s.getJoin_at().toString());
array.put(obj);
}
return array;
}
public static JSONArray chatListToJson(List<ChatEntry> chatList) {
JSONArray jsonArray = new JSONArray();
for (ChatEntry entry : chatList) {
JSONObject obj = new JSONObject();
obj.put("internal_id", entry.getId().toString());
obj.put("id", entry.getDisplayId());
obj.put("name", entry.getName());
obj.put("image_url", entry.getImageUrl());
obj.put("type", entry.getType());
obj.put("last_message_time", entry.getLastMessageTime() == null ? JSONObject.NULL : entry.getLastMessageTime().toString());
obj.put("is_owner", entry.isOwner());
obj.put("is_admin", entry.isAdmin());
obj.put("is_saved_messages", entry.isSavedMessages());
obj.put("unread_count", entry.getUnreadCount());
obj.put("last_message_preview", entry.getLastMessagePreview());
obj.put("last_message_type", entry.getLastMessageType());
obj.put("last_message_sender_id",
entry.getLastMessageSenderId() != null ? entry.getLastMessageSenderId().toString() : JSONObject.NULL);
jsonArray.put(obj);
}
return jsonArray;
}
public static JSONObject chatToJson(ChatEntry chat) {
JSONObject obj = new JSONObject();
obj.put("id", chat.getId());
obj.put("name", chat.getName());
obj.put("image_url", chat.getImageUrl() != null ? chat.getImageUrl() : JSONObject.NULL);
obj.put("type", chat.getType());
obj.put("last_message_time", chat.getLastMessageTime() != null ? chat.getLastMessageTime().toString() : JSONObject.NULL);
return obj;
}
}
@@ -0,0 +1,32 @@
package org.to.telegramfinalproject.Models;
import java.util.List;
public class LoginBootstrapData {
private User user;
private List<Contact> contacts;
private List<Group> groups;
private List<Channel> channels;
private List<Message> unreadMessages;
public LoginBootstrapData(User user, List<Contact> contacts, List<Group> groups,
List<Channel> channels, List<Message> unreadMessages) {
this.user = user;
this.contacts = contacts;
this.groups = groups;
this.channels = channels;
this.unreadMessages = unreadMessages;
}
public void setUnreadMessages(List<Message> unreadMessages){this.unreadMessages = unreadMessages;}
public void setContacts(List<Contact> contacts){this.contacts =contacts;}
public void setGroups(List<Group> groups){this.groups = groups;}
public void setChannels(List<Channel> channels){this.channels = channels;}
public void setUser(User user){this.user = user;}
public List<Message> getUnreadMessages(){return unreadMessages;}
public List<Group> getGroups(){return groups;}
public List<Contact> getContacts(){return contacts;}
public List<Channel> getChannels(){return channels;}
public User getUser(User user){return user;}
}
@@ -0,0 +1,24 @@
package org.to.telegramfinalproject.Models;
import java.util.UUID;
public class MediaRow {
public UUID messageId;
public String storagePath;
public String fileName;
public String mimeType;
public Long fileSize;
public String receiverType;
public UUID receiverId;
public UUID senderId;
public java.util.UUID attachmentId;
public java.util.UUID mediaKey;
public String fileType; // IMAGE/AUDIO/...
public Integer width;
public Integer height;
public Integer durationSeconds; //for audio only
public String thumbnailUrl;
public String fileUrl; //display link
public String chatType;
public UUID chatId;
}
@@ -0,0 +1,211 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public class Message {
private UUID message_id;
private UUID sender_id;
private String receiver_type;
private UUID receiver_id;
private String content;
private String message_type;
private LocalDateTime send_at;
private String status;
private UUID reply_to_id;
private boolean is_edited;
private UUID original_message_id;
private UUID forwarded_by;
private UUID forwarded_from;
private List<FileAttachment> attachments;
private boolean is_deleted_globally;
private LocalDateTime edited_at;
private transient String sender_name;
private transient String receiver_name;
// ✅ Full Constructor
public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content,
String message_type, LocalDateTime send_at, String status,
UUID reply_to_id, boolean is_edited, boolean is_deleted_globally,
UUID original_message_id, UUID forwarded_by, UUID forwarded_from) {
this.message_id = message_id;
this.sender_id = sender_id;
this.receiver_type = receiver_type;
this.receiver_id = receiver_id;
this.content = content;
this.message_type = message_type;
this.send_at = send_at;
this.status = status;
this.reply_to_id = reply_to_id;
this.is_edited = is_edited;
this.is_deleted_globally = is_deleted_globally;
this.original_message_id = original_message_id;
this.forwarded_by = forwarded_by;
this.forwarded_from = forwarded_from;
}
public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content,
String message_type, LocalDateTime send_at, String status,
UUID reply_to_id, boolean is_edited, UUID original_message_id,
UUID forwarded_by, UUID forwarded_from,boolean is_deleted_globally, LocalDateTime edited_at) {
this.message_id = message_id;
this.sender_id = sender_id;
this.receiver_type = receiver_type;
this.receiver_id = receiver_id;
this.content = content;
this.message_type = message_type;
this.send_at = send_at;
this.status = status;
this.reply_to_id = reply_to_id;
this.is_edited = is_edited;
this.original_message_id = original_message_id;
this.forwarded_by = forwarded_by;
this.forwarded_from = forwarded_from;
this.is_deleted_globally = is_deleted_globally;
this.edited_at = edited_at;
}
// ✅ Short Constructors
//for normal messages
public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType,
String content, String messageType, LocalDateTime sendAt) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_id = receiverId;
this.receiver_type = receiverType;
this.content = content;
this.message_type = messageType;
this.send_at = sendAt;
}
//for reply messages
public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId,
String content, String messageType, LocalDateTime sendAt, String status,
UUID replyToId) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_type = receiverType;
this.receiver_id = receiverId;
this.content = content;
this.message_type = messageType;
this.send_at = sendAt;
this.status = status;
this.reply_to_id = replyToId;
this.is_edited = false;
this.is_deleted_globally = false;
}
public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId,
String content, String messageType, LocalDateTime sendAt, String status,
UUID replyToId, boolean isEdited, boolean isDeletedGlobally,
UUID originalMessageId, UUID forwardedBy, UUID forwardedFrom,
LocalDateTime editedAt) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_type = receiverType;
this.receiver_id = receiverId;
this.content = content;
this.message_type = messageType;
this.send_at = sendAt;
this.status = status;
this.reply_to_id = replyToId;
this.is_edited = isEdited;
this.is_deleted_globally = isDeletedGlobally;
this.original_message_id = originalMessageId;
this.forwarded_by = forwardedBy;
this.forwarded_from = forwardedFrom;
this.edited_at = editedAt;
}
public Message() {
}
// ✅ Getters & Setters
public UUID getMessage_id() { return message_id; }
public void setMessage_id(UUID messageId) { this.message_id = messageId; }
public UUID getSender_id() { return sender_id; }
public void setSender_id(UUID sender_id) { this.sender_id = sender_id; }
public String getReceiver_type() { return receiver_type; }
public void setReceiver_type(String receiver_type) { this.receiver_type = receiver_type; }
public UUID getReceiver_id() { return receiver_id; }
public void setReceiver_id(UUID receiver_id) { this.receiver_id = receiver_id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getMessage_type() { return message_type; }
public void setMessage_type(String message_type) { this.message_type = message_type; }
public LocalDateTime getSend_at() { return send_at; }
public void setSend_at(LocalDateTime send_at) { this.send_at = send_at; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public UUID getReply_to_id() { return reply_to_id; }
public void setReply_to_id(UUID reply_to_id) { this.reply_to_id = reply_to_id; }
public boolean isIs_edited() { return is_edited; }
public void setIs_edited(boolean is_edited) { this.is_edited = is_edited; }
public boolean isIs_deleted_globally() { return is_deleted_globally; }
public void setIs_deleted_globally(boolean is_deleted_globally) {
this.is_deleted_globally = is_deleted_globally;
}
public UUID getOriginal_message_id() { return original_message_id; }
public void setOriginal_message_id(UUID original_message_id) { this.original_message_id = original_message_id; }
public UUID getForwarded_by() { return forwarded_by; }
public void setForwarded_by(UUID forwarded_by) { this.forwarded_by = forwarded_by; }
public UUID getForwarded_from() { return forwarded_from; }
public void setForwarded_from(UUID forwarded_from) { this.forwarded_from = forwarded_from; }
public List<FileAttachment> getAttachments() { return attachments; }
public void setAttachments(List<FileAttachment> attachments) { this.attachments = attachments; }
public String getSender_name() {
return sender_name;
}
public void setSender_name(String sender_name) {
this.sender_name = sender_name;
}
public String getReceiver_name() {
return receiver_name;
}
public void setReceiver_name(String receiver_name) {
this.receiver_name = receiver_name;
}
public LocalDateTime getEdited_at() {
return edited_at;
}
public void setEdited_at(LocalDateTime edited_at) {
this.edited_at = edited_at;
}
public boolean getIs_deleted_globally() {
return is_deleted_globally;
}
public void receiver_id(UUID receiverId) {this.receiver_id = receiverId;
}
}
@@ -0,0 +1,58 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.UUID;
public class MessageReceipt {
private UUID messageId;
private UUID userId;
private LocalDateTime readAt;
public MessageReceipt(UUID messageId, UUID userId, LocalDateTime readAt) {
this.messageId = messageId;
this.userId = userId;
this.readAt = readAt;
}
// Overloaded constructor if readAt is not provided (use current timestamp)
public MessageReceipt(UUID messageId, UUID userId) {
this.messageId = messageId;
this.userId = userId;
this.readAt = LocalDateTime.now();
}
// Getters
public UUID getMessageId() {
return messageId;
}
public UUID getUserId() {
return userId;
}
public LocalDateTime getReadAt() {
return readAt;
}
// Setters
public void setMessageId(UUID messageId) {
this.messageId = messageId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public void setReadAt(LocalDateTime readAt) {
this.readAt = readAt;
}
@Override
public String toString() {
return "MessageReceipt{" +
"messageId=" + messageId +
", userId=" + userId +
", readAt=" + readAt +
'}';
}
}
@@ -0,0 +1,50 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.util.UUID;
public class PrivateChat {
private final UUID chat_id;
private boolean user1_deleted;
private boolean user2_deleted;
private UUID user1_id;
private UUID user2_id;
private LocalDateTime created_at;
public PrivateChat(UUID chat_id, UUID user1_id, UUID user2_id){
this.chat_id = chat_id;
this.user1_id =user1_id;
this.user2_id =user2_id;
this.created_at =created_at;
}
public PrivateChat(UUID chatId, UUID user1, UUID user2, boolean user1Deleted, boolean user2Deleted, LocalDateTime createdAt) {
this.chat_id = chatId;
this.user1_id = user1;
this.user2_id = user2;
this.user1_deleted = user1Deleted;
this.user2_deleted = user2Deleted;
this.created_at = createdAt;
}
public PrivateChat(UUID chatId, UUID user1, UUID user2, boolean user1Deleted, boolean user2Deleted) {
this.chat_id = chatId;
this.user1_id = user1;
this.user2_id = user2;
this.user1_deleted = user1Deleted;
this.user2_deleted = user2Deleted;
}
public void setUser1_id(UUID user1_id){this.user1_id =user1_id;}
public void setUser2_id(UUID user2_id){this.user2_id =user2_id;}
public void setCreated_at(LocalDateTime created_at){this.created_at = created_at;}
public UUID getUser1_id(){return this.user1_id;}
public UUID getChat_id(){return this.chat_id;}
public UUID getUser2_id(){return this.user2_id;}
public LocalDateTime getCreated_at(){return this.created_at;}
}
@@ -0,0 +1,39 @@
package org.to.telegramfinalproject.Models;
public class RequestModel {
private String action;
private String user_id;
private String username;
private String password;
private String profile_name;
public RequestModel(String action, String user_id, String username, String password, String profile_name) {
this.action = action;
this.user_id = user_id;
this.username = username;
this.password = password;
this.profile_name = profile_name;
}
public String getAction() {
return this.action;
}
public String getUser_id() {
return this.user_id;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public String getProfile_name() {
return this.profile_name;
}
}
@@ -0,0 +1,50 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
public class ResponseModel {
private String status;
private String message;
private JSONObject data;
private String requestId;
public ResponseModel(String status, String message) {
this.status = status;
this.message = message;
this.data = null;
}
public ResponseModel(String status, String message, JSONObject data) {
this.status = status;
this.message = message;
this.data = data;
}
public String getStatus() {
return this.status;
}
public String getMessage() {
return this.message;
}
public JSONObject getData() {return this.data;}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public JSONObject toJson() {
JSONObject json = new JSONObject();
json.put("status", this.status);
json.put("message", this.message);
json.put("data", this.data != null ? this.data : JSONObject.NULL);
if (this.requestId != null) {
json.put("request_id", this.requestId);
}
return json;
}
}
@@ -0,0 +1,24 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
public class SearchRequestModel {
private String action;
private String keyword;
private String user_id;
public SearchRequestModel(String action, String keyword, String user_id) {
this.action = action;
this.keyword = keyword;
this.user_id = user_id;
}
public JSONObject toJson() {
JSONObject json = new JSONObject();
json.put("action", action);
json.put("keyword", keyword);
json.put("user_id", user_id);
return json;
}
}
@@ -0,0 +1,59 @@
package org.to.telegramfinalproject.Models;
public class SearchResultModel {
private final String type;
private final String id; // ← UUID
private final String displayId; // ← user_id یا group_id برای نمایش
private String name;
private final String content;
private final String sender;
private final String time;
public SearchResultModel(String type, String id, String displayId,
String content, String sender, String time) {
this.type = type;
this.id = id;
this.displayId = displayId;
this.content = content;
this.sender = sender;
this.time = time;
}
// فقط در صورت نیاز برای user/group/channel
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public String getId() {
return id;
}
public String getDisplayId() {
return displayId;
}
public String getName() {
return name;
}
public String getContent() {
return content;
}
public String getSender() {
return sender;
}
public String getTime() {
return time;
}
@Override
public String toString() {
return "[" + type.toUpperCase() + "] " + name + " (ID: " + displayId + ")";
}
}
@@ -0,0 +1,123 @@
package org.to.telegramfinalproject.Models;
import org.json.JSONObject;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class User {
private String user_id;
private UUID internal_uuid;
private String username;
private String password;
private String profile_name;
private String bio;
private String image_url;
private String status;
private LocalDateTime last_seen;
private List<Contact> contactList;
private List<Group> groupList;
private List<Channel> channelList;
private List<Message> unreadMessages;
private List<ChatEntry> chatList;
public User(String user_id, UUID internal_uuid, String username, String password, String profile_name, String bio, String image_url) {
this.user_id = user_id;
this.internal_uuid = internal_uuid;
this.username = username;
this.password = password;
this.profile_name = profile_name;
this.bio = bio;
this.image_url = image_url;
}
public void setUser_id(String user_id) {
this.user_id = user_id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setProfile_name(String profile_name) {
this.profile_name = profile_name;
}
public void setBio(String bio) {
this.bio = bio;
}
public void setImage_url(String image_url) {
this.image_url = image_url;
}
public void setStatus(String status) {
this.status = status;
}
public void setLast_seen(LocalDateTime last_seen) {
this.last_seen = last_seen;
}
public void setContactList(List<Contact> contactList){this.contactList = contactList;}
public void setChannelList(List<Channel> channelList){this.channelList = channelList;}
public void setGroupList(List<Group> groupList){this.groupList = groupList;}
public void setUnreadMessages(List<Message> unreadMessages){this.unreadMessages = unreadMessages;}
public void setChatList(List<ChatEntry> chatList){this.chatList = chatList;}
public UUID getInternal_uuid() {
return this.internal_uuid;
}
public String getUser_id() {
return this.user_id;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public String getProfile_name() {
return this.profile_name;
}
public String getBio() {
return this.bio;
}
public String getImage_url() {
return this.image_url;
}
public String getStatus() {
return this.status;
}
public LocalDateTime getLast_seen() {
return this.last_seen;
}
public List<Contact> getContactList(){return this.contactList;}
public List<Channel> getChannelList(){return this.channelList;}
public List<Group> getGroupList(){return this.groupList;}
public List<Message> getUnreadMessages(){return this.unreadMessages;}
public List<ChatEntry> getChatList(){return this.chatList;}
}
@@ -0,0 +1,30 @@
package org.to.telegramfinalproject.Security;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class PasswordHashing {
public PasswordHashing() {
}
public static String hash(String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashedBytes = md.digest(password.getBytes());
StringBuilder sb = new StringBuilder();
for(byte b : hashedBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Hashing algorithm not found!", e);
}
}
public static boolean verify(String password, String hashedPasswordFromDB) {
return hash(password).equals(hashedPasswordFromDB);
}
}
@@ -0,0 +1,59 @@
package org.to.telegramfinalproject.Server;
import java.util.UUID;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Models.User;
import org.to.telegramfinalproject.Security.PasswordHashing;
public class AuthService {
private final userDatabase userDb = new userDatabase();
public AuthService() {
}
public boolean register(String user_id, String username, String password, String profile_name) {
if (!this.userDb.existsByUsername(username) && !this.userDb.existsByUserId(user_id)) {
String passwordRegex = "\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b";
if (!password.matches(passwordRegex)) {
System.out.println("Password doesn't Valid(At list one capital and one special char(!@#$%^&*), minimum 8 char ");
return false;
} else {
UUID uuid = UUID.randomUUID();
password = PasswordHashing.hash(password);
User user = new User(user_id, uuid, username, password, profile_name, "", "");
return this.userDb.save(user);
}
} else {
System.out.println("Username/ user id is already taken");
return false;
}
}
public User login(String username, String password) {
User user = this.userDb.findByUsername(username);
if (user == null) {
System.out.println("User not found.");
return null;
} else if (!PasswordHashing.verify(password, user.getPassword())) {
System.out.println("Incorrect password");
return null;
} else {
return user;
}
}
public boolean loginCheck(String username , String password){
User user = userDb.findByUsername(username);
String pass = user.getPassword();
password = PasswordHashing.hash(password);
String Username = user.getUsername();
boolean login = false;
if(user!= null && Username.equals(username)&& pass.equals(password)){
login = true;
}
return login;
}
}
@@ -0,0 +1,23 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Models.Channel;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import java.time.LocalDateTime;
import java.util.UUID;
public class ChannelService {
public static boolean createChannel(String channelId, String channelName, UUID creatorUUID, String imageUrl,String description) {
UUID internalUUID = UUID.randomUUID();
LocalDateTime now = LocalDateTime.now();
boolean inserted = ChannelDatabase.insertChannel(internalUUID, channelId, channelName, creatorUUID, imageUrl, description,now);
if (inserted) {
ChannelDatabase.addSubscriber(internalUUID, creatorUUID,"owner");
return true;
}
return false;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
package org.to.telegramfinalproject.Server;
import java.util.UUID;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Database.ContactDatabase;
public class ContactService {
public static boolean addContact(UUID userId, UUID contactId) {
if (userId.equals(contactId)) return false;
if (userDatabase.findByInternalUUID(contactId) == null) return false;
if (userDatabase.findByInternalUUID(userId) == null) return false;
if (ContactDatabase.existsContact(userId, contactId)) return false;
return ContactDatabase.addContact(userId, contactId);
}
}
@@ -0,0 +1,26 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Models.Group;
import org.to.telegramfinalproject.Database.GroupDatabase;
import java.time.LocalDateTime;
import java.util.UUID;
public class GroupService {
public static boolean createGroup(String groupId, String groupName, UUID creatorUUID, String imageUrl) {
UUID internalUUID = UUID.randomUUID();
LocalDateTime now = LocalDateTime.now();
boolean inserted = GroupDatabase.insertGroup(internalUUID, groupId, groupName, creatorUUID, imageUrl, now);
if (inserted) {
GroupDatabase.addMember(internalUUID, creatorUUID, "owner");
return true;
}
return false;
}
}
@@ -0,0 +1,33 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Database.userDatabase;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MainServer {
private static final int PORT = 8080;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Server started on port " + PORT);
userDatabase.setAllUsersOffline();
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("New client connected: " + clientSocket.getInetAddress());
ClientHandler handler = new ClientHandler(clientSocket);
new Thread(handler).start();
}
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,22 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Database.ContactDatabase;
import org.to.telegramfinalproject.Models.ResponseModel;
import java.util.UUID;
public class PrivateChatService {
public static ResponseModel deletePrivateChat(UUID currentUserId, UUID targetUserId, boolean both) {
boolean success = both ?
ContactDatabase.deleteChatBoth(currentUserId, targetUserId) :
ContactDatabase.deleteChatOneSide(currentUserId, targetUserId);
if (success) {
return new ResponseModel("success", "Chat deleted successfully");
} else {
return new ResponseModel("error", "Chat not found or failed to delete");
}
}
}
@@ -0,0 +1,504 @@
package org.to.telegramfinalproject.Server;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.*;
import org.to.telegramfinalproject.Models.Message;
import org.to.telegramfinalproject.Models.User;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class RealTimeEventDispatcher {
public static void sendToUser(UUID userId, JSONObject data) {
Socket socket = SessionManager.getUserSocket(userId);
if (socket != null && !socket.isClosed()) {
try {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
System.out.println("🚀 Sending to user: " + userId + "" + data);
out.println(data.toString());
} catch (IOException e) {
System.err.println("❌ Error sending to user: " + e.getMessage());
}
}
else {
System.out.println("⚠️ User " + userId + " is offline. Skipping real-time send.");
}
}
public static void broadcastToUsers(List<UUID> userIds, JSONObject data) {
for (UUID userId : userIds) {
sendToUser(userId, data);
}
}
public static JSONObject buildEvent(String action, JSONObject payload) {
JSONObject json = new JSONObject();
json.put("action", action);
json.put("data", payload);
return json;
}
public static void notifyNewMessage(Message msg, User sender) {
JSONObject data = new JSONObject();
data.put("sender", sender.getUser_id());
data.put("receiver_type", msg.getReceiver_type());
data.put("receiver_id", msg.getReceiver_id());
data.put("content", msg.getContent());
data.put("time", msg.getSend_at().toString());
JSONObject event = buildEvent("new_message", data);
switch (msg.getReceiver_type()) {
case "private" -> RealTimeEventDispatcher.sendToUser(msg.getReceiver_id(), event);
case "group" -> {
List<UUID> memberIds = GroupDatabase.getMemberUUIDs(msg.getReceiver_id());
memberIds.remove(sender.getInternal_uuid());
RealTimeEventDispatcher.broadcastToUsers(memberIds, event);
}
case "channel" -> {
List<UUID> subscriberIds = ChannelDatabase.getSubscriberUUIDs(msg.getReceiver_id());
subscriberIds.remove(sender.getInternal_uuid());
subscriberIds.remove(sender.getInternal_uuid());
RealTimeEventDispatcher.broadcastToUsers(subscriberIds, event);
}
}
}
public static void notifyMessageEdited(UUID chatId, UUID messageId, String newContent, LocalDateTime editedAt, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("new_content", newContent);
data.put("edited_at", editedAt.toString());
JSONObject event = new JSONObject();
event.put("action", "message_edited");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void sendNewMessage(Message message, List<UUID> receivers, String kind, JSONObject meta) {
JSONObject payload = new JSONObject();
payload.put("action", "new_message");
JSONObject data = new JSONObject();
data.put("id", message.getMessage_id().toString());
data.put("chat_id", message.getReceiver_id().toString());
data.put("sender_id", message.getSender_id().toString());
data.put("receiver_id", message.getReceiver_id().toString());
data.put("receiver_type", message.getReceiver_type());
data.put("content", message.getContent());
data.put("message_type", message.getMessage_type());
data.put("send_at", message.getSend_at().toString());
User sender = userDatabase.findByInternalUUID(message.getSender_id());
if (sender != null) data.put("sender_name", sender.getProfile_name());
data.put("kind", kind == null ? "plain" : kind); // plain|reply|forward
if (meta != null) data.put("meta", meta); // reply_to {...} | forwarded_from {...}
payload.put("data", data);
for (UUID userId : receivers) sendToUser(userId, payload);
}
public static void sendNewMessageFiltered(Message m, List<UUID> receivers, UUID senderId, String kind, JSONObject meta) {
JSONObject payload = new JSONObject()
.put("action", "new_message")
.put("data", new JSONObject()
.put("id", m.getMessage_id().toString())
.put("message_id", m.getMessage_id().toString())
.put("sender_id", m.getSender_id().toString())
.put("sender_name", userDatabase.findByInternalUUID(m.getSender_id()).getProfile_name())
.put("receiver_id", m.getReceiver_id().toString())
.put("receiver_type", m.getReceiver_type())
.put("content", m.getContent())
.put("message_type", m.getMessage_type())
.put("send_at", m.getSend_at().toString())
.put("kind", kind)
.put("meta", meta != null ? meta : JSONObject.NULL)
);
for (UUID uid : receivers) {
//If receiver blocked
if ("private".equalsIgnoreCase(m.getReceiver_type()) && ContactDatabase.isBlocked(uid, senderId)) continue;
sendToUser(uid, payload);
}
}
public static void notifyMessageDeletedGlobal(UUID chatId, UUID messageId, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
JSONObject event = new JSONObject();
event.put("action", "message_deleted_global");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyReactionAdded(UUID chatId, UUID messageId, String emoji,
int totalForEmoji, JSONObject countsAll, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("emoji", emoji);
data.put("counts", countsAll); // {"❤️":3,"👍":1,...}
data.put("count_for_emoji", totalForEmoji);
JSONObject event = new JSONObject();
event.put("action", "message_reacted");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyReactionRemoved(UUID chatId, UUID messageId, String emoji,
int totalForEmoji, JSONObject countsAll, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("emoji", emoji);
data.put("counts", countsAll);
data.put("count_for_emoji", totalForEmoji);
JSONObject event = new JSONObject();
event.put("action", "message_unreacted");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyUserUpdated(UUID userId, String newProfileName, String newImageUrl, List<UUID> contactIds) {
JSONObject data = new JSONObject();
data.put("user_id", userId.toString());
data.put("new_name", newProfileName);
data.put("new_image_url", newImageUrl);
JSONObject event = new JSONObject();
event.put("action", "update_user");
event.put("data", data);
broadcastToUsers(contactIds, event);
}
public static void notifyChatDeleted(String type, UUID id, List<UUID> affectedUsers) {
JSONObject data = new JSONObject();
data.put("chat_type", type); // private, group, channel
data.put("chat_id", id.toString());
JSONObject event = new JSONObject();
event.put("action", "chat_deleted");
event.put("data", data);
broadcastToUsers(affectedUsers, event);
}
public static void notifyGroupOrChannelUpdated(String type, UUID id, String newName, String newImageUrl, List<UUID> affectedUsers) {
JSONObject data = new JSONObject();
data.put("chat_type", type); // "group" or "channel"
data.put("chat_id", id.toString());
data.put("new_name", newName);
data.put("new_image_url", newImageUrl);
JSONObject event = new JSONObject();
event.put("action", "update_group_or_channel");
event.put("data", data);
broadcastToUsers(affectedUsers, event);
}
public static void notifyMediaMessage(Message msg, User sender) {
JSONObject data = new JSONObject();
data.put("sender", sender.getUser_id());
data.put("receiver_type", msg.getReceiver_type());
data.put("receiver_id", msg.getReceiver_id());
data.put("file_type", msg.getMessage_type()); // IMAGE, FILE, VIDEO...
data.put("time", msg.getSend_at().toString());
JSONObject event = new JSONObject();
event.put("action", "new_media");
event.put("data", data);
switch (msg.getReceiver_type()) {
case "private" -> sendToUser(msg.getReceiver_id(), event);
case "group" -> {
List<UUID> members = GroupDatabase.getMemberUUIDs(msg.getReceiver_id());
members.remove(sender.getInternal_uuid());
broadcastToUsers(members, event);
}
case "channel" -> {
List<UUID> subs = ChannelDatabase.getSubscriberUUIDs(msg.getReceiver_id());
subs.remove(sender.getInternal_uuid());
broadcastToUsers(subs, event);
}
}
}
public static void notifyAddedToChat(String type, UUID chatId, String chatName, String imageUrl, UUID userId) {
JSONObject data = new JSONObject();
data.put("chat_type", type);
data.put("chat_id", chatId.toString());
data.put("chat_name", chatName);
data.put("image_url", imageUrl);
JSONObject event = new JSONObject();
event.put("action", type.equals("group") ? "added_to_group" : "added_to_channel");
event.put("data", data);
sendToUser(userId, event);
}
public static void notifyRemovedFromChat(String type, UUID chatId, UUID userId) {
JSONObject data = new JSONObject();
data.put("chat_type", type);
data.put("chat_id", chatId.toString());
JSONObject event = new JSONObject();
event.put("action", type.equals("group") ? "removed_from_group" : "removed_from_channel");
event.put("data", data);
sendToUser(userId, event);
}
public static void notifyMessageSeen(UUID messageId, UUID senderId) {
JSONObject data = new JSONObject();
data.put("message_id", messageId.toString());
data.put("seen_at", LocalDateTime.now().toString());
JSONObject event = new JSONObject();
event.put("action", "message_seen");
event.put("data", data);
sendToUser(senderId, event);
}
public static void notifyBlocked(UUID blockerId, UUID blockedUserId) {
JSONObject data = new JSONObject();
data.put("blocker_id", blockerId.toString());
JSONObject event = new JSONObject();
event.put("action", "blocked_by_user");
event.put("data", data);
sendToUser(blockedUserId, event);
}
public static void notifyUnblocked(UUID unblockerId, UUID unblockedUserId) {
JSONObject data = new JSONObject();
data.put("unblocker_id", unblockerId.toString());
JSONObject event = new JSONObject();
event.put("action", "unblocked_by_user");
event.put("data", data);
sendToUser(unblockedUserId, event);
}
public static void notifyUserStatusChanged(UUID userId, String status, List<UUID> contacts) {
JSONObject data = new JSONObject();
data.put("user_id", userId.toString());
data.put("status", status); // online | offline
data.put("time", LocalDateTime.now().toString());
JSONObject event = buildEvent("user_status_changed", data);
broadcastToUsers(contacts, event);
}
public static void sendGroupOrChannelUpdate(String type, UUID chatId, String name, String imageUrl, String description, List<UUID> affectedUsers) {
JSONObject data = new JSONObject();
data.put("chat_type", type);
data.put("chat_id", chatId.toString());
data.put("name", name);
data.put("image_url", imageUrl);
data.put("description", description != null ? description : "");
JSONObject event = new JSONObject();
event.put("action", "chat_updated");
event.put("data", data);
broadcastToUsers(affectedUsers, event);
}
public static void notifyBecameAdmin(String type, UUID chatId, String chatName, String imageUrl, UUID userId) {
JSONObject data = new JSONObject();
data.put("chat_type", type); // group or channel
data.put("chat_id", chatId.toString());
data.put("chat_name", chatName);
data.put("image_url", imageUrl);
JSONObject event = new JSONObject();
event.put("action", "became_admin");
event.put("data", data);
sendToUser(userId, event);
}
public static void notifyRemovedAdminFromChat(String type, UUID chatId, String chatName, String imageUrl, UUID userId) {
JSONObject data = new JSONObject();
data.put("chat_type", type);
data.put("chat_id", chatId.toString());
data.put("chat_name", chatName);
data.put("image_url", imageUrl);
JSONObject event = new JSONObject();
event.put("action", "removed_admin");
event.put("data", data);
sendToUser(userId, event);
}
public static void sendOwnershipTransferred(String type, UUID chatId, String chatName, List<UUID> affectedUsers) {
JSONObject data = new JSONObject();
data.put("chat_type", type); // "group" or "channel"
data.put("chat_id", chatId.toString());
data.put("chat_name", chatName);
JSONObject event = new JSONObject();
event.put("action", "ownership_transferred");
event.put("data", data);
broadcastToUsers(affectedUsers, event);
}
public static void sendNewMessage(Message message, List<UUID> receivers) {
JSONObject payload = new JSONObject();
payload.put("action", "new_message");
JSONObject data = new JSONObject();
data.put("id", message.getMessage_id().toString());
data.put("sender_id", message.getSender_id().toString());
data.put("receiver_id", message.getReceiver_id().toString());
data.put("receiver_type", message.getReceiver_type());
data.put("content", message.getContent());
data.put("send_at", message.getSend_at().toString());
User sender = userDatabase.findByInternalUUID(message.getSender_id());
if (sender != null) {
data.put("sender_name", sender.getProfile_name());
}
payload.put("data", data);
for (UUID userId : receivers) {
sendToUser(userId, payload);
}
for (UUID uid : receivers) {
//If receiver blocked
if ("private".equalsIgnoreCase(message.getReceiver_type()) && ContactDatabase.isBlocked(uid, message.getSender_id())) continue;
sendToUser(uid, payload);
}
}
public static void notifyChatUpdated(UUID chatId, String chatType, Message lastMsg) {
if (chatId == null || chatType == null) return;
final String type = chatType.toLowerCase(Locale.ROOT);
List<UUID> receivers;
switch (type) {
case "private":
receivers = PrivateChatDatabase.getMembers(chatId);
break;
case "group":
receivers = GroupDatabase.getMemberUUIDs(chatId);
break;
case "channel":
receivers = ChannelDatabase.getSubscriberUUIDs(chatId);
break;
default:
receivers = Collections.emptyList();
}
if (receivers == null || receivers.isEmpty()) return;
// 2) ساخت خلاصه آخرین پیام برای نمایش در لیست چت
String senderName = null;
if (lastMsg != null && lastMsg.getSender_id() != null) {
User u = userDatabase.findByInternalUUID(lastMsg.getSender_id());
if (u != null) senderName = u.getProfile_name();
}
String messageType = lastMsg != null && lastMsg.getMessage_type() != null
? lastMsg.getMessage_type().toLowerCase(Locale.ROOT) : "text";
// preview ساده: برای مدیا، برچسب کوتاه؛ برای متن، کوتاه‌سازی
String preview;
if (!"text".equals(messageType)) {
switch (messageType) {
case "image": preview = "[Photo]"; break;
case "video": preview = "[Video]"; break;
case "audio": preview = "[Audio]"; break;
case "file": preview = "[File]"; break;
default: preview = "[Media]";
}
} else {
String t = lastMsg != null ? nullToEmpty(lastMsg.getContent()) : "";
preview = t.length() > 80 ? t.substring(0, 80) + "" : t;
}
String sendAt = (lastMsg != null && lastMsg.getSend_at() != null)
? lastMsg.getSend_at().toString()
: java.time.OffsetDateTime.now().toString();
// 3) payload رویداد chat_updated
JSONObject payload = new JSONObject()
.put("action", "chat_updated")
.put("data", new JSONObject()
.put("chat_id", chatId.toString())
.put("chat_type", type)
.put("last_message", new JSONObject()
.put("id", lastMsg != null ? lastMsg.getMessage_id().toString() : JSONObject.NULL)
.put("sender_id", lastMsg != null ? lastMsg.getSender_id().toString() : JSONObject.NULL)
.put("sender_name", senderName != null ? senderName : JSONObject.NULL)
.put("message_type", messageType)
.put("preview", preview)
.put("send_at", sendAt)
)
.put("last_message_time", sendAt)
.put("update_reason", "new_message") // برای کلاینت مفید است
);
// 4) ارسال به همه اعضای چت
for (UUID uid : receivers) {
sendToUser(uid, payload);
}
}
private static String nullToEmpty(String s) { return s == null ? "" : s; }
}
@@ -0,0 +1,21 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import org.to.telegramfinalproject.Database.GroupDatabase;
import org.to.telegramfinalproject.Database.PrivateChatDatabase;
import java.util.List;
import java.util.UUID;
public class Receivers {
public static List<UUID> resolveFor(String type, UUID chatId, UUID exclude) {
List<UUID> ids = switch (type) {
case "private" -> PrivateChatDatabase.getMembers(chatId);
case "group" -> GroupDatabase.getMemberUUIDs(chatId);
case "channel" -> ChannelDatabase.getSubscriberUUIDs(chatId);
default -> List.of();
};
if (exclude != null) ids.remove(exclude);
return ids;
}
}
@@ -0,0 +1,43 @@
package org.to.telegramfinalproject.Server;
import java.net.Socket;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class SessionManager {
private static final Map<UUID, Socket> onlineUsers = new ConcurrentHashMap<>(); //catch UUID for find the user socket
public static void addUser(UUID userId, Socket socket) {
onlineUsers.put(userId, socket);
}
public static void removeUser(UUID userId) {
onlineUsers.remove(userId);
}
public static boolean isOnline(UUID userId) {
return onlineUsers.containsKey(userId);
}
public static Socket getUserSocket(UUID userId) {
return onlineUsers.get(userId);
}
public static UUID getUserIdBySocket(Socket socket) {
for (Map.Entry<UUID, Socket> entry : onlineUsers.entrySet()) {
if (entry.getValue().equals(socket)) {
return entry.getKey();
}
}
return null;
}
public static boolean contains(UUID userId) {
return onlineUsers.containsKey(userId);
}
}
@@ -0,0 +1,291 @@
package org.to.telegramfinalproject.Server;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.ContactDatabase;
import org.to.telegramfinalproject.Database.MessageDatabase;
import org.to.telegramfinalproject.Database.PrivateChatDatabase;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Models.ContactEntry;
import org.to.telegramfinalproject.Models.Message;
import org.to.telegramfinalproject.Models.ResponseModel;
import org.to.telegramfinalproject.Models.User;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class SidebarService {
private static userDatabase userDB = new userDatabase();
// Returns current user's profile data (name, bio, status, etc.)
public static JSONObject getUserProfile(UUID userUUID) {
User user = userDB.findByInternalUUID(userUUID);
if (user == null) {
return null;
}
JSONObject profile = new JSONObject();
profile.put("user_id", user.getUser_id());
profile.put("profile_name", user.getProfile_name());
profile.put("bio", user.getBio() != null ? user.getBio() : "");
profile.put("status", "ONLINE");
profile.put("profile_picture_url", user.getImage_url());
return profile;
}
// Changes the user's profile picture
public static ResponseModel updateProfilePicture(UUID userUUID, String newImageUrl) {
User user = userDB.findByInternalUUID(userUUID);
if (user == null) {
return new ResponseModel("error", "User not found.");
}
String currentUrl = user.getImage_url();
if (Objects.equals(currentUrl, newImageUrl)) {
return new ResponseModel("error", "No changes made. Profile picture is the same.");
}
user.setImage_url(newImageUrl); // Can be null
boolean success = userDB.updateByUUID(userUUID, user);
if (success) {
return new ResponseModel("success", "Profile picture updated successfully.");
} else {
user.setImage_url(currentUrl);
return new ResponseModel("error", "Failed to update profile picture.");
}
}
// Changes user's bio
public static ResponseModel updateBio(UUID userUUID, String newBio) {
if (newBio.length() > 70) {
return new ResponseModel("error", "Bio is too long.");
}
User user = userDB.findByInternalUUID(userUUID);
if (user == null) {
return new ResponseModel("error", "User not found.");
}
String currentBio = user.getBio();
if (Objects.equals(currentBio, newBio)) {
return new ResponseModel("error", "No changes made. Bio is the same.");
}
user.setBio(newBio);
boolean success = userDB.updateByUUID(userUUID, user);
if (success) {
return new ResponseModel("success", "Bio updated successfully.");
} else {
user.setBio(currentBio);
return new ResponseModel("error", "Failed to update bio.");
}
}
// Changes user-id
public static ResponseModel updateUserId(UUID userUUID, String newUserId) {
if (newUserId == null || newUserId.trim().isEmpty()) {
return new ResponseModel("error", "User ID cannot be empty.");
}
newUserId = newUserId.trim();
if (newUserId.contains(" ")) {
return new ResponseModel("error", "User ID cannot contain spaces.");
}
if (!newUserId.matches("^[a-zA-Z0-9_]+$")) {
return new ResponseModel("error", "User ID can only contain letters, digits, and underscores.");
}
if (userDB.findByUserId(newUserId) != null) {
return new ResponseModel("error", "This user ID is already taken.");
}
User user = userDB.findByInternalUUID(userUUID);
if (user == null) {
return new ResponseModel("error", "User not found.");
}
String currentUserId = user.getUser_id();
if (Objects.equals(currentUserId, newUserId)) {
return new ResponseModel("error", "No changes made. User ID is the same.");
}
user.setUser_id(newUserId);
boolean saved = userDB.updateByUUID(userUUID, user);
if (saved) {
return new ResponseModel("success", "User ID updated successfully.");
} else {
user.setUser_id(currentUserId);
return new ResponseModel("error", "Failed to update user ID due to server error.");
}
}
// Changes user's profile name
public static ResponseModel updateProfileName(UUID userUUID, String newProfileName) {
if (newProfileName == null || newProfileName.trim().isEmpty()) {
return new ResponseModel("error", "Invalid input."); // Invalid input (empty or just spaces)
}
// Fetch the user from database
User user = userDB.findByInternalUUID(userUUID);
if (user == null) {
return new ResponseModel("error", "User not found."); // User doesn't exist
}
String oldName = user.getProfile_name();
if (Objects.equals(oldName, newProfileName)) {
return new ResponseModel("error", "No changes made. Profile name is the same.");
}
// Only set the profile name *after* successful DB update
user.setProfile_name(newProfileName);
boolean saved = userDB.updateByUUID(userUUID, user);
if (saved) {
return new ResponseModel("success", "Profile name updated successfully.");
} else {
user.setProfile_name(oldName);
return new ResponseModel("error", "Failed to update profile name.");
}
}
// Search in user's contact list
public static ResponseModel handleSearchContacts(String userUUID, String searchTerm) {
if (searchTerm == null || searchTerm.trim().isEmpty()) {
return new ResponseModel("error", "Search term cannot be empty.");
}
List<ContactEntry> searchResult = ContactDatabase.searchContacts(UUID.fromString(userUUID), searchTerm);
if (searchResult.isEmpty()) {
return new ResponseModel("error", "No contacts found.");
}
JSONObject data = new JSONObject();
JSONArray contacts = new JSONArray();
for (ContactEntry entry : searchResult) {
contacts.put(new JSONObject()
.put("contact_id", entry.getContactId())
.put("user_id", entry.getUserId())
.put("contact_display_id", entry.getContact_displayId())
.put("profile_name", entry.getProfileName())
.put("image_url", entry.getImageUrl())
.put("is_blocked", entry.isBlocked())
.put("last_seen", entry.getLastSeenTime())
);
}
data.put("contacts", contacts);
if (searchResult.isEmpty()) {
return new ResponseModel("error", "No contacts found.");
}
return new ResponseModel("success", "Search contacts successfully.", data);
}
// Remove a contact in user's contact list
public static ResponseModel handleRemoveContact(UUID userUUID, UUID contactId) {
if (userUUID == null || contactId == null) {
return new ResponseModel("error", "Invalid input.");
}
boolean removed = ContactDatabase.removeContact(userUUID, contactId);
if (removed) {
return new ResponseModel("success", "Contact removed successfully.");
} else {
return new ResponseModel("error", "Contact not found.");
}
}
// public static ResponseModel handleGetSavedMessages(UUID userId) {
// try {
//
// UUID chatId = PrivateChatDatabase.getOrCreateSavedMessagesChat(userId);
// if (chatId == null) {
// return new ResponseModel("error", "Failed to create or find saved messages chat.");
// }
//
// List<Message> messages = MessageDatabase.privateChatHistory(chatId, userId);
//
// JSONArray messageArray = new JSONArray();
// if (!messages.isEmpty()) {
// for (Message msg : messages) {
// JSONObject msgJson = new JSONObject();
// msgJson.put("message_id", msg.getMessage_id().toString());
// msgJson.put("sender_id", msg.getSender_id().toString());
// msgJson.put("receiver_type", msg.getReceiver_type());
// msgJson.put("receiver_id", msg.getReceiver_id().toString());
// msgJson.put("content", msg.getContent());
// msgJson.put("message_type", msg.getMessage_type());
// msgJson.put("send_at", msg.getSend_at().toString()); // LocalDateTime
// msgJson.put("status", msg.getStatus());
// msgJson.put("reply_to_id", msg.getReply_to_id() != null ? msg.getReply_to_id().toString() : JSONObject.NULL);
// msgJson.put("is_edited", msg.isIs_edited());
// msgJson.put("original_message_id", msg.getOriginal_message_id() != null ? msg.getOriginal_message_id().toString() : JSONObject.NULL);
// msgJson.put("forwarded_by", msg.getForwarded_by() != null ? msg.getForwarded_by().toString() : JSONObject.NULL);
// msgJson.put("forwarded_from", msg.getForwarded_from() != null ? msg.getForwarded_from().toString() : JSONObject.NULL);
//
// messageArray.put(msgJson);
// }
// }
//
// JSONObject data = new JSONObject();
// data.put("chat_id", chatId.toString());
// data.put("messages", messageArray);
//
// return new ResponseModel("success", "Saved messages retrieved successfully", data);
//
// } catch (Exception e) {
// e.printStackTrace();
// return new ResponseModel("error", "Unexpected server error.");
// }
// }
// Save messages to DB
// public static ResponseModel handleSendMessage(JSONObject requestJson) {
// try {
// Message message = new Message(
// UUID.fromString(requestJson.getString("message_id")),
// UUID.fromString(requestJson.getString("sender_id")),
// requestJson.getString("receiver_type"),
// UUID.fromString(requestJson.getString("receiver_id")),
// requestJson.optString("content", null),
// requestJson.optString("message_type", "TEXT"),
// LocalDateTime.now(), // send_at
// requestJson.optString("status", "SEND"),
// requestJson.isNull("reply_to_id") ? null : UUID.fromString(requestJson.getString("reply_to_id")),
// requestJson.optBoolean("is_edited", false),
// requestJson.isNull("original_message_id") ? null : UUID.fromString(requestJson.getString("original_message_id")),
// requestJson.isNull("forwarded_by") ? null : UUID.fromString(requestJson.getString("forwarded_by")),
// requestJson.isNull("forwarded_from") ? null : UUID.fromString(requestJson.getString("forwarded_from")),
// requestJson.optBoolean("is_deleted_globally", false),
// requestJson.isNull("edited_at") ? null :
// LocalDateTime.ofInstant(
// Instant.ofEpochMilli(requestJson.getLong("edited_at")),
// ZoneId.systemDefault()
// )
// );
//
// MessageDatabase.insertSavedMessage(message);
// return new ResponseModel("success", "Message saved successfully.");
//
// } catch (Exception e) {
// e.printStackTrace();
// return new ResponseModel("error", "Unexpected server error.");
// }
// }
}
@@ -0,0 +1,57 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Database.userDatabase;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestServer {
private static final int SOCKET_PORT = 8000; // سرور سوکت
private static final int HTTP_PORT = 8080; // سرور آپلود
private static final String UPLOAD_BASE_DIR = "uploads"; // پوشه‌ی ذخیره فایل‌ها
public static void main(String[] args) {
// 1) استارت HTTP Upload در ترد جدا
Thread httpThread = new Thread(() -> {
try {
UploadHttp.start(HTTP_PORT, UPLOAD_BASE_DIR);
} catch (IOException e) {
System.err.println("Upload HTTP failed to start: " + e.getMessage());
e.printStackTrace();
}
}, "upload-http");
httpThread.setDaemon(true);
httpThread.start();
// 2) سرور سوکت با Thread Pool
ExecutorService pool = Executors.newCachedThreadPool();
try (ServerSocket serverSocket = new ServerSocket(SOCKET_PORT)) {
System.out.println("Socket server started on port " + SOCKET_PORT);
userDatabase.setAllUsersOffline();
// 3) Shutdown Hook برای خاموشی تمیز
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\nShutting down...");
try { serverSocket.close(); } catch (IOException ignore) {}
pool.shutdownNow();
userDatabase.setAllUsersOffline();
System.out.println("Goodbye.");
}));
// 4) حلقه پذیرش اتصال‌ها
while (!serverSocket.isClosed()) {
Socket clientSocket = serverSocket.accept();
clientSocket.setTcpNoDelay(true);
System.out.println("New client connected: " + clientSocket.getInetAddress());
pool.submit(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("Socket server error: " + e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,183 @@
package org.to.telegramfinalproject.Server;
import static spark.Spark.*;
import javax.imageio.ImageIO;
import javax.servlet.MultipartConfigElement;
import javax.servlet.http.Part;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDate;
import javax.sound.sampled.*; // برای WAV
import org.json.JSONObject;
import com.mpatric.mp3agic.Mp3File;
public class UploadHttp {
public static void start(int httpPort, String baseDir) throws IOException {
port(httpPort);
Path basePath = Paths.get(baseDir).toAbsolutePath().normalize();
Files.createDirectories(basePath);
staticFiles.externalLocation(basePath.toString());
post("/upload", (req, res) -> {
res.type("application/json");
try {
long MAX_FILE = 25L * 1024 * 1024; // 25MB
req.attribute("org.eclipse.jetty.multipartConfig",
new MultipartConfigElement("/tmp", MAX_FILE, MAX_FILE, 0));
Part filePart = req.raw().getPart("file");
if (filePart == null || filePart.getSize() == 0) {
res.status(400);
return jsonError("empty file");
}
if (filePart.getSize() > MAX_FILE) {
res.status(413);
return jsonError("file too large");
}
String mime = filePart.getContentType();
if (mime == null) {
res.status(415);
return jsonError("unknown mime");
}
String original = filePart.getSubmittedFileName();
String ext = guessExt(original, mime);
String day = LocalDate.now().toString();
String typeDir = subdirFor(mime); // images/audios/files
String subdir = typeDir + "/" + day;
String name = java.util.UUID.randomUUID() + ext;
Path dir = basePath.resolve(subdir).normalize();
Files.createDirectories(dir);
Path target = dir.resolve(name).normalize();
try (InputStream in = filePart.getInputStream()) {
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
} finally {
filePart.delete();
}
String fileUrl = "/" + subdir.replace('\\', '/') + "/" + name;
String fileType = mapToFileType(mime);
//Meta deta only for audio and image
Integer width = null, height = null, durationSeconds = null;
String thumbnailUrl = null;
if ("IMAGE".equals(fileType) || "GIF".equals(fileType)) {
int[] wh = imageSize(target);
if (wh != null) { width = wh[0]; height = wh[1]; }
} else if ("AUDIO".equals(fileType)) {
durationSeconds = audioDurationSeconds(target, mime, ext);
}
res.status(200);
return new JSONObject()
.put("file_url", fileUrl)
.put("file_type", fileType)
.put("file_name", original == null ? "" : safeName(original))
.put("file_size", Files.size(target))
.put("mime_type", mime)
.put("width", width == null ? JSONObject.NULL : width)
.put("height", height == null ? JSONObject.NULL : height)
.put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds)
.put("thumbnail_url", JSONObject.NULL)
.toString();
} catch (Exception e) {
e.printStackTrace();
res.status(500);
return jsonError("internal error");
}
});
init();
awaitInitialization();
System.out.println("Upload HTTP server on http://localhost:" + httpPort + " baseDir=" + basePath);
}
// ---------- Helpers ----------
private static String jsonError(String msg) {
return new JSONObject().put("error", msg).toString();
}
private static String subdirFor(String mime) {
String m = mime.toLowerCase();
if (m.startsWith("image/")) return "images";
if (m.startsWith("audio/")) return "audios";
return "files";
}
private static String mapToFileType(String mime) {
String m = mime.toLowerCase();
if (m.startsWith("image/")) {
if (m.contains("gif")) return "GIF";
return "IMAGE";
}
if (m.startsWith("audio/")) return "AUDIO";
return "FILE";
}
private static String guessExt(String original, String mime) {
if (original != null && original.contains(".")) {
String ext = original.substring(original.lastIndexOf('.'));
if (ext.length() <= 10) return ext;
}
if ("image/png".equalsIgnoreCase(mime)) return ".png";
if ("image/jpeg".equalsIgnoreCase(mime)) return ".jpg";
if ("image/gif".equalsIgnoreCase(mime)) return ".gif";
if ("audio/mpeg".equalsIgnoreCase(mime)) return ".mp3";
if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime)) return ".wav";
if ("application/pdf".equalsIgnoreCase(mime)) return ".pdf";
return "";
}
private static String safeName(String name) {
return name.replace("\"", "").replace("\n", "").replace("\r", "");
}
private static int[] imageSize(Path file) {
try {
BufferedImage bi = ImageIO.read(file.toFile());
if (bi != null) return new int[]{bi.getWidth(), bi.getHeight()};
} catch (Exception ignore) {}
return null;
}
//only audio
private static Integer audioDurationSeconds(Path file, String mime, String ext) {
try {
if ("audio/mpeg".equalsIgnoreCase(mime) || ".mp3".equalsIgnoreCase(ext)) {
Mp3File mp3 = new Mp3File(file.toFile());
return (int) mp3.getLengthInSeconds();
}
// WAV با javax.sound.sampled
if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime) || ".wav".equalsIgnoreCase(ext)) {
try (AudioInputStream ais = AudioSystem.getAudioInputStream(file.toFile())) {
AudioFormat format = ais.getFormat();
long frames = ais.getFrameLength();
if (frames > 0 && format.getFrameRate() > 0) {
double seconds = frames / format.getFrameRate();
return (int)Math.round(seconds);
}
}
}
} catch (UnsupportedAudioFileException | IOException ignore) {
// فرمت صوتی پشتیبانی نشده برای AudioSystem
} catch (Exception ignore) {
// mp3agic یا سایر استثناها
}
return null;
}
}
@@ -0,0 +1,125 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.UUID;
public class AddAdminsController {
@FXML private VBox addAdminsCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private ScrollPane membersScroll;
@FXML private VBox membersList;
private String groupId;
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(addAdminsCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(addAdminsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addAdminsCard.getParent()));
// Smooth scroll feel
membersScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
membersScroll.setPannable(true);
membersScroll.setFitToWidth(true);
membersScroll.setFitToHeight(false);
membersScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
membersScroll.setVvalue(membersScroll.getVvalue() - deltaY);
});
}
public void setGroupData(String groupId, JSONArray members) {
this.groupId = groupId;
membersList.getChildren().clear();
for (int i = 0; i < members.length(); i++) {
JSONObject m = members.getJSONObject(i);
String role = m.optString("role", "member");
if ("owner".equalsIgnoreCase(role) || "admin".equalsIgnoreCase(role)) {
continue; // skip existing admins/owner
}
addMemberRow(m);
}
}
private void addMemberRow(JSONObject m) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = m.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(m.optBoolean("is_online", false) ? "online" : "last seen recently");
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
Button promoteBtn = new Button("Promote");
promoteBtn.getStyleClass().add("link-btn");
promoteBtn.setOnAction(e -> promoteToAdmin(m.optString("internal_uuid"), row));
row.getChildren().addAll(avatar, details, spacer, promoteBtn);
membersList.getChildren().add(row);
}
private void promoteToAdmin(String internalUuid, HBox row) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/set_group_admin_permissions.fxml"));
Node overlay = loader.load();
SetGroupAdminPermissionsController controller = loader.getController();
controller.setTarget(UUID.fromString(groupId), UUID.fromString(internalUuid), false);
// Pass a callback to remove row if promotion succeeds
controller.setOnSuccess(() -> membersList.getChildren().remove(row));
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load permissions scene.", Alert.AlertType.ERROR
);
}
}
}
@@ -0,0 +1,313 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
public class AddMembersController {
@FXML private VBox contactsList;
@FXML private VBox addMembersCard;
@FXML private TextField searchField;
@FXML private Pane overlayBackground;
@FXML private ScrollPane contactsScroll;
@FXML private Button searchIcon;
@FXML private Label memberCountLabel;
@FXML private FlowPane selectedMembersPane;
@FXML private Button cancelButton;
@FXML private Button createButton;
private UUID groupInternalId;
private String groupName;
private String groupDisplayId;
private File groupImageFile;
public enum Mode {
CREATE, // from New Group
ADD // from Group Info
}
private Mode mode = Mode.CREATE; // default
private final Set<Contact> selectedContacts = new HashSet<>();
private final List<Contact> allContacts = new ArrayList<>();
@FXML
public void initialize() {
// Smooth scroll feel
contactsScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
contactsScroll.setPannable(true);
contactsScroll.setFitToWidth(true);
contactsScroll.setFitToHeight(false);
contactsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
});
Platform.runLater(() -> {
if (addMembersCard.getScene() != null) {
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
createButton.setOnAction(e -> onAddMembers());
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
Platform.runLater(() -> searchField.requestFocus());
loadContactsFromSession();
updateMemberCount();
renderContacts(allContacts);
}
// Called when creating a new group from sidebar
public void setGroupInfo(UUID internalId, String groupName, String displayId, File groupImageFile) {
this.groupInternalId = internalId;
this.groupName = groupName;
this.groupDisplayId = displayId;
this.groupImageFile = groupImageFile;
this.mode = Mode.CREATE;
updateActionButtonText();
}
// Called when adding to an existing group
public void setGroupForAdd(UUID internalId, String groupName) {
this.groupInternalId = internalId;
this.groupName = groupName;
this.mode = Mode.ADD;
updateActionButtonText();
}
private void updateActionButtonText() {
if (createButton != null) {
createButton.setText(mode == Mode.CREATE ? "Create" : "Add");
}
}
private void loadContactsFromSession() {
allContacts.clear();
var u = Session.currentUser;
var arr = (u == null) ? null : u.optJSONArray("contact_list");
if (arr != null) {
for (int i = 0; i < arr.length(); i++) {
var c = arr.optJSONObject(i);
if (c == null) continue;
String name = c.optString("profile_name", "");
String id = c.optString("contact_id", ""); // internal_uuid مخاطب
if (id.isBlank()) continue;
String imageUrl = c.optString("image_url", "").trim();
if (imageUrl.isEmpty() || imageUrl.equals("null")) {
imageUrl = "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
}
String status = Optional.ofNullable(c.optString("last_seen", ""))
.filter(s -> !s.isBlank()).map(s -> "last seen " + s).orElse("");
allContacts.add(new Contact(id, name, status, imageUrl));
}
}
allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER));
}
private void applyFilter(String q) {
String f = (q == null) ? "" : q.trim().toLowerCase(Locale.ROOT);
List<Contact> filtered = allContacts.stream()
.filter(c -> c.getName().toLowerCase(Locale.ROOT).contains(f))
.collect(Collectors.toList());
renderContacts(filtered);
}
private void renderContacts(List<Contact> contacts) {
contactsList.getChildren().clear();
if (contacts.isEmpty()) {
StackPane emptyPane = new StackPane();
emptyPane.setPrefHeight(240);
emptyPane.setAlignment(Pos.CENTER);
Label emptyLabel = new Label("No contacts found");
emptyLabel.getStyleClass().add("no-contacts-label");
emptyPane.getChildren().add(emptyLabel);
contactsList.getChildren().add(emptyPane);
return;
}
for (Contact c : contacts) {
HBox item = new HBox(10);
item.getStyleClass().add("contact-item");
// Avatar
ImageView avatar = new ImageView(loadAvatar(c.getImageUrl()));
avatar.setFitWidth(48);
avatar.setFitHeight(48);
avatar.setPreserveRatio(true);
// Details (name + status)
VBox details = new VBox(2);
Label nameLabel = new Label(c.getName());
nameLabel.getStyleClass().add("contact-name");
Label statusLabel = new Label(c.getStatus());
statusLabel.getStyleClass().add("contact-status");
details.getChildren().addAll(nameLabel, statusLabel);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
// Add elements (no checkbox here)
item.getChildren().addAll(avatar, details, spacer);
// Highlight if already selected
if (selectedContacts.contains(c)) {
item.getStyleClass().add("contact-selected");
}
// Toggle selection by clicking the whole row
item.setOnMouseClicked(e -> {
if (selectedContacts.contains(c)) {
selectedContacts.remove(c);
item.getStyleClass().remove("contact-selected");
} else {
selectedContacts.add(c);
item.getStyleClass().add("contact-selected");
}
updateMemberCount();
updateSelectedMembersPane();
});
contactsList.getChildren().add(item);
}
}
private Image loadAvatar(String path) {
try {
var in = getClass().getResourceAsStream(path);
if (in != null) return new Image(in);
return new Image(path, true);
} catch (Exception e) {
return new Image(
Objects.requireNonNull(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")
)
);
}
}
private void onAddMembers() {
if (groupInternalId == null) {
showToast("Group internal_id is missing. Make sure setGroupInfo(UUID, ...) was called.");
return;
}
if (selectedContacts.isEmpty()) {
MainController.getInstance().closeOverlay(addMembersCard.getParent());
return;
}
List<String> ids = selectedContacts.stream().map(Contact::getId).toList();
JSONObject batchReq = new JSONObject()
.put("action", "add_members_to_group")
.put("group_id", groupInternalId.toString())
.put("user_ids", new JSONArray(ids));
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(batchReq);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
MainController.getInstance().closeOverlay(addMembersCard.getParent());
});
return;
}
boolean allOk = true;
for (String uid : ids) {
JSONObject single = new JSONObject()
.put("action", "add_members_to_group")
.put("group_id", groupInternalId.toString())
.put("user_id", uid);
JSONObject r = ActionHandler.sendWithResponse(single);
if (r == null || !"success".equalsIgnoreCase(r.optString("status"))) {
allOk = false;
}
}
boolean finalAllOk = allOk;
Platform.runLater(() -> {
if (!finalAllOk) {
showToast("Some members failed.");
}
MainController.getInstance().closeOverlay(addMembersCard.getParent());
});
}).start();
}
private void updateSelectedMembersPane() {
selectedMembersPane.getChildren().clear();
for (Contact c : selectedContacts) {
Label chip = new Label(c.getName());
chip.getStyleClass().add("member-chip");
selectedMembersPane.getChildren().add(chip);
}
}
private void updateMemberCount() {
memberCountLabel.setText(selectedContacts.size() + " / 200000");
}
private void updateSearchIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/search_light.png"
: "/org/to/telegramfinalproject/Icons/search_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
searchIcon.setGraphic(icon);
}
private void showToast(String msg) {
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(addMembersCard.getScene().getWindow());
a.show();
}
public static class Contact {
private final String id; // internal_uuid
private final String name;
private final String status;
private final String imageUrl;
public Contact(String id, String name, String status, String imageUrl) {
this.id = id;
this.name = name;
this.status = status;
this.imageUrl = imageUrl;
}
public String getId() { return id; }
public String getName() { return name; }
public String getStatus() { return status; }
public String getImageUrl() { return imageUrl; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Contact c)) return false;
return Objects.equals(id, c.id);
}
@Override public int hashCode() { return Objects.hash(id); }
}
}
@@ -0,0 +1,291 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
public class AddSubscriberController {
@FXML private VBox contactsList;
@FXML private VBox addMembersCard;
@FXML private TextField searchField;
@FXML private Pane overlayBackground;
@FXML private ScrollPane contactsScroll;
@FXML private Button searchIcon;
@FXML private Label memberCountLabel;
@FXML private FlowPane selectedMembersPane;
@FXML private Button skipButton;
@FXML private Button addButton;
private UUID channelInternalId;
private String channelName;
private String channelDisplayId;
private File channelImageFile;
private String description;
private final Set<Contact> selected = new HashSet<>();
private final List<Contact> allContacts = new ArrayList<>();
@FXML
public void initialize() {
contactsScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
contactsScroll.setPannable(true);
contactsScroll.setFitToWidth(true);
contactsScroll.setFitToHeight(false);
contactsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
});
Platform.runLater(() -> {
if (addMembersCard.getScene() != null) {
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
skipButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
addButton.setOnAction(e -> onAddSubscribers());
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
Platform.runLater(() -> searchField.requestFocus());
loadContactsFromSession();
updateCount();
renderContacts(allContacts);
}
public void setChannelInfo(UUID internalId, String name, String displayId, File imageFile, String description) {
this.channelInternalId = internalId;
this.channelName = name;
this.channelDisplayId = displayId;
this.channelImageFile = imageFile;
this.description = description;
}
public void setChannelInfo(String name, String id, String description, File image) {
this.channelName = name;
this.channelDisplayId = id;
this.channelImageFile = image;
this.description = description;
}
private void loadContactsFromSession() {
allContacts.clear();
var u = Session.currentUser;
var arr = (u == null) ? null : u.optJSONArray("contact_list");
if (arr != null) {
for (int i = 0; i < arr.length(); i++) {
var c = arr.optJSONObject(i);
if (c == null) continue;
String name = c.optString("profile_name", "");
String id = c.optString("contact_id", ""); // internal_uuid
if (id.isBlank()) continue;
String imageUrl = c.optString("image_url", "").trim();
if (imageUrl.isEmpty() || imageUrl.equals("null")) {
imageUrl = "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
}
String status = Optional.ofNullable(c.optString("last_seen", ""))
.filter(s -> !s.isBlank()).map(s -> "last seen " + s).orElse("");
allContacts.add(new Contact(id, name, status, imageUrl));
}
}
allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER));
}
private void applyFilter(String q) {
String f = (q == null) ? "" : q.trim().toLowerCase(Locale.ROOT);
List<Contact> filtered = allContacts.stream()
.filter(c -> c.getName().toLowerCase(Locale.ROOT).contains(f))
.collect(Collectors.toList());
renderContacts(filtered);
}
private void renderContacts(List<Contact> contacts) {
contactsList.getChildren().clear();
if (contacts.isEmpty()) {
StackPane emptyPane = new StackPane();
emptyPane.setPrefHeight(240);
emptyPane.setAlignment(Pos.CENTER);
Label emptyLabel = new Label("No contacts found");
emptyLabel.getStyleClass().add("no-contacts-label");
emptyPane.getChildren().add(emptyLabel);
contactsList.getChildren().add(emptyPane);
return;
}
for (Contact c : contacts) {
HBox item = new HBox(10);
item.getStyleClass().add("contact-item");
// Avatar
ImageView avatar = new ImageView(loadAvatar(c.getImageUrl()));
avatar.setFitWidth(48);
avatar.setFitHeight(48);
avatar.setPreserveRatio(true);
// Details
VBox details = new VBox(2);
Label nameLabel = new Label(c.getName());
nameLabel.getStyleClass().add("contact-name");
Label statusLabel = new Label(c.getStatus());
statusLabel.getStyleClass().add("contact-status");
details.getChildren().addAll(nameLabel, statusLabel);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
// Add elements (no checkbox)
item.getChildren().addAll(avatar, details, spacer);
// Highlight if already selected
if (selected.contains(c)) {
item.getStyleClass().add("contact-selected");
}
// Toggle selection by clicking the row
item.setOnMouseClicked(e -> {
if (selected.contains(c)) {
selected.remove(c);
item.getStyleClass().remove("contact-selected");
} else {
selected.add(c);
item.getStyleClass().add("contact-selected");
}
updateCount();
updateSelectedPane();
});
contactsList.getChildren().add(item);
}
}
private Image loadAvatar(String path) {
try {
var in = getClass().getResourceAsStream(path);
if (in != null) return new Image(in);
return new Image(path, true);
} catch (Exception e) {
return new Image(
Objects.requireNonNull(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")
)
);
}
}
private void onAddSubscribers() {
if (channelInternalId == null) {
showToast("Channel internal_id is missing. Make sure setChannelInfo(UUID, ...) was called.");
return;
}
if (selected.isEmpty()) {
MainController.getInstance().closeOverlay(addMembersCard.getParent());
return;
}
List<String> ids = selected.stream().map(Contact::getId).toList();
JSONObject batchReq = new JSONObject()
.put("action", "add_subscribers_to_channel")
.put("channel_id", channelInternalId.toString())
.put("user_ids", new JSONArray(ids));
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(batchReq);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
MainController.getInstance().closeOverlay(addMembersCard.getParent());
});
return;
}
boolean allOk = true;
for (String uid : ids) {
JSONObject single = new JSONObject()
.put("action", "add_subscriber_to_channel")
.put("channel_id", channelInternalId.toString())
.put("user_id", uid);
JSONObject r = ActionHandler.sendWithResponse(single);
if (r == null || !"success".equalsIgnoreCase(r.optString("status"))) {
allOk = false;
}
}
boolean finalAllOk = allOk;
Platform.runLater(() -> {
if (!finalAllOk) {
showToast("Some subscribers failed.");
}
MainController.getInstance().closeOverlay(addMembersCard.getParent());
});
}).start();
}
private void updateSelectedPane() {
selectedMembersPane.getChildren().clear();
for (Contact c : selected) {
Label chip = new Label(c.getName());
chip.getStyleClass().add("member-chip");
selectedMembersPane.getChildren().add(chip);
}
}
private void updateCount() {
memberCountLabel.setText(selected.size() + " / 200000");
}
private void updateSearchIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/search_light.png"
: "/org/to/telegramfinalproject/Icons/search_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
searchIcon.setGraphic(icon);
}
private void showToast(String msg) {
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(addMembersCard.getScene().getWindow());
a.show();
}
public static class Contact {
private final String id; // internal_uuid
private final String name;
private final String status;
private final String imageUrl;
public Contact(String id, String name, String status, String imageUrl) {
this.id = id; this.name = name; this.status = status; this.imageUrl = imageUrl;
}
public String getId() { return id; }
public String getName() { return name; }
public String getStatus() { return status; }
public String getImageUrl() { return imageUrl; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Contact c)) return false;
return Objects.equals(id, c.id);
}
@Override public int hashCode() { return Objects.hash(id); }
}
}
@@ -0,0 +1,37 @@
// org.to.telegramfinalproject.UI.AppRouter
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public final class AppRouter {
private static Stage stage;
private static Scene scene;
private AppRouter() {}
public static void init(Stage st, Scene sc) {
stage = st;
scene = sc;
}
public static void showIntro() { setRoot("/org/to/telegramfinalproject/Fxml/intro.fxml"); }
public static void showLogin() { setRoot("/org/to/telegramfinalproject/Fxml/login_view.fxml"); }
public static void showRegister(){ setRoot("/org/to/telegramfinalproject/Fxml/register_view.fxml"); }
public static void showMain() { setRoot("/org/to/telegramfinalproject/Fxml/main.fxml"); }
private static void setRoot(String fxmlPath) {
try {
System.out.println("Router: setRoot -> " + fxmlPath);
FXMLLoader fx = new FXMLLoader(AppRouter.class.getResource(fxmlPath));
Parent root = fx.load();
if (scene != null) scene.setRoot(root);
else if (stage != null) stage.setScene(new Scene(root, 1480, 820));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,21 @@
package org.to.telegramfinalproject.UI;
import javafx.beans.binding.Bindings;
import javafx.scene.image.ImageView;
import javafx.scene.shape.Circle;
public final class AvatarFX {
public static void circleClip(ImageView iv, double sizePx) {
iv.setFitWidth(sizePx);
iv.setFitHeight(sizePx);
iv.setPreserveRatio(true);
iv.setSmooth(true);
Circle c = new Circle();
c.radiusProperty().bind(Bindings.min(iv.fitWidthProperty(), iv.fitHeightProperty()).divide(2));
c.centerXProperty().bind(iv.fitWidthProperty().divide(2));
c.centerYProperty().bind(iv.fitHeightProperty().divide(2));
iv.setClip(c);
}
}
@@ -0,0 +1,234 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
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 org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import java.util.*;
import java.util.stream.IntStream;
public class BlockedUsersController {
@FXML private Pane overlayBackground;
@FXML private Button backButton;
@FXML private Button closeButton;
@FXML private Label countLabel;
@FXML private ListView<HBox> blockedList;
@FXML private VBox blockedCard;
private final Map<UUID, HBox> rowByUserId = new HashMap<>();
@FXML
public void initialize() {
backButton.setGraphic(makeIcon("/org/to/telegramfinalproject/Icons/back_button_dark.png"));
loadBlockedUsers();
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayBackground.getParent()));
backButton.setOnAction(e -> MainController.getInstance().goBack((StackPane) overlayBackground.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(blockedCard.getParent()));
// Theme
Platform.runLater(() -> {
if (blockedCard.getScene() != null) {
ThemeManager.getInstance().registerScene(blockedCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateBackIcon(newVal));
updateBackIcon(ThemeManager.getInstance().isDarkMode());
// Smooth scroll feel
blockedList.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
blockedList.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
ScrollBar vBar = (ScrollBar) blockedList.lookup(".scroll-bar:vertical");
if (vBar != null) {
blockedList.setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
double newValue = vBar.getValue() - deltaY;
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
});
}
}
});
}
/* ===================== Networking ===================== */
private void loadBlockedUsers() {
new Thread(() -> {
JSONObject req = new JSONObject().put("action", "get_blocked_users");
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
System.err.println("get_blocked_users failed: " + (resp != null ? resp.optString("message") : "no response"));
Platform.runLater(() -> {
blockedList.getItems().clear();
updateCount();
});
return;
}
JSONObject data = resp.optJSONObject("data");
JSONArray arr = data != null ? data.optJSONArray("blocked_users") : null;
if (arr == null) arr = new JSONArray();
final JSONArray finalArr = arr;
Platform.runLater(() -> renderBlockedUsers(finalArr));
}).start();
}
private void unblockUser(UUID targetId) {
String me = Session.currentUser != null ? Session.currentUser.optString("internal_uuid", "") : "";
if (me.isBlank() || targetId == null) return;
new Thread(() -> {
JSONObject req = new JSONObject()
.put("action", "toggle_block")
.put("user_id", me) // UUID
.put("target_id", targetId.toString()); // UUID
JSONObject resp = ActionHandler.sendWithResponse(req);
boolean ok = resp != null && "success".equalsIgnoreCase(resp.optString("status"));
Platform.runLater(() -> {
if (ok) {
HBox row = rowByUserId.remove(targetId);
if (row != null) blockedList.getItems().remove(row);
updateCount();
} else {
String msg = (resp != null ? resp.optString("message", "Failed to unblock.") : "Failed to unblock.");
showToast(msg);
}
});
}).start();
}
/* ===================== UI build ===================== */
private void renderBlockedUsers(JSONArray list) {
blockedList.getItems().clear();
rowByUserId.clear();
IntStream.range(0, list.length()).forEach(i -> {
JSONObject o = list.optJSONObject(i);
if (o == null) return;
UUID uid = parseUUID(optS(o, "internal_uuid"));
if (uid == null) return;
String name = nz(optS(o, "profile_name", optS(o, "name", "User")));
String handle = firstNonEmpty(
optS(o, "username"),
optS(o, "display_id"),
optS(o, "user_name"),
""
);
String avatarUrl = optS(o, "image_url");
HBox row = buildRow(uid, name, handle, avatarUrl);
rowByUserId.put(uid, row);
blockedList.getItems().add(row);
});
updateCount();
}
private HBox buildRow(UUID uid, String name, String id, String avatarUrl) {
// Avatar
Image img = null;
if (hasVal(avatarUrl)) {
try { img = org.to.telegramfinalproject.Client.AvatarLocalResolver.load(avatarUrl); } catch (Exception ignore) {}
}
if (img == null) {
img = new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png"));
}
ImageView avatar = new ImageView(img);
avatar.setFitWidth(60);
avatar.setFitHeight(60);
avatar.setPreserveRatio(true);
avatar.setSmooth(true);
avatar.setCache(true);
avatar.setClip(new Circle(20, 20, 40));
// Name + ID
VBox info = new VBox(2);
Label nameLabel = new Label(name);
nameLabel.getStyleClass().add("blocked-name");
Label idLabel = new Label(id);
idLabel.getStyleClass().add("blocked-id");
info.getChildren().addAll(nameLabel, idLabel);
// Unblock btn
Button unblockBtn = new Button("Unblock");
unblockBtn.getStyleClass().add("unblock-button");
unblockBtn.setOnAction(e -> unblockUser(uid));
HBox row = new HBox(12, avatar, info, unblockBtn);
row.getStyleClass().add("blocked-row");
HBox.setHgrow(info, javafx.scene.layout.Priority.ALWAYS);
return row;
}
private void updateCount() {
countLabel.setText(blockedList.getItems().size() + " blocked users");
}
/* ===================== Helpers ===================== */
private void updateBackIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/back_button_light.png"
: "/org/to/telegramfinalproject/Icons/back_button_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
backButton.setGraphic(icon);
}
private ImageView makeIcon(String path) {
ImageView iv = new ImageView(new Image(getClass().getResourceAsStream(path)));
iv.setFitWidth(20);
iv.setFitHeight(20);
return iv;
}
private static String nz(String s){ return s==null? "": s.trim(); }
private static boolean hasVal(String s){ return s!=null && !s.trim().isEmpty() && !"null".equalsIgnoreCase(s); }
private static String optS(JSONObject j, String k){ return j!=null ? nz(j.optString(k,"")) : ""; }
private static String optS(JSONObject j, String k, String fallback){
String v = optS(j,k);
return hasVal(v) ? v : fallback;
}
private static String firstNonEmpty(String... vals){
for (String v : vals) if (hasVal(v)) return v;
return "";
}
private static UUID parseUUID(String s){
try { return UUID.fromString(nz(s)); } catch (Exception e) { return null; }
}
private void showToast(String msg) {
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
if (a.getDialogPane().getScene() != null) {
ThemeManager.getInstance().registerScene(a.getDialogPane().getScene());
}
a.showAndWait();
}
}
@@ -0,0 +1,197 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
public class ChangeCredentialsController {
@FXML private VBox credentialsCard;
@FXML private Pane overlayBackground;
@FXML private TextField usernameField;
@FXML private Label usernameLabel;
@FXML private Label passwordLabel;
@FXML private PasswordField passwordField;
@FXML private TextField visiblePasswordField;
@FXML private Button togglePasswordBtn;
@FXML private Label confirmPasswordLabel;
@FXML private PasswordField confirmPasswordField;
@FXML private TextField visibleConfirmPasswordField;
@FXML private Button toggleConfirmBtn;
@FXML private Button cancelButton;
@FXML private Button saveButton;
@FXML private Button closeButton;
private boolean passwordVisible = false;
private boolean confirmVisible = false;
private String currentPassword;
public void setCurrentPassword(String p) { this.currentPassword = p; }
@FXML
private void initialize() {
// Toggle password visibility
togglePasswordBtn.setOnAction(e -> togglePasswordVisibility());
toggleConfirmBtn.setOnAction(e -> toggleConfirmVisibility());
saveButton.setOnAction(e -> saveAndClose());
closeButton.setOnAction(e -> saveAndClose());
cancelButton.setOnAction(e -> saveAndClose());
overlayBackground.setOnMouseClicked(e -> saveAndClose());
// Reset error when typing
usernameField.textProperty().addListener((obs, o, n) -> removeError(usernameField, usernameLabel));
passwordField.textProperty().addListener((obs, o, n) -> {
visiblePasswordField.setText(n); // keep synced
removeError(passwordField, passwordLabel);
removeError(visiblePasswordField, passwordLabel);
});
visiblePasswordField.textProperty().addListener((obs, o, n) -> {
passwordField.setText(n);
removeError(passwordField, passwordLabel);
removeError(visiblePasswordField, passwordLabel);
});
confirmPasswordField.textProperty().addListener((obs, o, n) -> {
visibleConfirmPasswordField.setText(n);
removeError(confirmPasswordField, confirmPasswordLabel);
removeError(visibleConfirmPasswordField, confirmPasswordLabel);
});
visibleConfirmPasswordField.textProperty().addListener((obs, o, n) -> {
confirmPasswordField.setText(n);
removeError(confirmPasswordField, confirmPasswordLabel);
removeError(visibleConfirmPasswordField, confirmPasswordLabel);
});
Platform.runLater(() -> usernameField.requestFocus());
}
public void prefillFromSession() {
var u = Session.currentUser;
if (u == null) return;
String handle = u.optString("user_id",
u.optString("username",
u.optString("display_id","")));
usernameField.setText(handle);
}
private void togglePasswordVisibility() {
passwordVisible = !passwordVisible;
visiblePasswordField.setText(passwordField.getText());
visiblePasswordField.setVisible(passwordVisible);
visiblePasswordField.setManaged(passwordVisible);
passwordField.setVisible(!passwordVisible);
passwordField.setManaged(!passwordVisible);
togglePasswordBtn.setText(passwordVisible ? "👁" : "👁");
}
private void toggleConfirmVisibility() {
confirmVisible = !confirmVisible;
visibleConfirmPasswordField.setText(confirmPasswordField.getText());
visibleConfirmPasswordField.setVisible(confirmVisible);
visibleConfirmPasswordField.setManaged(confirmVisible);
confirmPasswordField.setVisible(!confirmVisible);
confirmPasswordField.setManaged(!confirmVisible);
toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁");
}
private void saveAndClose() {
boolean anyError = false;
boolean sentSomething = false;
var cu = Session.currentUser;
String oldUsername = cu != null ? cu.optString("user_id",
cu.optString("username",
cu.optString("display_id",""))) : "";
String newUsername = usernameField.getText().trim();
if (!newUsername.isEmpty() && !newUsername.equals(oldUsername)) {
sentSomething = true;
JSONObject req = new JSONObject()
.put("action", "update_username")
.put("current_password", currentPassword)
.put("new_username", newUsername);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
anyError = true;
showError("Changing username failed",
resp != null ? resp.optString("message","Unknown error") : "No response");
} else {
if (cu != null) cu.put("user_id", newUsername);
}
}
// 2) پسورد
String newPwd = (passwordVisible ? visiblePasswordField.getText() : passwordField.getText()).trim();
String confirm = (confirmVisible ? visibleConfirmPasswordField.getText() : confirmPasswordField.getText()).trim();
if (!newPwd.isEmpty()) {
if (confirm.isEmpty() || !confirm.equals(newPwd)) {
addError(confirmPasswordField, confirmPasswordLabel);
addError(visibleConfirmPasswordField, confirmPasswordLabel);
return;
}
sentSomething = true;
JSONObject req = new JSONObject()
.put("action", "update_password")
.put("current_password", currentPassword)
.put("new_password", newPwd);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
anyError = true;
showError("Changing password failed",
resp != null ? resp.optString("message","Unknown error") : "No response");
}
}
if (!sentSomething || !anyError) {
var sc = SettingsController.getInstance();
if (sc != null) sc.populateFromSession();
MainController.getInstance().closeOverlay(credentialsCard.getParent());
}
}
private void addError(TextField field, Label label) {
if (!field.getStyleClass().contains("error-input")) {
field.getStyleClass().add("error-input");
}
if (!label.getStyleClass().contains("error-label")) {
label.getStyleClass().add("error-label");
}
}
private void removeError(TextField field, Label label) {
field.getStyleClass().remove("error-input");
label.getStyleClass().remove("error-label");
}
private void showError(String title, String msg) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setTitle(title);
a.setHeaderText(null);
a.setContentText(msg);
if (a.getDialogPane().getScene()!=null) {
ThemeManager.getInstance().registerScene(a.getDialogPane().getScene());
}
a.showAndWait();
}
}
@@ -0,0 +1,355 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
public class ChannelInfoController {
@FXML private VBox channelCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private ImageView channelAvatar;
@FXML private Label channelName;
@FXML private Label subscriberCount;
@FXML private Label channelDescription;
@FXML private Button infoMoreButton;
@FXML private ContextMenu infoMoreMenu;
@FXML private MenuItem addMembersBtn;
@FXML private MenuItem manageChannelBtn;
@FXML private MenuItem deleteChannelBtn;
@FXML private ImageView moreIcon;
@FXML private ImageView manageChannelIcon;
@FXML private ImageView addMemberIcon;
@FXML private VBox subscribersList;
@FXML private Label subscribersHeader;
@FXML private Button addSubscriberButton;
@FXML private ImageView subscribersIcon;
@FXML private ScrollPane subscribersScroll;
private UUID channelId;
private String myRole;
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML
private void initialize() {
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(channelCard.getParent()));
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(channelCard.getParent()));
infoMoreButton.setOnAction(e -> {
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
});
if (manageChannelBtn != null) {
manageChannelBtn.setOnAction(e -> openManageChannel());
}
deleteChannelBtn.setOnAction(e -> handleDeleteChannel());
Platform.runLater(() -> {
if (channelCard.getScene() != null) {
ThemeManager.getInstance().registerScene(channelCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateIcons(newVal);
});
updateIcons(ThemeManager.getInstance().isDarkMode());
// Smooth scroll feel
subscribersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
subscribersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
ScrollBar vBar = (ScrollBar) subscribersScroll.lookup(".scroll-bar:vertical");
if (vBar != null) {
subscribersScroll.setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
double newValue = vBar.getValue() - deltaY;
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
});
}
}
});
if (addSubscriberButton != null) {
addSubscriberButton.setOnAction(e -> openAddSubscriberScene());
}
if (addMembersBtn != null) {
addMembersBtn.setOnAction(e -> openAddSubscriberScene());
}
}
public void setChannelDataFromJson(ChatEntry entry, JSONObject data) {
this.channelId = entry.getId();
channelName.setText(data.optString("channel_name", entry.getName()));
subscriberCount.setText(data.optInt("subscriber_count", 0) + " subscribers");
channelDescription.setText(data.optString("description", ""));
// --- Role-based UI ---
myRole = data.optString("my_role", "subscriber").toLowerCase();
deleteChannelBtn.setVisible("owner".equals(myRole));
boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
addSubscriberButton.setVisible(canAdd);
addSubscriberButton.setManaged(canAdd);
if (addMembersBtn != null) {
addMembersBtn.setVisible(canAdd);
}
boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
infoMoreButton.setVisible(showMore);
infoMoreButton.setManaged(showMore);
// --- Avatar ---
String imgUrl = data.optString("image_url", "");
if (!imgUrl.isBlank()) {
try {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) channelAvatar.setImage(img);
} catch (Exception ignore) {}
} else {
channelAvatar.setImage(new Image(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_channel_profile.png")
));
}
// --- Subscribers list ---
subscribersList.getChildren().clear();
var arr = data.optJSONArray("subscribers");
if (arr != null) {
subscribersHeader.setText(arr.length() + " SUBSCRIBERS");
for (int i = 0; i < arr.length(); i++) {
JSONObject s = arr.getJSONObject(i);
addSubscriberRow(s);
}
}
}
private void addSubscriberRow(JSONObject s) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
AvatarFX.circleClip(avatar, 36);
String imgUrl = s.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + status
VBox nameBox = new VBox(2);
Label name = new Label(s.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
String status;
if (s.optBoolean("is_online", false)) {
status = "online";
} else {
status = ChatPageController.getInstance().userStatusText(
false,
s.optString("last_seen", null)
);
}
Label statusLbl = new Label(status);
statusLbl.getStyleClass().add("member-status");
nameBox.getChildren().addAll(name, statusLbl);
// Role
Label roleLbl = new Label();
String roleStr = s.optString("role", "");
if (!roleStr.isBlank()) {
roleLbl.setText(roleStr.toLowerCase());
roleLbl.getStyleClass().add("member-role");
}
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, nameBox, spacer, roleLbl);
subscribersList.getChildren().add(row);
}
private void openManageChannel() {
new Thread(() -> {
try {
JSONObject req = new JSONObject()
.put("action", "view_channel")
.put("channel_id", channelId.toString()); // matches server case
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
resp != null ? resp.optString("message") : "Server not responding.",
Alert.AlertType.ERROR
));
return;
}
JSONObject data = resp.optJSONObject("data");
if (data == null) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Malformed server response.",
Alert.AlertType.ERROR
));
return;
}
Platform.runLater(() -> {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_channel.fxml"));
Node overlay = loader.load();
ManageChannelController controller = loader.getController();
controller.setChannelData(data); // pass JSON to controller
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Manage Channel scene.",
Alert.AlertType.ERROR
);
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Error while fetching channel info: " + e.getMessage(),
Alert.AlertType.ERROR
));
}
}).start();
}
private void openAddSubscriberScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); // point to new fxml
Node overlay = loader.load();
AddSubscriberController controller = loader.getController();
controller.setChannelInfo(channelId, channelName.getText(), "", null, "");
// passing UUID + name, other fields optional
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Subscribers scene.",
Alert.AlertType.ERROR
);
}
}
private void handleDeleteChannel() {
ChatEntry entry = Session.currentChatEntry;
if (entry == null || !"channel".equalsIgnoreCase(entry.getType())) {
alert(Alert.AlertType.INFORMATION, "This action is only available for channels.");
return;
}
if (!confirm("Delete Channel",
"Are you sure you want to DELETE this channel?\nThis action cannot be undone.")) {
return;
}
JSONObject req = new JSONObject()
.put("action", "delete_channel")
.put("channel_id", entry.getId().toString());
JSONObject res = ActionHandler.sendWithResponse(req);
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
alert(Alert.AlertType.INFORMATION, "✅ Channel deleted successfully.");
MainController.getInstance().refreshChatListUI();
AppRouter.showMain();
} else {
String msg = (res != null) ? res.optString("message", "Failed to delete channel.") : "null response";
alert(Alert.AlertType.ERROR, "" + msg);
}
}
private boolean confirm(String title, String msg) {
Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL);
a.setTitle(title);
return a.showAndWait().filter(btn -> btn == ButtonType.OK).isPresent();
}
private void alert(Alert.AlertType type, String msg) {
new Alert(type, msg, ButtonType.OK).show();
}
private void updateIcons(boolean dark) {
String suffix = dark ? "_light.png" : "_dark.png";
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
subscribersIcon.setImage(loadImage(ICON_PATH + "channel_subscriber" + suffix));
addSubscriberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
}
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) return null;
return new Image(res.toExternalForm());
}
}
@@ -0,0 +1,150 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.util.Objects;
public class ChatItemController {
@FXML private ImageView profileImage;
@FXML private Label chatName;
@FXML private Label lastMessage;
@FXML private Label chatTime;
@FXML private Label unreadCount;
@FXML private StackPane systemAvatar;
@FXML private ImageView profileImageUser;
@FXML private ImageView profileImageSystem;
@FXML private Circle systemCircle;
/**
* Set chat item data, including a profile image (or default).
*
* @param name Chat name
* @param lastMsg Last message text
* @param time Last message time
* @param unread Unread message count
* @param imageUrl Path/URL of profile image (can be null/empty)
*/
public void setChatData(String name, String lastMsg, String time, int unread, String imageUrl, String chatType) {
chatName.setText(name);
lastMessage.setText(lastMsg);
chatTime.setText(time);
// Unread count
if (unread > 0) {
unreadCount.setVisible(true);
unreadCount.setText(String.valueOf(unread));
} else {
unreadCount.setVisible(false);
}
// Reset all avatar states
profileImageUser.setVisible(false);
profileImageUser.setManaged(false);
systemAvatar.setVisible(false);
systemAvatar.setManaged(false);
if ("Saved Messages".equals(name)) {
systemCircle.setFill(javafx.scene.paint.Color.web("#2ca4ff")); // Telegram blue
profileImageSystem.setImage(new Image(
Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Icons/saved_messages_light.png"))
));
systemAvatar.setVisible(true);
systemAvatar.setManaged(true);
} else if ("Archived Chats".equals(name)) {
systemCircle.setFill(javafx.scene.paint.Color.web("#808080")); // gray
profileImageSystem.setImage(new Image(
Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Icons/archived_chats_light.png"))
));
systemAvatar.setVisible(true);
systemAvatar.setManaged(true);
} else if (imageUrl != null && !imageUrl.isEmpty()) {
// profileImageUser.setImage(new Image(imageUrl, true));
// profileImageUser.setVisible(true);
// profileImageUser.setManaged(true);
Image img = AvatarLocalResolver.load(imageUrl);
if (img != null) {
profileImageUser.setImage(img);
AvatarFX.circleClip(profileImageUser, 40);
} else {
String path;
if ("group".equalsIgnoreCase(chatType)) {
path = "/org/to/telegramfinalproject/Avatars/default_group_profile.png";
} else if ("channel".equalsIgnoreCase(chatType)) {
path = "/org/to/telegramfinalproject/Avatars/default_channel_profile.png";
} else {
path = "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
}
profileImageUser.setImage(new Image(
java.util.Objects.requireNonNull(getClass().getResourceAsStream(path))
));
}
profileImageUser.setVisible(true);
profileImageUser.setManaged(true);
} else {
String path;
if ("group".equalsIgnoreCase(chatType)) {
path = "/org/to/telegramfinalproject/Avatars/default_group_profile.png";
} else if ("channel".equalsIgnoreCase(chatType)) {
path = "/org/to/telegramfinalproject/Avatars/default_channel_profile.png";
} else {
path = "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
}
profileImageUser.setImage(new Image(
Objects.requireNonNull(getClass().getResourceAsStream(path))
));
profileImageUser.setVisible(true);
profileImageUser.setManaged(true);
}
}
public void setUnread(int unread) {
boolean show = unread > 0;
unreadCount.setVisible(show);
unreadCount.setManaged(show);
if (show) unreadCount.setText(String.valueOf(unread));
}
// ChatItemController.java
@FXML private ImageView avatarImage;
private String chatType;
public void updateAvatar(String url) {
try {
if (url == null || url.isBlank()) {
String def = switch (chatType == null ? "" : chatType.toLowerCase()) {
case "group" -> "/org/to/telegramfinalproject/Avatars/default_group_profile.png";
case "channel" -> "/org/to/telegramfinalproject/Avatars/default_channel_profile.png";
default -> "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
};
avatarImage.setImage(new Image(Objects.requireNonNull(
getClass().getResourceAsStream(def))));
} else {
avatarImage.setImage(new Image(url, true)); // true = لود غیرهمزمان
}
} catch (Exception e) {
e.printStackTrace();
}
}
private String safeTitle(ChatEntry e) {
if (e.getName() != null && !e.getName().isBlank()) return e.getName();
if (e.getDisplayId() != null && !e.getDisplayId().isBlank()) return e.getDisplayId();
return "Unknown";
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
package org.to.telegramfinalproject.UI;
//For search handling
public enum ChatViewMode {
NORMAL, // member/contact; can send messages
NEEDS_JOIN, // group/channel preview; show Join button
NEEDS_ADD_CONTACT, // private preview; show Add Contact button
READ_ONLY,
BLOCKED
}
@@ -0,0 +1,173 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import java.io.IOException;
public class CheckPasswordController {
@FXML private VBox checkPasswordCard;
@FXML private Pane overlayBackground;
@FXML private Button backButton;
@FXML private Button closeButton;
@FXML private Button cancelButton;
@FXML private Button confirmButton;
@FXML private PasswordField passwordField;
@FXML private TextField visiblePasswordField;
@FXML private Button toggleVisibilityBtn;
@FXML private Label passwordLabel;
private boolean passwordVisible = false;
@FXML
public void initialize() {
// Set back button icon
ImageView backIcon = new ImageView(
new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/back_button_dark.png"))
);
backIcon.setFitWidth(18);
backIcon.setFitHeight(18);
backButton.setGraphic(backIcon);
cancelButton.setOnAction(e ->
MainController.getInstance().closeOverlay(checkPasswordCard.getParent())
);
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(checkPasswordCard.getParent())
);
backButton.setOnAction(e ->
MainController.getInstance().goBack(checkPasswordCard)
);
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(checkPasswordCard.getParent())
);
confirmButton.setOnAction(e -> validatePassword());
// Reset error state when user starts typing again
passwordField.textProperty().addListener((obs, oldVal, newVal) -> {
if (!newVal.trim().isEmpty()) {
passwordField.getStyleClass().remove("error");
passwordLabel.getStyleClass().remove("error");
}
});
// Reset error state when user starts typing again
visiblePasswordField.textProperty().addListener((obs, oldVal, newVal) -> {
if (!newVal.trim().isEmpty()) {
visiblePasswordField.getStyleClass().remove("error");
passwordLabel.getStyleClass().remove("error");
}
});
// Auto_focus password field when overlay opens
Platform.runLater(() -> passwordField.requestFocus());
// Keep fields in sync
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
// Toggle button action
toggleVisibilityBtn.setOnAction(e -> togglePasswordVisibility());
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (checkPasswordCard.getScene() != null) {
ThemeManager.getInstance().registerScene(checkPasswordCard.getScene());
}
});
// Listener for theme change
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateBackIcon(newVal);
});
// Set initial state
updateBackIcon(ThemeManager.getInstance().isDarkMode());
}
private void validatePassword() {
String entered = (passwordVisible ? visiblePasswordField.getText() : passwordField.getText()).trim();
if (entered.isEmpty()) {
if (!passwordField.getStyleClass().contains("error"))
passwordField.getStyleClass().add("error");
if (!visiblePasswordField.getStyleClass().contains("error"))
visiblePasswordField.getStyleClass().add("error");
if (!passwordLabel.getStyleClass().contains("error"))
passwordLabel.getStyleClass().add("error");
return;
}
org.json.JSONObject req = new org.json.JSONObject()
.put("action", "verify_password")
.put("current_password", entered);
org.json.JSONObject resp = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req);
boolean ok = (resp != null && "success".equalsIgnoreCase(resp.optString("status")));
if (ok) {
passwordField.getStyleClass().remove("error");
visiblePasswordField.getStyleClass().remove("error");
passwordLabel.getStyleClass().remove("error");
openChangeCredentials(entered);
} else {
if (!passwordField.getStyleClass().contains("error"))
passwordField.getStyleClass().add("error");
if (!visiblePasswordField.getStyleClass().contains("error"))
visiblePasswordField.getStyleClass().add("error");
if (!passwordLabel.getStyleClass().contains("error"))
passwordLabel.getStyleClass().add("error");
}
}
private void togglePasswordVisibility() {
passwordVisible = !passwordVisible;
visiblePasswordField.setVisible(passwordVisible);
visiblePasswordField.setManaged(passwordVisible);
passwordField.setVisible(!passwordVisible);
passwordField.setManaged(!passwordVisible);
toggleVisibilityBtn.setText(passwordVisible ? "👁" : "👁");
}
private void updateBackIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/back_button_light.png"
: "/org/to/telegramfinalproject/Icons/back_button_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
backButton.setGraphic(icon);
}
private void openChangeCredentials(String currentPassword) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/change_credentials.fxml"));
Node overlay = loader.load();
ChangeCredentialsController c = loader.getController();
c.setCurrentPassword(currentPassword);
c.prefillFromSession();
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
@@ -0,0 +1,299 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
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.to.telegramfinalproject.Models.ContactEntry;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class ContactsController {
@FXML private VBox contactsList;
@FXML private VBox contactsCard;
@FXML private TextField searchField;
@FXML private Button closeFooterButton;
@FXML private Pane overlayBackground;
@FXML private ScrollPane contactsScroll;
@FXML private Button searchIcon;
private final List<ContactVM> allContacts = new ArrayList<>();
@FXML
public void initialize() {
loadContactsAndRender();
searchField.textProperty().addListener((obs, ov, nv) -> {
String f = nv == null ? "" : nv.trim().toLowerCase();
List<ContactVM> filtered = allContacts.stream()
.filter(c -> c.profileName.toLowerCase().contains(f)
|| (c.userId != null && c.userId.toLowerCase().contains(f)))
.sorted(Comparator.comparing(c -> c.profileName.toLowerCase()))
.collect(Collectors.toList());
renderContacts(filtered);
});
Platform.runLater(() -> searchField.requestFocus());
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
contactsScroll.setPannable(true);
contactsScroll.setFitToWidth(true);
contactsScroll.setFitToHeight(false);
contactsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
});
Platform.runLater(() -> {
if (contactsCard.getScene() != null) {
ThemeManager.getInstance().registerScene(contactsCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal));
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
}
private void loadContactsAndRender() {
CompletableFuture
.supplyAsync(() -> {
try {
if (Session.contactEntries != null && !Session.contactEntries.isEmpty()) {
return new ArrayList<>(Session.contactEntries);
}
JSONObject req = new JSONObject()
.put("action", "view_contacts")
.put("user_id", Session.getUserUUID());
JSONObject res = ActionHandler.sendWithResponse(req);
if (res == null || !"success".equals(res.optString("status"))) {
return Collections.emptyList();
}
JSONObject data = res.optJSONObject("data");
JSONArray arr = data != null ? data.optJSONArray("contacts") : null;
if (arr == null) return Collections.emptyList();
List<ContactEntry> fetched = new ArrayList<>();
for (int i = 0; i < arr.length(); i++) {
JSONObject c = arr.getJSONObject(i);
UUID contactId = UUID.fromString(c.getString("contact_id"));
String userId = c.optString("user_id", null);
String contactDisplay = c.optString("contact_display_id", userId);
String profileName = c.optString("profile_name", contactDisplay);
String imageUrl = c.optString("image_url", "/org/to/telegramfinalproject/Avatars/default_user_profile.png");
boolean isBlocked = c.optBoolean("is_blocked", false);
String lastSeenStr = c.optString("last_seen", null);
LocalDateTime lastSeen = null;
if (lastSeenStr != null && !lastSeenStr.isEmpty()) {
try { lastSeen = LocalDateTime.parse(lastSeenStr); } catch (Exception ignore) {}
}
fetched.add(new ContactEntry(contactId, userId, contactDisplay, profileName, imageUrl, isBlocked, lastSeen));
}
if (Session.contactEntries == null) Session.contactEntries = new ArrayList<>();
Session.contactEntries.clear();
Session.contactEntries.addAll(fetched);
return fetched;
} catch (Exception e) {
e.printStackTrace();
return Collections.<ContactEntry>emptyList();
}
})
.thenAccept(entries -> Platform.runLater(() -> {
allContacts.clear();
for (Object ce : entries) {
allContacts.add(ContactVM.from((ContactEntry) ce));
}
allContacts.sort(Comparator.comparing(vm -> vm.profileName.toLowerCase()));
renderContacts(allContacts);
}));
}
private void renderContacts(List<ContactVM> contacts) {
contactsList.getChildren().clear();
if (contacts.isEmpty()) {
StackPane emptyPane = new StackPane();
emptyPane.setPrefHeight(300);
emptyPane.setAlignment(Pos.CENTER);
Label emptyLabel = new Label("No contacts found");
emptyLabel.getStyleClass().add("no-contacts-label");
emptyPane.getChildren().add(emptyLabel);
contactsList.getChildren().add(emptyPane);
return;
}
for (ContactVM c : contacts) {
HBox item = new HBox(10);
item.getStyleClass().add("contact-item");
item.setCursor(Cursor.HAND);
ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl));
avatar.setFitWidth(58);
avatar.setFitHeight(58);
avatar.setPreserveRatio(true);
VBox details = new VBox(2);
Label nameLabel = new Label(c.profileName);
nameLabel.getStyleClass().add("contact-name");
details.getChildren().addAll(nameLabel);
item.getChildren().addAll(avatar, details);
item.setOnMouseClicked(e -> openOrStartPrivateChat(c));
contactsList.getChildren().add(item);
}
}
private Image loadAvatarSafe(String urlOrResource) {
try {
if (urlOrResource != null) {
if (urlOrResource.startsWith("/")) {
return new Image(Objects.requireNonNull(
getClass().getResourceAsStream(urlOrResource)));
}
if (urlOrResource.startsWith("http://") ||
urlOrResource.startsWith("https://") ||
urlOrResource.startsWith("file:")) {
return new Image(urlOrResource, true); // true = لود async
}
}
return new Image(Objects.requireNonNull(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
} catch (Exception e) {
return new Image(Objects.requireNonNull(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
}
}
private void updateSearchIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/search_light.png"
: "/org/to/telegramfinalproject/Icons/search_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
searchIcon.setGraphic(icon);
}
private void openOrStartPrivateChat(ContactVM contact) {
ChatEntry existing = findExistingPrivateChatWith(contact.contactId);
if (existing != null) {
MainController.getInstance().openChat(existing);
MainController.getInstance().closeOverlay(contactsCard.getParent());
return;
}
CompletableFuture
.supplyAsync(() -> {
try {
UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
JSONObject req = new JSONObject()
.put("action", "get_or_create_private_chat")
.put("user1", myId.toString())
.put("user2", contact.contactId.toString());
JSONObject res = ActionHandler.sendWithResponse(req);
if (res == null || !"success".equals(res.optString("status"))) {
throw new RuntimeException(res != null ? res.optString("message", "Unknown error")
: "null response");
}
JSONObject data = res.getJSONObject("data");
UUID chatId = UUID.fromString(data.getString("chat_id"));
ChatEntry entry = new ChatEntry(
chatId,
contact.userId,
contact.profileName,
contact.imageUrl,
"private",
null,
false,
false
);
entry.setOtherUserId(contact.contactId);
return entry;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
})
.thenAccept(entry -> Platform.runLater(() -> {
MainController.getInstance().onJoinedOrAdded(entry);
MainController.getInstance().openChat(entry);
MainController.getInstance().closeOverlay(contactsCard.getParent());
}))
.exceptionally(err -> {
Platform.runLater(() -> {
Alert a = new Alert(Alert.AlertType.ERROR, "Failed to start chat: " + err.getMessage(), ButtonType.OK);
a.showAndWait();
});
return null;
});
}
private ChatEntry findExistingPrivateChatWith(UUID otherUserUuid) {
if (Session.chatList != null) {
for (ChatEntry ce : Session.chatList) {
if (!"private".equalsIgnoreCase(ce.getType())) continue;
UUID stored = ce.getOtherUserId();
if (stored != null && stored.equals(otherUserUuid)) return ce;
}
}
if (Session.activeChats != null) {
for (ChatEntry ce : Session.activeChats) {
if (!"private".equalsIgnoreCase(ce.getType())) continue;
UUID stored = ce.getOtherUserId();
if (stored != null && stored.equals(otherUserUuid)) return ce;
}
}
return null;
}
private static class ContactVM {
final UUID contactId; // internal UUID
final String userId; // @id
final String profileName;
final String imageUrl;
static ContactVM from(ContactEntry ce) {
return new ContactVM(ce.getContactId(), ce.getUserId(), ce.getProfileName(), ce.getImageUrl());
}
ContactVM(UUID contactId, String userId, String profileName, String imageUrl) {
this.contactId = contactId;
this.userId = userId;
this.profileName = profileName != null ? profileName : (userId != null ? userId : "Unknown");
this.imageUrl = imageUrl != null ? imageUrl : "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
}
}
}
@@ -0,0 +1,333 @@
package org.to.telegramfinalproject.UI;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
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;
@FXML private Button backButton;
@FXML private Button closeButton;
@FXML private ImageView profileImageView;
@FXML private ImageView cameraIcon;
@FXML private Button cameraButton;
@FXML private Label profileName;
@FXML private Label profileStatus;
@FXML private TextField nameField;
@FXML private TextField bioField;
@FXML private TextField usernameField;
// EditProfileController.java
private SettingsController parentSettings;
public void setParentSettings(SettingsController sc) {
this.parentSettings = sc;
}
@FXML
private void initialize() {
backButton.setGraphic(new ImageView(
new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/back_button_dark.png"))
));
// close on background click
// overlayBackground.setOnMouseClicked(e -> {
// saveChangesAndClose();
// closeEdit();
// });
//
// closeButton.setOnAction(e -> {
// saveChangesAndClose();
// closeEdit();
// });
//
// backButton.setOnAction(e -> {
// saveChangesAndClose();
// MainController.getInstance().goBack(overlayBackground);
// });
overlayBackground.setOnMouseClicked(e -> {
saveChangesAndClose();
closeEdit(); // همینه
});
closeButton.setOnAction(e -> {
saveChangesAndClose();
closeEdit(); // همینه
});
backButton.setOnAction(e -> {
saveChangesAndClose();
MainController.getInstance().goBack(overlayBackground);
});
// Load your camera icon image (white camera icon for visibility)
cameraIcon.setImage(new Image(
Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Icons/camera.png"))
));
// Handle click to open file chooser
cameraButton.setOnMouseClicked(e -> openImageChooser());
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (editCard.getScene() != null) {
ThemeManager.getInstance().registerScene(editCard.getScene());
}
});
// Listener for theme change
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateBackIcon(newVal);
});
// Set initial state
updateBackIcon(ThemeManager.getInstance().isDarkMode());
}
private void updateBackIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/back_button_light.png"
: "/org/to/telegramfinalproject/Icons/back_button_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
backButton.setGraphic(icon);
}
private void openImageChooser() {
FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg")
);
File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow());
if (file != null) {
pickedAvatarFile = file;
profileImageView.setImage(new Image(file.toURI().toString()));
}
}
private void closeEdit() {
// MainController.getInstance().goBack(overlayBackground);
MainController.getInstance().closeOverlay(editCard.getParent());
}
public void setProfileData(String name, String status, String bio, String userId, Image profileImage) {
profileStatus.setText(status);
profileName.setText(name);
nameField.setText(name);
bioField.setText(bio != null ? bio : "");
usernameField.setText(userId);
profileImageView.setImage(profileImage);
}
private void saveChangesAndClose() {
JSONObject currentUser = Session.currentUser;
String newBio = bioField.getText().trim();
String newName = nameField.getText().trim();
String newUserId = usernameField.getText().trim();
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()) {
String extracted = extractImageUrl(url);
if (isLikelyImageUrl(extracted)) {
String finalUrl = addCacheBusterIfHttp(extracted);
Session.currentUser.put("image_url", finalUrl);
} else {
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)) {
JSONObject req = new JSONObject().put("action", "edit_bio").put("new_bio", newBio);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equals(resp.optString("status"))) {
anyError = true;
showAlert("Error", resp != null ? resp.optString("message", "Unknown error") : "Unknown error", Alert.AlertType.ERROR);
} else {
currentUser.put("bio", newBio);
}
}
// --- Update profile name ---
String oldName = currentUser.optString("profile_name", "");
if (!newName.equals(oldName)) {
JSONObject req = new JSONObject().put("action", "edit_profile_name").put("new_profile_name", newName);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equals(resp.optString("status"))) {
anyError = true;
showAlert("Error", resp != null ? resp.optString("message", "Unknown error") : "Unknown error", Alert.AlertType.ERROR);
} else {
currentUser.put("profile_name", newName);
}
}
// --- Update user ID (username) ---
String oldUserId = currentUser.optString("user_id", "");
if (!newUserId.equals(oldUserId)) {
JSONObject req = new JSONObject().put("action", "edit_user_id").put("new_user_id", newUserId);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equals(resp.optString("status"))) {
anyError = true;
showAlert("Error", resp != null ? resp.optString("message", "Unknown error") : "Unknown error", Alert.AlertType.ERROR);
} else {
currentUser.put("user_id", newUserId);
}
}
if (!anyError) {
var mp = MyProfileController.getInstance();
if (mp != null) {
mp.setProfileData(
currentUser.optString("profile_name"),
"online",
currentUser.optString("bio"),
currentUser.optString("user_id"),
currentUser.optString("image_url", "/org/to/telegramfinalproject/Avatars/default_user_profile.png")
);
}
if (parentSettings != null) {
parentSettings.populateFromSession();
}
MainController.getInstance().refreshSidebarUserFromSession();
// MainController.getInstance().goBack(overlayBackground);
MainController.getInstance().closeOverlay(editCard.getParent());
}
}
private void showAlert(String title, String message, Alert.AlertType type) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(null); // no big header, just the message
alert.setContentText(message);
// optional: style it to fit your dark/light theme
if (alert.getDialogPane().getScene() != null) {
ThemeManager.getInstance().registerScene(alert.getDialogPane().getScene());
}
alert.showAndWait();
}
private boolean isLikelyImageUrl(String s) {
if (s == null || s.isBlank()) return false;
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();
}
return url;
}
private String extractImageUrl(String raw) {
if (raw == null) return null;
String s = raw.trim();
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;
}
}
@@ -0,0 +1,363 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
public class GroupInfoController {
@FXML private VBox groupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private ImageView groupImage;
@FXML private Label groupName;
@FXML private Label memberCount;
@FXML private Button infoMoreButton;
@FXML private ContextMenu infoMoreMenu;
@FXML private MenuItem addMemberItem;
@FXML private MenuItem manageGroupItem;
@FXML private MenuItem deleteGroupItem;
@FXML private ImageView moreIcon;
@FXML private ImageView manageChannelIcon;
@FXML private ImageView addMemberIcon;
@FXML private VBox membersList;
@FXML private Label membersHeader;
@FXML private Button addMemberButton;
@FXML private ImageView membersIcon;
@FXML private ScrollPane membersScroll;
private String groupId; // the UUID of this group
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML
private void initialize() {
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(groupCard.getParent()));
infoMoreButton.setOnAction(e -> {
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
});
if (manageGroupItem != null) {
manageGroupItem.setOnAction(e -> openManageGroupScene());
}
deleteGroupItem.setOnAction(e -> handleDeleteGroup());
Platform.runLater(() -> {
if (groupCard.getScene() != null) {
ThemeManager.getInstance().registerScene(groupCard.getScene());
}
});
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateIcons(newVal);
});
updateIcons(ThemeManager.getInstance().isDarkMode());
// Smooth scroll feel
membersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
membersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
ScrollBar vBar = (ScrollBar) membersScroll.lookup(".scroll-bar:vertical");
if (vBar != null) {
membersScroll.setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
double newValue = vBar.getValue() - deltaY;
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
});
}
}
});
if (addMemberButton != null) {
addMemberButton.setOnAction(e -> openAddMemberScene());
}
if (addMemberItem != null) {
addMemberItem.setOnAction(e -> openAddMemberScene());
}
}
public void setGroupDataFromJson(ChatEntry entry, JSONObject data) {
this.groupId = data.optString("internal_uuid", entry.getId().toString());
groupName.setText(data.optString("group_name", entry.getName()));
memberCount.setText(data.optInt("member_count", 0) + " members");
// --- Role-based UI ---
String myRole = data.optString("my_role", "member").toLowerCase();
// Delete group only owner
deleteGroupItem.setVisible("owner".equals(myRole));
// Add member (button + menu item) owner or admin
boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
addMemberButton.setVisible(canAdd);
addMemberButton.setManaged(canAdd);
if (addMemberItem != null) {
addMemberItem.setVisible(canAdd);
}
// More menu (3-dot) hide entirely for plain members
boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
infoMoreButton.setVisible(showMore);
infoMoreButton.setManaged(showMore);
// --- Group picture ---
String imgUrl = data.optString("image_url", "");
if (!imgUrl.isBlank()) {
try {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) groupImage.setImage(img);
} catch (Exception ignore) {}
} else {
groupImage.setImage(
new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_group_profile.png"))
);
}
// --- Members list ---
membersList.getChildren().clear();
var arr = data.optJSONArray("members");
if (arr != null) {
membersHeader.setText(arr.length() + " MEMBERS");
for (int i = 0; i < arr.length(); i++) {
JSONObject m = arr.getJSONObject(i);
addMemberRow(m);
}
}
}
private void addMemberRow(JSONObject m) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// === Avatar ===
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
AvatarFX.circleClip(avatar, 36);
String imgUrl = m.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// === Name + Status ===
VBox nameBox = new VBox(2);
Label name = new Label(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
// Status = online / last seen recently
String status;
if (m.optBoolean("is_online", false)) {
status = "online";
} else {
status = ChatPageController.getInstance().userStatusText(
false,
m.optString("last_seen", null)
);
}
Label statusLbl = new Label(status);
statusLbl.getStyleClass().add("member-status");
nameBox.getChildren().addAll(name, statusLbl);
// === Role (owner/admin/member) ===
Label role = new Label();
String roleStr = m.optString("role", "");
if (!roleStr.isBlank()) {
role.setText(roleStr.toLowerCase());
role.getStyleClass().add("member-role");
}
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, nameBox, spacer, role);
membersList.getChildren().add(row);
}
private void openManageGroupScene() {
new Thread(() -> {
try {
JSONObject req = new JSONObject()
.put("action", "view_group")
.put("group_id", groupId)
.put("viewer_id", Session.getUserUUID());
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
resp != null ? resp.optString("message") : "Server not responding.",
Alert.AlertType.ERROR
));
return;
}
JSONObject data = resp.optJSONObject("data");
if (data == null) {
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Malformed server response.",
Alert.AlertType.ERROR
));
return;
}
Platform.runLater(() -> {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_group.fxml"));
Node overlay = loader.load();
ManageGroupController controller = loader.getController();
controller.setGroupData(data); // pass the full JSON
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Manage Group scene.",
Alert.AlertType.ERROR
);
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.runLater(() -> MainController.getInstance().showAlert(
"Error",
"Error while fetching group info: " + e.getMessage(),
Alert.AlertType.ERROR
));
}
}).start();
}
private void openAddMemberScene() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
Node overlay = loader.load();
AddMembersController controller = loader.getController();
controller.setGroupForAdd(UUID.fromString(groupId), String.valueOf(groupName));
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Member scene.",
Alert.AlertType.ERROR
);
}
}
private void handleDeleteGroup() {
ChatEntry entry = Session.currentChatEntry;
// if (entry == null || !"group".equalsIgnoreCase(entry.getType())) {
// alert(Alert.AlertType.INFORMATION, "This action is only available for groups.");
// return;
// }
if (!confirm("Delete Group",
"Are you sure you want to DELETE this group?\nThis action cannot be undone.")) {
return;
}
JSONObject req = new JSONObject()
.put("action", "delete_group")
.put("group_id", entry.getId().toString());
JSONObject res = ActionHandler.sendWithResponse(req);
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
alert(Alert.AlertType.INFORMATION, "✅ Group deleted successfully.");
MainController.getInstance().refreshChatListUI();
AppRouter.showMain();
} else {
String msg = (res != null) ? res.optString("message", "Failed to delete group.") : "null response";
alert(Alert.AlertType.ERROR, "" + msg);
}
}
private boolean confirm(String title, String msg) {
Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL);
a.setTitle(title);
return a.showAndWait().filter(btn -> btn == ButtonType.OK).isPresent();
}
private void alert(Alert.AlertType type, String msg) {
new Alert(type, msg, ButtonType.OK).show();
}
private void updateIcons(boolean dark) {
String suffix = dark ? "_light.png" : "_dark.png";
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
}
// --- helpers -------------------------------------------------------------
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) return null;
return new Image(res.toExternalForm());
}
}
@@ -0,0 +1,151 @@
package org.to.telegramfinalproject.UI;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class IntroController {
@FXML private StackPane imagePane;
@FXML private Label caption;
@FXML private HBox dotContainer;
@FXML private Button startButton;
private List<Image> images = new ArrayList<>();
private List<String> captions = new ArrayList<>();
private int currentIndex = 0;
private final int width = 200;
private final int height = 200;
@FXML
public void initialize() {
loadSlides();
setupDots();
showSlide(currentIndex);
startAutoSlide();
}
private void loadSlides() {
images.add(new Image(getClass().getResource("/org/to/telegramfinalproject/Images/telegram_icon.png").toExternalForm()));
captions.add("🚀 The world's fastest messaging app.\nIt is free and secure.");
images.add(new Image(getClass().getResource("/org/to/telegramfinalproject/Images/fast.png").toExternalForm()));
captions.add("🚀 Telegram delivers messages \nfastest than any other application.");
images.add(new Image(getClass().getResource("/org/to/telegramfinalproject/Images/free.png").toExternalForm()));
captions.add("🎁 Telegram is free forever. No ads.\nNo subscription fees.");
images.add(new Image(getClass().getResource("/org/to/telegramfinalproject/Images/secure.png").toExternalForm()));
captions.add("🔒 Telegram keeps your messages \nsafe from hacker attacks.");
}
private void setupDots() {
dotContainer.getChildren().clear();
for (int i = 0; i < images.size(); i++) {
Circle dot = new Circle(5, Color.LIGHTGRAY);
dotContainer.getChildren().add(dot);
}
}
private void updateDots() {
for (int i = 0; i < dotContainer.getChildren().size(); i++) {
Circle dot = (Circle) dotContainer.getChildren().get(i);
if (i == currentIndex) {
dot.setFill(getSlideColor(i));
} else {
dot.setFill(Color.LIGHTGRAY);
}
}
}
private Color getSlideColor(int index) {
return switch (index) {
case 0 -> Color.DODGERBLUE; // Blue for Telegram logo
case 1 -> Color.FIREBRICK; // Red slide
case 2 -> Color.GOLDENROD; // Yellow slide
case 3 -> Color.SEAGREEN; // Green slide
default -> Color.GRAY;
};
}
private void showSlide(int index) {
imagePane.getChildren().clear();
ImageView imageView = new ImageView(images.get(index));
imageView.setFitWidth(180);
imageView.setFitHeight(180);
imageView.setPreserveRatio(true);
// Clip the image into a circle
Circle clip = new Circle(90, 90, 80); // centerX, centerY, radius
imageView.setClip(clip);
imagePane.getChildren().add(imageView);
caption.setText(captions.get(index));
updateDots();
}
private void startAutoSlide() {
Thread slider = new Thread(() -> {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {}
javafx.application.Platform.runLater(() -> {
currentIndex = (currentIndex + 1) % images.size();
showSlide(currentIndex);
});
}
});
slider.setDaemon(true);
slider.start();
}
//
// @FXML
// private void handleStartMessaging(ActionEvent event) {
// try {
// FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/login_view.fxml"));
// Scene loginScene = new Scene(loader.load());
//
// // Get the current stage and its dimensions
// Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
// double currentWidth = stage.getWidth();
// double currentHeight = stage.getHeight();
//
// // Set the new scene and apply the previous size
// stage.setScene(loginScene);
// stage.setWidth(currentWidth);
// stage.setHeight(currentHeight);
// stage.show();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
@FXML
private void handleStartMessaging(ActionEvent event) {
AppRouter.showLogin();
}
@FXML private void goRegister() { AppRouter.showRegister(); }
}
@@ -0,0 +1,208 @@
package org.to.telegramfinalproject.UI;
import javafx.animation.PauseTransition;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.Parent;
import javafx.scene.control.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.ClientConnection;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Client.TelegramClient;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Models.User;
import org.to.telegramfinalproject.Security.PasswordHashing;
import java.io.IOException;
import static org.to.telegramfinalproject.UI.AppRouter.showRegister;
public class LoginController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
@FXML private TextField visiblePasswordField;
@FXML private Button toggleVisibilityBtn;
@FXML private Label errorLabel;
private ClientConnection connection;
private boolean passwordVisible = false;
@FXML
public void initialize() {
// Sync hidden and visible password fields
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
try {
connection = new ClientConnection("localhost", 8080);
} catch (Exception e) {
System.out.println("Could not connect to server: " + e.getMessage());
}
}
@FXML
private void togglePasswordVisibility() {
passwordVisible = !passwordVisible;
visiblePasswordField.setVisible(passwordVisible);
visiblePasswordField.setManaged(passwordVisible);
passwordField.setVisible(!passwordVisible);
passwordField.setManaged(!passwordVisible);
toggleVisibilityBtn.setText(passwordVisible ? "👁" : "👁");
}
// @FXML
// private void handleLogin() {
// String username = usernameField.getText();
// String password = passwordField.getText();
//
// // 1. Check for empty fields
// if (username.isEmpty() || password.isEmpty()) {
// showError("Please fill in all required fields.");
// return;
// }
//
// // 2. Check if username exists
// userDatabase userDb = new userDatabase();
// if (!userDb.existsByUsername(username)) {
// showError("This username doesnt exist.");
// return;
// }
//
// // 3. Check if password is correct
// User user = userDb.findByUsername(username);
// if (!PasswordHashing.verify(password, user.getPassword())) {
// showError("Incorrect password.");
// return;
// }
//
// // 4. Attempt to send to server
// try {
// JSONObject request = new JSONObject();
// request.put("action", "login");
// request.put("user_id", JSONObject.NULL);
// request.put("username", username);
// request.put("password", password);
// request.put("profile_name", JSONObject.NULL);
//
// if (connection != null) {
// connection.send(request.toString());
// String responseStr = connection.receive();
// JSONObject response = new JSONObject(responseStr);
// System.out.println("Status: " + response.getString("status"));
// System.out.println("Message: " + response.getString("message"));
//
// // Simulate successful login (since main.fxml isnt ready)
// Alert alert = new Alert(Alert.AlertType.INFORMATION, "Login successful!");
// alert.show();
// }
//
// } catch (Exception ex) {
// showError("Unable to connect to server. Please try again later.");
// }
// }
@FXML
private void handleLogin() {
String u = usernameField.getText().trim();
String p = passwordField.getText();
// 1. Check for empty fields
if (u.isEmpty() || p.isEmpty()) {
showError("Please fill in all required fields.");
return;
}
// 2. Check if username exists
userDatabase userDb = new userDatabase();
if (!userDb.existsByUsername(u)) {
showError("This username doesnt exist.");
return;
}
// 3. Check if password is correct
User user = userDb.findByUsername(u);
if (!PasswordHashing.verify(p, user.getPassword())) {
showError("Incorrect password.");
return;
}
setUiBusy(true);
new Thread(() -> {
try {
var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI();
var handler = cli.getHandler();
handler.login(u,p);
javafx.application.Platform.runLater(() -> {
setUiBusy(false);
if (!handler.wasSuccess() || Session.currentUser == null) {
showError(handler.getLastMessage().isEmpty() ? "Login failed." : handler.getLastMessage());
return;
}
goMain();
});
} catch (Exception ex) {
javafx.application.Platform.runLater(() -> {
setUiBusy(false);
showError("Connection error.");
});
}
}, "login-thread").start();
}
private void goMain() {
AppRouter.showMain();
}
private void setUiBusy(boolean b) {
usernameField.setDisable(b);
passwordField.setDisable(b);
}
@FXML
private void switchToRegister() throws IOException {
showRegister();
}
private void switchScene(String fxmlFile) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/" + fxmlFile));
Parent root = loader.load();
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setScene(new Scene(root, 500, 700));
stage.show();
} catch (IOException e) {
e.printStackTrace();
showError("Could not load scene: " + fxmlFile);
}
}
private void showError(String message) {
errorLabel.setVisible(false);
errorLabel.setText(""); // Clear the label text
PauseTransition pause = new PauseTransition(Duration.millis(50));
pause.setOnFinished(event -> {
errorLabel.setText(message);
errorLabel.setStyle("-fx-text-fill: red;");
errorLabel.setVisible(true);
});
pause.play();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,228 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ManageAdminsController {
@FXML private VBox adminsCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addAdminButton;
@FXML private ScrollPane adminsScroll;
@FXML private VBox adminsList;
private String groupId;
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
addAdminButton.setOnAction(e -> openAddAdminOverlay());
// Smooth scroll feel
adminsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
adminsScroll.setPannable(true);
adminsScroll.setFitToWidth(true);
adminsScroll.setFitToHeight(false);
adminsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
adminsScroll.setVvalue(adminsScroll.getVvalue() - deltaY);
});
}
public void setGroupData(String groupId, JSONArray admins) {
this.groupId = groupId;
adminsList.getChildren().clear();
// Convert to list for sorting
List<JSONObject> adminList = new ArrayList<>();
for (int i = 0; i < admins.length(); i++) {
adminList.add(admins.getJSONObject(i));
}
// Sort: owner first, then admins
adminList.sort((a, b) -> {
String roleA = a.optString("role", "member");
String roleB = b.optString("role", "member");
if ("owner".equalsIgnoreCase(roleA) && !"owner".equalsIgnoreCase(roleB)) return -1;
if ("owner".equalsIgnoreCase(roleB) && !"owner".equalsIgnoreCase(roleA)) return 1;
return 0; // keep relative order for admins
});
// Add rows
for (JSONObject a : adminList) {
addAdminRow(a);
}
}
private void addAdminRow(JSONObject a) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = a.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(a.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
a.optBoolean("is_online", false) ? "online"
: "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Remove admin (cannot remove owner)
String role = a.optString("role", "admin");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeAdmin(a.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
adminsList.getChildren().add(row);
// User clicks on a admin --> open set admin permission's scene
row.setOnMouseClicked(e -> {
if (!"owner".equalsIgnoreCase(role)) {
try {
// request current permissions from server
JSONObject req = new JSONObject()
.put("action", "get_group_admin_permissions")
.put("group_id", groupId)
.put("admin_id", a.optString("user_id"));
JSONObject resp = ActionHandler.sendWithResponse(req);
JSONObject oldPerms = (resp != null) ? resp.optJSONObject("permissions") : null;
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/set_group_admin_permissions.fxml"));
Node overlay = loader.load();
SetGroupAdminPermissionsController controller = loader.getController();
controller.setTarget(
UUID.fromString(groupId),
UUID.fromString(a.optString("user_id")),
true,
oldPerms
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
MainController.getInstance().showAlert("Error", "Could not open permissions scene.", Alert.AlertType.ERROR);
}
}
});
}
private void removeAdmin(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_admin_from_group")
.put("group_id", groupId)
.put("target_user_id", userId);
System.out.println("Sending remove_admin_from_group request: " + req.toString(2));
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> adminsList.getChildren().remove(row));
} else {
Platform.runLater(() -> {
row.setDisable(false);
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove admin.") : "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
private void openAddAdminOverlay() {
try {
// === Query the server for full members list ===
JSONObject req = new JSONObject()
.put("action", "view_group_members")
.put("group_id", groupId);
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().showAlert(
"Error",
"Could not fetch members.",
Alert.AlertType.ERROR
);
return;
}
JSONObject data = resp.optJSONObject("data");
JSONArray members = (data != null) ? data.optJSONArray("members") : new JSONArray();
// === Load Add Admins FXML ===
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_admins.fxml"));
Node overlay = loader.load();
AddAdminsController controller = loader.getController();
controller.setGroupData(groupId, members); // send raw members, filtering is done in AddAdminsController
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Add Admins scene.", Alert.AlertType.ERROR
);
}
}
}
@@ -0,0 +1,228 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ManageChannelAdminsController {
@FXML private VBox adminsCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addAdminButton;
@FXML private ScrollPane adminsScroll;
@FXML private VBox adminsList;
private String channelId; // internal_uuid of channel
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
//addAdminButton.setOnAction(e -> openAddAdminOverlay());
// Smooth scroll feel
adminsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
adminsScroll.setPannable(true);
adminsScroll.setFitToWidth(true);
adminsScroll.setFitToHeight(false);
adminsScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
adminsScroll.setVvalue(adminsScroll.getVvalue() - deltaY);
});
}
public void setChannelData(String channelId, JSONArray admins) {
this.channelId = channelId;
adminsList.getChildren().clear();
// Convert to list for sorting
List<JSONObject> adminList = new ArrayList<>();
for (int i = 0; i < admins.length(); i++) {
adminList.add(admins.getJSONObject(i));
}
// Sort: owner first, then admins
adminList.sort((a, b) -> {
String roleA = a.optString("role", "subscriber");
String roleB = b.optString("role", "subscriber");
if ("owner".equalsIgnoreCase(roleA) && !"owner".equalsIgnoreCase(roleB)) return -1;
if ("owner".equalsIgnoreCase(roleB) && !"owner".equalsIgnoreCase(roleA)) return 1;
return 0; // keep relative order for admins
});
// Add rows
for (JSONObject a : adminList) {
addAdminRow(a);
}
}
private void addAdminRow(JSONObject a) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = a.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(a.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
a.optBoolean("is_online", false) ? "online"
: "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Remove admin (cannot remove owner)
String role = a.optString("role", "admin");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeAdmin(a.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
adminsList.getChildren().add(row);
// User clicks an admin open set channel admin permissions scene
row.setOnMouseClicked(e -> {
if (!"owner".equalsIgnoreCase(role)) {
try {
// request current permissions from server
JSONObject req = new JSONObject()
.put("action", "get_channel_admin_permissions")
.put("channel_id", channelId)
.put("admin_id", a.optString("user_id"));
JSONObject resp = ActionHandler.sendWithResponse(req);
JSONObject oldPerms = (resp != null) ? resp.optJSONObject("permissions") : null;
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/set_channel_admin_permissions.fxml"));
Node overlay = loader.load();
// SetChannelAdminPermissionsController controller = loader.getController();
// controller.setTarget(
// UUID.fromString(channelId),
// UUID.fromString(a.optString("user_id")),
// true,
// oldPerms
// );
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
MainController.getInstance().showAlert("Error", "Could not open permissions scene.", Alert.AlertType.ERROR);
}
}
});
}
private void removeAdmin(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_admin_from_channel")
.put("channel_id", channelId)
.put("target_user_id", userId);
System.out.println("Sending remove_admin_from_channel request: " + req.toString(2));
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> adminsList.getChildren().remove(row));
} else {
Platform.runLater(() -> {
row.setDisable(false);
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove admin.") : "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
// private void openAddAdminOverlay() {
// try {
// // === Query the server for full subscribers list ===
// JSONObject req = new JSONObject()
// .put("action", "view_channel_subscribers")
// .put("channel_id", channelId);
//
// JSONObject resp = ActionHandler.sendWithResponse(req);
//
// if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
// MainController.getInstance().showAlert(
// "Error",
// "Could not fetch subscribers.",
// Alert.AlertType.ERROR
// );
// return;
// }
//
// JSONObject data = resp.optJSONObject("data");
// JSONArray subscribers = (data != null) ? data.optJSONArray("subscribers") : new JSONArray();
//
// // === Load Add Admins FXML (same FXML reused) ===
// FXMLLoader loader = new FXMLLoader(getClass().getResource(
// "/org/to/telegramfinalproject/Fxml/add_admins.fxml"));
// Node overlay = loader.load();
//
// AddChannelAdminsController controller = loader.getController();
// controller.setChannelData(channelId, subscribers); // send raw subscribers, filtering is done in AddChannelAdminsController
//
// MainController.getInstance().showOverlay(overlay);
//
// } catch (IOException e) {
// e.printStackTrace();
// MainController.getInstance().showAlert(
// "Error", "Could not load Add Admins scene.", Alert.AlertType.ERROR
// );
// }
// }
}
@@ -0,0 +1,180 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
public class ManageChannelController {
@FXML private VBox manageChannelCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
@FXML private ImageView channelImage;
@FXML private TextField channelNameField;
@FXML private TextField channelIdField;
@FXML private TextArea channelDescriptionField;
@FXML private Label adminCount, subscriberCount;
@FXML private Button manageAdminsButton;
@FXML private Button manageSubscribersButton;
private String channelId; // Internal UUID of the channel
private String originalName;
private String originalDisplayId;
private String originalImageUrl;
private String originalDescription;
private File selectedImageFile;
private JSONObject data;
@FXML
public void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
changePicButton.setOnAction(e -> choosePicture());
saveButton.setOnAction(e -> saveChanges());
manageAdminsButton.setOnAction(e -> openManageAdminsScene(data));
manageSubscribersButton.setOnAction(e -> openManageSubscribersScene(data));
}
public void setChannelData(JSONObject data) {
this.data = data;
this.channelId = data.optString("internal_uuid");
originalName = data.optString("channel_name", "");
originalDisplayId = data.optString("channel_id", "");
originalImageUrl = data.optString("image_url", "");
originalDescription = data.optString("description", "");
channelNameField.setText(originalName);
channelIdField.setText(originalDisplayId);
channelDescriptionField.setText(originalDescription);
if (!originalImageUrl.isBlank()) {
channelImage.setImage(new Image(originalImageUrl, true));
}
adminCount.setText(String.valueOf(countRole(data, "admin") + 1)); // owner + admins
subscriberCount.setText(String.valueOf(countRole(data, "subscriber") + Integer.parseInt(adminCount.getText())));
}
private int countRole(JSONObject channelData, String role) {
var arr = channelData.optJSONArray("subscribers");
if (arr == null) return 0;
int count = 0;
for (int i = 0; i < arr.length(); i++) {
if (role.equalsIgnoreCase(arr.getJSONObject(i).optString("role"))) count++;
}
return count;
}
private void choosePicture() {
FileChooser fc = new FileChooser();
fc.setTitle("Select channel picture");
selectedImageFile = fc.showOpenDialog(manageChannelCard.getScene().getWindow());
if (selectedImageFile != null) {
channelImage.setImage(new Image(selectedImageFile.toURI().toString()));
}
}
private void saveChanges() {
String newName = channelNameField.getText().trim();
String newChannelId = channelIdField.getText().trim();
String newDescription = channelDescriptionField.getText().trim();
String newImageUrl = (selectedImageFile != null)
? selectedImageFile.toURI().toString()
: originalImageUrl;
boolean changed =
!Objects.equals(originalName, newName) ||
!Objects.equals(originalDisplayId, newChannelId) ||
!Objects.equals(originalDescription, newDescription) ||
!Objects.equals(originalImageUrl, newImageUrl);
if (!changed) {
MainController.getInstance().closeOverlay(manageChannelCard.getParent());
return;
}
JSONObject req = new JSONObject()
.put("action", "edit_channel_info")
.put("channel_id", channelId) // internal UUID
.put("new_channel_id", newChannelId)
.put("name", newName)
.put("description", newDescription);
if (newImageUrl != null && !newImageUrl.isBlank()) {
req.put("image_url", newImageUrl);
} else {
req.put("image_url", JSONObject.NULL);
}
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().closeOverlay(manageChannelCard.getParent());
} else {
String msg = (resp != null)
? resp.optString("message", "Failed to update channel")
: "No response from server";
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
a.show();
}
}
private void openManageAdminsScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_channel_admins.fxml"));
Node overlay = loader.load();
ManageChannelAdminsController controller = loader.getController();
// Pass channelId and subscribers list from JSON
controller.setChannelData(
data.optString("internal_uuid"),
data.optJSONArray("subscribers")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Channel Admins scene.", Alert.AlertType.ERROR
);
}
}
private void openManageSubscribersScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_subscribers.fxml"));
Node overlay = loader.load();
ManageSubscribersController controller = loader.getController();
// Pass channelId and subscribers list from the JSON data
controller.setChannelData(
data.optString("internal_uuid"),
data.optJSONArray("subscribers")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Subscribers scene.", Alert.AlertType.ERROR);
}
}
}
@@ -0,0 +1,204 @@
package org.to.telegramfinalproject.UI;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.UUID;
public class ManageGroupController {
@FXML private VBox manageGroupCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
@FXML private ImageView groupImage;
@FXML private TextField groupNameField;
@FXML private Label adminCount, memberCount;
@FXML private TextField groupIdField;
@FXML private Button manageAdminsButton;
@FXML private Button manageMembersButton;
private String groupId; // Internal UUID of the current group
private String originalName;
private String originalGroupId;
private String originalImageUrl;
private File selectedImageFile;
private JSONObject data;
@FXML
public void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageGroupCard.getParent()));
changePicButton.setOnAction(e -> choosePicture());
saveButton.setOnAction(e -> saveChanges());
manageAdminsButton.setOnAction(e -> openManageAdminsScene(data));
manageMembersButton.setOnAction(e -> openManageMembersScene(data));
}
private void openManageAdminsScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_admins.fxml"));
Node overlay = loader.load();
ManageAdminsController controller = loader.getController();
String gid = data.optString("internal_uuid");
JSONArray members = data.optJSONArray("members");
JSONArray admins = new JSONArray();
// filter only admins
if (members != null) {
for (int i = 0; i < members.length(); i++) {
JSONObject m = members.getJSONObject(i);
String role = m.optString("role", "member");
if ("admin".equalsIgnoreCase(role) || "owner".equalsIgnoreCase(role)) {
admins.put(m);
}
}
}
controller.setGroupData(gid, admins);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Admins scene.", Alert.AlertType.ERROR);
}
}
private void openManageMembersScene(JSONObject data) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/manage_members.fxml"));
Node overlay = loader.load();
ManageMembersController controller = loader.getController();
// Pass groupId and members list from the JSON data
controller.setGroupData(
data.optString("internal_uuid"),
data.optJSONArray("members")
);
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Manage Members scene.", Alert.AlertType.ERROR);
}
}
public void setGroupData(JSONObject data) {
this.data = data;
this.groupId = data.optString("internal_uuid");
originalName = data.optString("group_name", "");
originalGroupId = data.optString("group_id", "");
originalImageUrl = data.optString("image_url", "");
groupNameField.setText(originalName);
groupIdField.setText(originalGroupId);
if (!originalImageUrl.isBlank()) {
String uri = resolveLocalUri(originalImageUrl);
if (uri != null) {
groupImage.setImage(new Image(uri, true));
}
}
adminCount.setText(String.valueOf(countRole(data, "admin") + 1));
memberCount.setText(String.valueOf(countRole(data, "member") + Integer.parseInt(adminCount.getText())));
}
private String resolveLocalUri(String raw) {
if (raw == null || raw.isBlank()) return null;
if (raw.startsWith("file:/") || raw.startsWith("http")) return raw;
Path base = Paths.get(System.getProperty("user.dir"), "uploads");
Path resolved = base.resolve(raw.startsWith("/") ? raw.substring(1) : raw)
.normalize();
return resolved.toUri().toString(); // file:///C:/.../uploads/avatars/2025-09-05/...
}
private int countRole(JSONObject groupData, String role) {
var arr = groupData.optJSONArray("members");
if (arr == null) return 0;
int count = 0;
for (int i = 0; i < arr.length(); i++) {
if (role.equalsIgnoreCase(arr.getJSONObject(i).optString("role"))) count++;
}
return count;
}
private void choosePicture() {
FileChooser fc = new FileChooser();
fc.setTitle("Select group picture");
selectedImageFile = fc.showOpenDialog(manageGroupCard.getScene().getWindow());
if (selectedImageFile != null) {
groupImage.setImage(new Image(selectedImageFile.toURI().toString()));
}
}
private void saveChanges() {
String newName = groupNameField.getText().trim();
String newGroupId = groupIdField.getText().trim(); // make sure you added this field
String newImageUrl = (selectedImageFile != null)
? selectedImageFile.toURI().toString()
: originalImageUrl;
// Check if anything actually changed
boolean changed =
!Objects.equals(originalName, newName) ||
!Objects.equals(originalGroupId, newGroupId) ||
!Objects.equals(originalImageUrl, newImageUrl);
if (!changed) {
// Nothing changed just close overlay
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
return;
}
// Build request with all fields (server expects them)
JSONObject req = new JSONObject()
.put("action", "edit_group_info")
.put("group_id", groupId) // internal_uuid
.put("new_group_id", newGroupId) // display id
.put("name", newName);
if (newImageUrl != null && !newImageUrl.isBlank()) {
req.put("image_url", newImageUrl);
} else {
req.put("image_url", JSONObject.NULL);
}
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().closeOverlay(manageGroupCard.getParent());
} else {
String msg = (resp != null)
? resp.optString("message", "Failed to update group")
: "No response from server";
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
a.show();
}
}
}
@@ -0,0 +1,188 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.Comparator;
import java.util.UUID;
public class ManageMembersController {
@FXML private VBox membersCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addMembersButton;
@FXML private ScrollPane membersScroll;
@FXML private VBox membersList;
private String groupId;
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(membersCard.getParent()));
addMembersButton.setOnAction(e -> {
// open add members overlay
openAddMembersOverlay(groupId);
});
// Smooth scroll feel
membersScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
membersScroll.setPannable(true);
membersScroll.setFitToWidth(true);
membersScroll.setFitToHeight(false);
membersScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
membersScroll.setVvalue(membersScroll.getVvalue() - deltaY);
});
}
public void setGroupData(String groupId, JSONArray members) {
this.groupId = groupId;
membersList.getChildren().clear();
// Convert safely into JSONObject list
java.util.List<JSONObject> parsed = new java.util.ArrayList<>();
for (int i = 0; i < members.length(); i++) {
Object raw = members.get(i);
if (raw instanceof JSONObject obj) {
parsed.add(obj);
} else if (raw instanceof java.util.Map<?, ?> map) {
parsed.add(new JSONObject(map));
} else {
System.err.println("Skipping invalid member element at index " + i + ": " + raw);
}
}
// Sort: owners first, then others
parsed.sort((a, b) -> {
boolean aOwner = "owner".equalsIgnoreCase(a.optString("role"));
boolean bOwner = "owner".equalsIgnoreCase(b.optString("role"));
return Boolean.compare(!aOwner, !bOwner); // false < true owner first
});
// Add rows
for (JSONObject m : parsed) {
addMemberRow(m);
}
}
private void addMemberRow(JSONObject m) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = m.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(m.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
m.optBoolean("is_online", false) ? "online"
: "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Only non-owners can be removed
String role = m.optString("role", "member");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeMember(m.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
membersList.getChildren().add(row);
}
private void removeMember(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_member_from_group")
.put("group_id", groupId) // must be the group's internal_uuid
.put("user_id", userId); // target user's internal_uuid
// Disable the button while request is in progress
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
membersList.getChildren().remove(row);
});
} else {
Platform.runLater(() -> {
row.setDisable(false); // re-enable on error
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove member.")
: "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
public void openAddMembersOverlay(String groupId) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
Node overlay = loader.load();
AddMembersController controller = loader.getController();
// Pass the groupId as UUID
controller.setGroupForAdd(UUID.fromString(groupId), "");
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error",
"Could not load Add Members scene.",
Alert.AlertType.ERROR
);
}
}
}
@@ -0,0 +1,160 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import java.io.IOException;
import java.util.UUID;
public class ManageSubscribersController {
@FXML private VBox subscribersCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button closeFooterButton;
@FXML private Button addSubscribersButton;
@FXML private ScrollPane subscribersScroll;
@FXML private VBox subscribersList;
private String channelId; // internal UUID
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
addSubscribersButton.setOnAction(e -> openAddSubscribersOverlay(channelId));
// Smooth scroll
subscribersScroll.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
);
subscribersScroll.setPannable(true);
subscribersScroll.setFitToWidth(true);
subscribersScroll.setFitToHeight(false);
subscribersScroll.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.003;
subscribersScroll.setVvalue(subscribersScroll.getVvalue() - deltaY);
});
}
public void setChannelData(String channelId, JSONArray subscribers) {
this.channelId = channelId;
subscribersList.getChildren().clear();
for (int i = 0; i < subscribers.length(); i++) {
Object raw = subscribers.get(i);
JSONObject sub = (raw instanceof JSONObject)
? (JSONObject) raw
: new JSONObject((java.util.Map<?, ?>) raw);
addSubscriberRow(sub);
}
}
private void addSubscriberRow(JSONObject sub) {
HBox row = new HBox(10);
row.getStyleClass().add("member-row");
row.setAlignment(Pos.CENTER_LEFT);
// Avatar
ImageView avatar = new ImageView();
avatar.setFitWidth(36);
avatar.setFitHeight(36);
avatar.setPreserveRatio(true);
String imgUrl = sub.optString("image_url", "");
if (!imgUrl.isBlank()) {
Image img = AvatarLocalResolver.load(imgUrl);
if (img != null) avatar.setImage(img);
} else {
avatar.setImage(new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
)));
}
// Name + Status
VBox details = new VBox(2);
Label name = new Label(sub.optString("profile_name", "Unknown"));
name.getStyleClass().add("member-name");
Label status = new Label(
sub.optBoolean("is_online", false) ? "online" : "last seen recently"
);
status.getStyleClass().add("member-status");
details.getChildren().addAll(name, status);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
row.getChildren().addAll(avatar, details, spacer);
// Only non-owners can be removed
String role = sub.optString("role", "subscriber");
if (!"owner".equalsIgnoreCase(role)) {
Button removeBtn = new Button("Remove");
removeBtn.getStyleClass().add("link-btn");
removeBtn.setOnAction(e -> removeSubscriber(sub.optString("user_id"), row));
row.getChildren().add(removeBtn);
}
subscribersList.getChildren().add(row);
}
private void removeSubscriber(String userId, HBox row) {
JSONObject req = new JSONObject()
.put("action", "remove_subscriber_from_channel")
.put("channel_id", channelId)
.put("user_id", userId);
row.setDisable(true);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> subscribersList.getChildren().remove(row));
} else {
Platform.runLater(() -> {
row.setDisable(false);
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message", "Failed to remove subscriber.")
: "No response from server.",
ButtonType.OK);
a.show();
});
}
}).start();
}
private void openAddSubscribersOverlay(String channelId) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml"));
Node overlay = loader.load();
AddSubscriberController controller = loader.getController();
controller.setChannelInfo(UUID.fromString(channelId), "", "", null, "");
MainController.getInstance().showOverlay(overlay);
} catch (IOException e) {
e.printStackTrace();
MainController.getInstance().showAlert(
"Error", "Could not load Add Subscribers scene.", Alert.AlertType.ERROR
);
}
}
}
@@ -0,0 +1,27 @@
package org.to.telegramfinalproject.UI;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class MediaPathResolver {
private final Path uploadsRoot;
public MediaPathResolver(Path uploadsRoot) {
this.uploadsRoot = uploadsRoot;
}
public String toFileUri(String raw) {
if (raw == null || raw.isBlank()) return null;
if (raw.startsWith("file:/")) return raw;
Path p;
if (raw.startsWith("/")) {
String sub = raw.substring(1).replace("/", File.separator);
p = uploadsRoot.resolve(sub).normalize();
} else {
p = Paths.get(raw).normalize();
}
return p.toUri().toString();
}
}
@@ -0,0 +1,153 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
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 {
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button editButton;
@FXML private ImageView profileImage;
@FXML private Label profileName;
@FXML private Label profileStatus;
@FXML private Label userBio;
@FXML private Label userId;
@FXML private VBox profileCard;
@FXML private VBox bioBlock;
private String userID;
private static MyProfileController instance;
public MyProfileController() {
instance = this;
}
public static MyProfileController getInstance() { return instance;}
@FXML
private void initialize() {
overlayBackground.setOnMouseClicked(e -> closeProfile());
closeButton.setOnAction(e -> closeProfile());
// Round profile picture
profileImage.setClip(new Circle(40, 40, 40));
// Default avatar
profileImage.setImage(new Image(
Objects.requireNonNull(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"))
));
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (profileCard.getScene() != null) {
ThemeManager.getInstance().registerScene(profileCard.getScene());
}
});
// Listener for theme change
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateEditIcon(newVal);
});
// Set initial state
updateEditIcon(ThemeManager.getInstance().isDarkMode());
// === Open edit profile ===
editButton.setOnAction(e -> openEditProfile());
}
private void closeProfile() {
MainController.getInstance().closeOverlay(profileCard.getParent());
}
// Populate user data from DB or active session
public void setProfileData(String name, String status, String bio, String userId, String imageUrl) {
profileName.setText(name);
profileStatus.setText(status);
if (bio == null || bio.isBlank()) {
bioBlock.setVisible(false);
bioBlock.setManaged(false);
} else {
userBio.setText(bio);
bioBlock.setVisible(true);
bioBlock.setManaged(true);
}
this.userId.setText("@" + userId);
userID = userId;
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 ===
private void updateEditIcon(boolean darkMode) {
String iconPath = darkMode
? "/org/to/telegramfinalproject/Icons/edit_light.png"
: "/org/to/telegramfinalproject/Icons/edit_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(iconPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
editButton.setGraphic(icon);
}
private void openEditProfile() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/edit_profile.fxml"));
Node editOverlay = loader.load();
EditProfileController controller = loader.getController();
// Pass current data (so fields are pre-filled)
controller.setProfileData(
profileName.getText(),
profileStatus.getText(),
bioBlock.isVisible() ? userBio.getText() : null,
userID,
profileImage.getImage()
);
// Show new overlay
MainController.getInstance().showOverlay(editOverlay);
// Close the current My Profile overlay
closeProfile();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
@@ -0,0 +1,216 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.scene.layout.Pane;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
public class NewChannelController {
@FXML private VBox newChannelCard;
@FXML private Pane overlayBackground;
@FXML private TextField channelNameField;
@FXML private Label channelNameLabel;
@FXML private TextArea channelDescField;
@FXML private Label channelDescLabel;
@FXML private Button cameraButton;
@FXML private ImageView cameraIcon;
@FXML private Button cancelButton;
@FXML private Button createButton;
@FXML private StackPane overlayRoot;
@FXML private Label descCounter;
@FXML private Label channelIdLabel;
@FXML private TextField channelIdField;
private File channelImageFile;
@FXML
public void initialize() {
cameraIcon.setImage(new Image(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/camera.png")
));
cameraButton.setOnAction(e -> {
FileChooser chooser = new FileChooser();
chooser.setTitle("Choose Channel Picture");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg")
);
File file = chooser.showOpenDialog(cameraButton.getScene().getWindow());
if (file != null) {
channelImageFile = file;
cameraIcon.setImage(new Image(file.toURI().toString()));
}
});
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayRoot));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(overlayRoot));
createButton.setOnAction(e -> onCreateChannel());
// Limit description
final int MAX_LENGTH = 255;
channelDescField.addEventFilter(javafx.scene.input.KeyEvent.KEY_TYPED, e -> {
if (channelDescField.getText().length() >= MAX_LENGTH) e.consume();
});
channelDescField.textProperty().addListener((obs, oldText, newText) -> {
if (newText.length() > MAX_LENGTH) {
channelDescField.setText(newText.substring(0, MAX_LENGTH));
channelDescField.positionCaret(MAX_LENGTH);
}
int current = channelDescField.getText().length();
descCounter.setText(current + " / " + MAX_LENGTH);
descCounter.setStyle(current == MAX_LENGTH ? "-fx-text-fill: red;" : "");
});
descCounter.setText("0 / 255");
// Reset error when user types again
channelNameField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) {
channelNameField.getStyleClass().remove("error");
channelNameLabel.getStyleClass().remove("error");
}
});
channelIdField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) {
channelIdField.getStyleClass().remove("error");
channelIdLabel.getStyleClass().remove("error");
}
});
Platform.runLater(() -> channelNameField.requestFocus());
Platform.runLater(() -> {
if (newChannelCard.getScene() != null) {
ThemeManager.getInstance().registerScene(newChannelCard.getScene());
}
});
}
private void onCreateChannel() {
String name = nz(channelNameField.getText()).trim();
String dispId = nz(channelIdField.getText()).trim();
String description = nz(channelDescField.getText()).trim();
boolean ok = true;
if (name.isEmpty()) { channelNameField.getStyleClass().add("error"); channelNameLabel.getStyleClass().add("error"); ok = false; }
if (dispId.isEmpty()) { channelIdField.getStyleClass().add("error"); channelIdLabel.getStyleClass().add("error"); ok = false; }
if (!ok) return;
final String me = Session.getUserUUID();
if (me == null || me.isBlank()) {
showToast("Cannot create channel: current user UUID missing.");
return;
}
// در مرحله ساخت، image_url را خالی بفرست؛ بعداً آپلود می‌کنیم
JSONObject req = new JSONObject()
.put("action", "create_channel")
.put("channel_id", dispId)
.put("channel_name", name)
.put("user_id", me)
.put("image_url", (Object) null)
.put("description", description);
createButton.setDisable(true);
new Thread(() -> {
try {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
createButton.setDisable(false);
channelIdField.getStyleClass().add("error");
channelIdLabel.getStyleClass().add("error");
});
return;
}
JSONObject data = resp.optJSONObject("data");
if (data == null) {
Platform.runLater(() -> {
createButton.setDisable(false);
showToast("Create failed: empty data.");
});
return;
}
UUID internalId = UUID.fromString(data.optString("internal_id"));
String returnedName = data.optString("name", name);
String returnedDisp = data.optString("id", dispId);
String returnedImg = data.optString("image_url", "");
// اگر کاربر عکس انتخاب کرده بود: الان آپلود کن (target_type=channel)
if (channelImageFile != null) {
ActionHandler.instance.uploadAvatarFor("channel", internalId, channelImageFile);
if (ActionHandler.instance.wasSuccess()) {
String url = ActionHandler.instance.getLastMessage(); // display_url
if (url != null && !url.isBlank()) {
returnedImg = url;
}
} else {
// اختیاری: پیام خطا را نشان بده
System.out.println("Channel avatar upload failed: " + ActionHandler.instance.getLastMessage());
}
}
String finalImg = returnedImg;
Platform.runLater(() -> {
// 1) کانال را به سایدبار اضافه و انتخاب کن
ChatEntry entry = ChatEntry.fromServer(
internalId, "channel", returnedName, returnedDisp, finalImg,
/*isOwner*/ true, /*isAdmin*/ true
);
MainController.getInstance().addChatAndSelect(entry);
// 2) باز کردن Overlay افزودن سابسکرایبر و بستن این Overlay
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml"));
StackPane addSubsOverlay = loader.load();
AddSubscriberController controller = loader.getController();
controller.setChannelInfo(internalId, returnedName, returnedDisp, channelImageFile, description);
MainController.getInstance().showOverlay(addSubsOverlay);
MainController.getInstance().closeOverlay(overlayRoot);
} catch (IOException ex) {
ex.printStackTrace();
showToast("Failed to open Add Subscribers.");
}
createButton.setDisable(false);
});
} catch (Exception ex) {
Platform.runLater(() -> {
createButton.setDisable(false);
showToast("Create failed: " + ex.getMessage());
});
}
}).start();
}
private void showToast(String msg) {
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(overlayRoot.getScene().getWindow());
a.show();
}
private static String nz(String s){ return s==null? "": s; }
}
@@ -0,0 +1,187 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.scene.layout.Pane;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Client.TelegramClient;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.*;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import static org.to.telegramfinalproject.Client.ActionHandler.detectMime;
public class NewGroupController {
@FXML private VBox newGroupCard;
@FXML private Pane overlayBackground;
@FXML private TextField groupNameField;
@FXML private Label groupNameLabel;
@FXML private Button cameraButton;
@FXML private ImageView cameraIcon;
@FXML private Button cancelButton;
@FXML private Button nextButton;
@FXML private StackPane overlayRoot; // the root
@FXML private TextField groupIdField;
@FXML private Label groupIdLabel;
private File groupImageFile;
@FXML
public void initialize() {
cameraIcon.setImage(new Image(
getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/camera.png")
));
cameraButton.setOnAction(e -> {
FileChooser chooser = new FileChooser();
chooser.setTitle("Choose Group Picture");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg")
);
File file = chooser.showOpenDialog(cameraButton.getScene().getWindow());
if (file != null) {
groupImageFile = file;
cameraIcon.setImage(new Image(file.toURI().toString()));
}
});
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayRoot));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(overlayRoot));
nextButton.setOnAction(e -> onNext());
// clear errors on typing
groupNameField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) {
groupNameField.getStyleClass().remove("error");
groupNameLabel.getStyleClass().remove("error");
}
});
groupIdField.textProperty().addListener((obs, ov, nv) -> {
if (!nv.trim().isEmpty()) {
groupIdField.getStyleClass().remove("error");
groupIdLabel.getStyleClass().remove("error");
}
});
Platform.runLater(() -> groupNameField.requestFocus());
}
private void onNext() {
String groupName = groupNameField.getText() == null ? "" : groupNameField.getText().trim();
String groupId = groupIdField.getText() == null ? "" : groupIdField.getText().trim();
boolean ok = true;
if (groupName.isEmpty()) { groupNameField.getStyleClass().add("error"); groupNameLabel.getStyleClass().add("error"); ok = false; }
if (groupId.isEmpty()) { groupIdField.getStyleClass().add("error"); groupIdLabel.getStyleClass().add("error"); ok = false; }
if (!ok) return;
final String me = Session.getUserUUID();
if (me == null || me.isBlank()) {
showToast("Cannot create group: current user UUID missing.");
return;
}
// در مرحلهٔ ساخت، image_url را خالی بفرست (بعداً آپلود می‌کنیم)
final String imageUrl = null;
JSONObject req = new JSONObject()
.put("action", "create_group")
.put("group_id", groupId)
.put("group_name", groupName)
.put("user_id", me)
.put("image_url", imageUrl);
new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
Platform.runLater(() -> {
groupIdField.getStyleClass().add("error");
groupIdLabel.getStyleClass().add("error");
});
return;
}
JSONObject data = resp.optJSONObject("data");
if (data == null) {
Platform.runLater(() -> showToast("Create failed: empty data."));
return;
}
UUID internalId = UUID.fromString(data.optString("internal_id"));
String returnedName = data.optString("name", groupName);
String returnedDisp = data.optString("id", groupId);
String returnedImg = data.optString("image_url", "");
// اگر عکس انتخاب شده، حالا آپلود کن (target_type=group, target_id=internalId)
String finalImageUrl = returnedImg;
if (groupImageFile != null) {
ActionHandler.instance.uploadAvatarFor("group", internalId, groupImageFile);
if (ActionHandler.instance.wasSuccess()) {
String url = ActionHandler.instance.getLastMessage(); // display_url
if (url != null && !url.isBlank()) {
finalImageUrl = url;
}
}
}
// 1) به سایدبار اضافه و انتخاب کن
String imageUrlToUse = finalImageUrl; // برای استفاده داخل lambda
Platform.runLater(() -> {
ChatEntry entry = ChatEntry.fromServer(
internalId,
"group",
returnedName,
returnedDisp,
imageUrlToUse,
/*isOwner*/ true,
/*isAdmin*/ true
);
MainController.getInstance().addChatAndSelect(entry);
// اگر URL جدید داریم، می‌تونی یک bust-cache بزنی:
// MainController.getInstance().refreshChatAvatar(internalId, imageUrlToUse + "?v=" + System.currentTimeMillis());
});
// 2) پنجره Add Members
Platform.runLater(() -> {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/add_member.fxml"));
StackPane addMembersOverlay = loader.load();
AddMembersController controller = loader.getController();
controller.setGroupInfo(internalId, returnedName, returnedDisp, groupImageFile);
MainController.getInstance().showOverlay(addMembersOverlay);
MainController.getInstance().closeOverlay(overlayRoot);
} catch (IOException ex) {
ex.printStackTrace();
showToast("Failed to open Add Members.");
}
});
}).start();
}
private void showToast(String msg) {
// جایگزینش کن با سیستم نوتی شما
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(overlayRoot.getScene().getWindow());
a.show();
}
}
@@ -0,0 +1,124 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import java.io.IOException;
import java.net.URL;
public class PrivacySecurityController {
@FXML private VBox privacyCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button backButton;
@FXML private Button blockedUsersButton;
@FXML private Button changeCredentialsButton;
@FXML private ImageView blockIcon;
@FXML private ImageView usernameIcon;
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML
public void initialize() {
// Set back button icon
ImageView backIcon = new ImageView(
new Image(getClass().getResourceAsStream("/org/to/telegramfinalproject/Icons/back_button_dark.png"))
);
backIcon.setFitWidth(18);
backIcon.setFitHeight(18);
backButton.setGraphic(backIcon);
changeCredentialsButton.setOnAction(e -> openPasswordCheckOverlay());
// Blocked users management
blockedUsersButton.setOnAction(e -> {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/blocked_users.fxml"));
Node overlay = loader.load();
// Show new overlay
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
}
});
// Close
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(privacyCard.getParent()));
// Back
backButton.setOnAction(e -> MainController.getInstance().goBack(privacyCard));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(privacyCard.getParent()));
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (privacyCard.getScene() != null) {
ThemeManager.getInstance().registerScene(privacyCard.getScene());
}
});
// Listener for theme change
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateIcons(newVal);
});
// Set initial state
updateIcons(ThemeManager.getInstance().isDarkMode());
}
private void openPasswordCheckOverlay() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/check_password.fxml"));
Node overlay = loader.load();
// Show new overlay
MainController.getInstance().showOverlay(overlay);
} catch (IOException ex) {
ex.printStackTrace();
}
}
private void updateIcons(boolean darkMode) {
String suffix = darkMode ? "_light.png" : "_dark.png";
backButton.setGraphic(makeIcon(ICON_PATH + "back_button" + suffix));
blockIcon.setImage(loadImage(ICON_PATH + "hand" + suffix));
usernameIcon.setImage(loadImage(ICON_PATH + "username" + suffix));
}
// --- helpers -------------------------------------------------------------
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) {
System.err.println("Resource not found: " + path);
return null;
}
return new Image(res.toExternalForm());
}
}
@@ -0,0 +1,245 @@
package org.to.telegramfinalproject.UI;
import javafx.animation.PauseTransition;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ClientConnection;
import org.to.telegramfinalproject.Database.userDatabase;
import java.io.IOException;
import static org.to.telegramfinalproject.UI.AppRouter.showLogin;
public class RegisterController {
@FXML private TextField userIdField;
@FXML private TextField usernameField;
@FXML private TextField profileNameField;
@FXML private PasswordField passwordField;
@FXML private TextField visiblePasswordField;
@FXML private PasswordField confirmPasswordField;
@FXML private TextField visibleConfirmPasswordField;
@FXML private Button togglePasswordBtn;
@FXML private Button toggleConfirmBtn;
@FXML private Label errorLabel;
private ClientConnection connection;
private boolean passwordVisible = false;
private boolean confirmVisible = false;
@FXML
public void initialize() {
try {
org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI();
} catch (Exception ignored) {
}
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
visibleConfirmPasswordField.textProperty().bindBidirectional(confirmPasswordField.textProperty());
}
@FXML
private void togglePasswordVisibility() {
passwordVisible = !passwordVisible;
visiblePasswordField.setVisible(passwordVisible);
visiblePasswordField.setManaged(passwordVisible);
passwordField.setVisible(!passwordVisible);
passwordField.setManaged(!passwordVisible);
togglePasswordBtn.setText(passwordVisible ? "👁" : "👁");
}
@FXML
private void toggleConfirmPasswordVisibility() {
confirmVisible = !confirmVisible;
visibleConfirmPasswordField.setVisible(confirmVisible);
visibleConfirmPasswordField.setManaged(confirmVisible);
confirmPasswordField.setVisible(!confirmVisible);
confirmPasswordField.setManaged(!confirmVisible);
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 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";
// اعتبارسنجی‌ها
if (userID.isEmpty() || username.isEmpty() || profileName.isEmpty() || password.isEmpty() || confirmPass.isEmpty()) {
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; }
// UI را موقتاً disable کن (اختیاری)
setBusy(true);
new Thread(() -> {
try {
// 1) مطمئن شو کانکشن/لیسنر روشن است
var cli = org.to.telegramfinalproject.Client.TelegramClient.getOrInitForUI();
var handler = cli.getHandler();
// 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);
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;
}
// 3) Auto-Login با همان کانکشن
handler.login(username, password);
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) {
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();
}
private void switchScene(String fxmlFile) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/" + fxmlFile));
Parent root = loader.load();
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setScene(new Scene(root, 500, 700));
stage.show();
} catch (IOException e) {
e.printStackTrace();
showError("Could not load scene: " + fxmlFile);
}
}
private void showError(String message) {
errorLabel.setVisible(false);
errorLabel.setText(""); // Clear the label text
PauseTransition pause = new PauseTransition(Duration.millis(50));
pause.setOnFinished(event -> {
errorLabel.setText(message);
errorLabel.setStyle("-fx-text-fill: red;"); // 🔴 force red
errorLabel.setVisible(true);
});
pause.play();
}
}
@@ -0,0 +1,148 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import java.util.UUID;
public class SetGroupAdminPermissionsController {
@FXML private VBox permissionsCard;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
@FXML private Button cancelButton;
@FXML private Button saveButton;
@FXML private CheckBox canAddMember;
@FXML private CheckBox canRemoveMember;
@FXML private CheckBox canAddAdmin;
@FXML private CheckBox canRemoveAdmin;
@FXML private CheckBox canEditGroup;
private UUID groupId;
private UUID userId;
private Runnable onSuccess;
private JSONObject oldPermissions; // store existing permissions
private boolean isEditMode = false; // true if editing existing admin
public void setTarget(UUID groupId, UUID userId, boolean isEditMode) {
this.groupId = groupId;
this.userId = userId;
this.isEditMode = isEditMode;
if (isEditMode) {
loadCurrentPermissions();
}
}
public void setTarget(UUID groupId, UUID userId, boolean isEditMode, JSONObject oldPermissions) {
this.groupId = groupId;
this.userId = userId;
this.isEditMode = isEditMode;
this.oldPermissions = oldPermissions;
}
public void setOnSuccess(Runnable r) {
this.onSuccess = r;
}
@FXML
private void initialize() {
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(permissionsCard.getParent()));
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(permissionsCard.getParent()));
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(permissionsCard.getParent()));
saveButton.setOnAction(e -> savePermissions());
// Register for dark mode updates
Platform.runLater(() -> {
if (permissionsCard.getScene() != null) {
ThemeManager.getInstance().registerScene(permissionsCard.getScene());
}
});
}
private void loadCurrentPermissions() {
JSONObject req = new JSONObject()
.put("action", "get_group_admin_permissions")
.put("group_id", groupId.toString())
.put("admin_id", userId.toString());
JSONObject resp = ActionHandler.sendWithResponse(req);
System.out.println("Permissions response: " + resp); // full server response
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
// "data" is an object containing { "permissions": { ... } }
JSONObject data = resp.optJSONObject("data");
System.out.println("Data object: " + data);
if (data != null) {
JSONObject perms = data.optJSONObject("permissions");
System.out.println("Extracted permissions object: " + perms);
if (perms != null) {
oldPermissions = perms;
// Update checkboxes safely on FX thread
Platform.runLater(() -> {
canAddMember.setSelected(perms.optBoolean("can_add_member", false));
canRemoveMember.setSelected(perms.optBoolean("can_remove_member", false));
canAddAdmin.setSelected(perms.optBoolean("can_add_admin", false));
canRemoveAdmin.setSelected(perms.optBoolean("can_remove_admin", false));
canEditGroup.setSelected(perms.optBoolean("can_edit_group", false));
});
} else {
System.out.println("⚠️ Permissions object was null inside data.");
}
} else {
System.out.println("⚠️ Data object was null in response.");
}
} else {
System.out.println("⚠️ Response was null or status != success");
}
}
private void savePermissions() {
JSONObject newPermissions = new JSONObject()
.put("can_add_member", canAddMember.isSelected())
.put("can_remove_member", canRemoveMember.isSelected())
.put("can_add_admin", canAddAdmin.isSelected())
.put("can_remove_admin", canRemoveAdmin.isSelected())
.put("can_edit_group", canEditGroup.isSelected());
JSONObject req;
if (isEditMode) {
// --- Editing existing admin permissions ---
req = new JSONObject()
.put("action", "edit_admin_permissions")
.put("chat_id", groupId.toString())
.put("chat_type", "group") // since this controller is only for groups
.put("admin_id", userId.toString())
.put("permissions", newPermissions);
} else {
// --- Adding a new admin ---
req = new JSONObject()
.put("action", "add_admin_to_group")
.put("group_id", groupId.toString())
.put("user_id", userId.toString())
.put("permissions", newPermissions);
}
JSONObject resp = ActionHandler.sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
MainController.getInstance().closeOverlay(permissionsCard.getParent());
if (onSuccess != null) onSuccess.run(); // update UI after success
} else {
Alert a = new Alert(Alert.AlertType.ERROR,
resp != null ? resp.optString("message") : "Failed to save admin permissions.",
ButtonType.OK);
a.show();
}
}
}
@@ -0,0 +1,265 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session;
import java.io.IOException;
import java.net.URL;
public class SettingsController {
@FXML private VBox settingsCard;
@FXML private ImageView profileImage;
@FXML private Label profileName;
@FXML private Label profileId;
@FXML private MenuButton menuButton;
@FXML private MenuItem editProfileItem;
@FXML private MenuItem logoutItem;
@FXML private Button myAccountButton;
@FXML private Button privacyButton;
@FXML private Button faqButton;
@FXML private Button featuresButton;
@FXML private Pane overlayBackground;
@FXML private Button closeButton;
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
private Image cachedAvatar;
private String cachedName = "";
private String cachedUsername = "";
private String cachedStatus = "";
private String cachedBio = "";
private static String nz(String s){ return s==null? "": s.trim(); }
private static boolean hasVal(String s){ return s!=null && !s.trim().isEmpty() && !"null".equalsIgnoreCase(s); }
private static SettingsController instance;
public static SettingsController getInstance() { return instance; }
@FXML
public void initialize() {
instance = this;
editProfileItem.setOnAction(e -> openEditProfile());
logoutItem.setOnAction(e -> onLogoutClicked());
populateFromSession();
// Make it circular
Circle clip = new Circle(40, 30, 38); // centerX, centerY, radius
profileImage.setClip(clip);
// Buttons
myAccountButton.setOnAction(e -> openEditProfile());
privacyButton.setOnAction(e ->
{
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/privacy_security.fxml"));
Node privacyOverlay = loader.load();
MainController.getInstance().showOverlay(privacyOverlay);
} catch (IOException ex) {
ex.printStackTrace();
}
});
faqButton.setOnAction(e -> System.out.println("Telegram Q&A"));
featuresButton.setOnAction(e -> System.out.println("Telegram Features"));
// Close overlay on background click
overlayBackground.setOnMouseClicked(e ->
MainController.getInstance().closeOverlay(settingsCard.getParent()));
closeButton.setOnAction(e ->
MainController.getInstance().closeOverlay(settingsCard.getParent())
);
// Register scene for ThemeManager stylesheet swap will handle colors/icons
Platform.runLater(() -> {
if (settingsCard.getScene() != null) {
ThemeManager.getInstance().registerScene(settingsCard.getScene());
}
});
// Listener for theme change
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
updateEditIcon(newVal);
updateMoreIcon(newVal);
updateButtonsIcons(newVal);
});
// Set initial state
updateEditIcon(ThemeManager.getInstance().isDarkMode());
updateMoreIcon(ThemeManager.getInstance().isDarkMode());
updateButtonsIcons(ThemeManager.getInstance().isDarkMode());
}
private void openEditProfile() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/edit_profile.fxml"));
Node editOverlay = loader.load();
EditProfileController controller = loader.getController();
controller.setProfileData(
cachedName,
cachedStatus,
cachedBio,
cachedUsername,
cachedAvatar
);
controller.setParentSettings(this);
MainController.getInstance().showOverlay(editOverlay);
} catch (IOException ex) {
ex.printStackTrace();
}
}
private void updateEditIcon(boolean darkMode) {
String editPath = darkMode
? "/org/to/telegramfinalproject/Icons/edit_light.png"
: "/org/to/telegramfinalproject/Icons/edit_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(editPath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
editProfileItem.setGraphic(icon);
}
private void updateMoreIcon(boolean darkMode) {
String morePath = darkMode
? "/org/to/telegramfinalproject/Icons/more_light.png"
: "/org/to/telegramfinalproject/Icons/more_dark.png";
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream(morePath)));
icon.setFitWidth(16);
icon.setFitHeight(16);
menuButton.setGraphic(icon);
}
private void updateButtonsIcons(boolean darkMode) {
String suffix = darkMode ? "_light.png" : "_dark.png";
myAccountButton.setGraphic(makeIcon(ICON_PATH + "my_profile" + suffix));
privacyButton.setGraphic(makeIcon(ICON_PATH + "lock" + suffix));
faqButton.setGraphic(makeIcon(ICON_PATH + "telegram_features" + suffix));
featuresButton.setGraphic(makeIcon(ICON_PATH + "telegram_qna" + suffix));
}
// --- helpers -------------------------------------------------------------
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) {
System.err.println("Resource not found: " + path);
return null;
}
return new Image(res.toExternalForm());
}
void populateFromSession() {
var u = org.to.telegramfinalproject.Client.Session.currentUser;
if (u == null) return;
// نام
String name = nz(u.optString("profile_name",
u.optString("name",
u.optString("first_name",""))));
// یوزرنیم/آی‌دی نمایشی
String handle = nz(u.optString("username",
u.optString("display_id",
u.optString("user_name",""))));
// وضعیت
String status = u.optBoolean("online", false) ? "online" : "online";
String lastSeen = nz(u.optString("last_seen", ""));
if (!u.optBoolean("online", false) && hasVal(lastSeen)) status = "online"; // ساده
// بیو (اگر داری)
String bio = nz(u.optString("bio",""));
// آواتار
Image avatar = null;
String imageUrl = nz(u.optString("image_url",""));
if (hasVal(imageUrl)) {
try { avatar = org.to.telegramfinalproject.Client.AvatarLocalResolver.load(imageUrl); }
catch (Exception ignore) {}
}
if (avatar == null) {
avatar = new Image(getClass().getResourceAsStream(
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"));
}
// کش محلی برای استفاده در صفحه‌ی ویرایش
cachedName = hasVal(name) ? name : "User";
cachedUsername = hasVal(handle) ? handle : "";
cachedStatus = status;
cachedBio = bio;
cachedAvatar = avatar;
// ست کردن در UI
profileName.setText(cachedName);
profileId.setText("@" + cachedUsername);
profileImage.setImage(cachedAvatar);
// گرد کردن تصویر (با توجه به اندازه‌ی واقعی)
Circle clip = new Circle(40, 30, 38); // centerX, centerY, radius
profileImage.setClip(clip);
}
// جایی مثل SidebarMenuController یا MainController
private void onLogoutClicked() {
new Thread(() -> {
JSONObject req = new JSONObject().put("action","logout"); // user_id لازم نیست
req.put("user_id",Session.getUserUUID()); // user_id لازم نیست
JSONObject res = ActionHandler.sendWithResponse(req);
Platform.runLater(() -> {
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
try {
// قطع ارتباط/لیسنر (اگر متد داری)
// TelegramClient.disconnect();
} catch (Exception ignore) {}
// پاک‌سازی امن سشن (ترجیحاً clear به‌جای null)
Session.currentUser = null;
Session.chatList = null;
AppRouter.showIntro(); // intro.fxml
} else {
new Alert(Alert.AlertType.ERROR,
"Logout not successful: " + (res != null ? res.optString("message") : "No response")
).showAndWait();
}
});
}).start();
}
}
@@ -0,0 +1,444 @@
package org.to.telegramfinalproject.UI;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
public class SidebarMenuController {
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
@FXML private VBox sidebarRoot;
@FXML private ImageView profileImage;
@FXML private Label usernameLabel;
@FXML private Button myProfileButton;
@FXML private Button newGroupButton;
@FXML private Button newChannelButton;
@FXML private Button contactsButton;
@FXML private Button savedMessagesButton;
@FXML private Button settingsButton;
@FXML private Button telegramFeaturesButton;
@FXML private Button telegramQnAButton;
// Custom Telegram-style toggle
@FXML private HBox nightModeToggle;
@FXML private Region toggleThumb;
@FXML private ImageView nightModeIcon;
// convenience handle
private final ThemeManager themeManager = ThemeManager.getInstance();
@FXML
public void initialize() {
// Load default profile image
Image profile = loadImage("/org/to/telegramfinalproject/Avatars/default_user_profile.png");
if (profile != null) profileImage.setImage(profile);
AvatarFX.circleClip(profileImage, 56);
setupButtonActions();
setupToggleAction();
// When the Scene is ready:
sidebarRoot.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
// 1) Register with ThemeManager so this scene auto-updates on theme changes
themeManager.registerScene(newScene);
// 2) Make sure we start in LIGHT mode
themeManager.setDarkMode(false);
// 3) Sync icons & toggle with current mode
boolean dark = themeManager.isDarkMode();
updateIcons(dark);
syncToggleVisual(dark, /*animate=*/true);
// Ensure the thumb is correctly positioned after layout pass
Platform.runLater(() -> syncToggleVisual(themeManager.isDarkMode(), false));
}
});
// If theme is changed from somewhere else (another screen), keep sidebar in sync
themeManager.darkModeProperty().addListener((o, wasDark, isDark) -> {
updateIcons(isDark);
syncToggleVisual(isDark, /*animate=*/true);
});
}
private void setupButtonActions() {
myProfileButton.setOnAction(e -> openMyProfile());
newGroupButton.setOnAction(e -> createNewGroup());
newChannelButton.setOnAction(e -> createNewChannel());
contactsButton.setOnAction(e -> openContacts());
savedMessagesButton.setOnAction(e -> openSavedMessages());
settingsButton.setOnAction(e -> openSettings());
telegramFeaturesButton.setOnAction(e -> openTelegramFeatures());
telegramQnAButton.setOnAction(e -> openTelegramQnA());
}
private void setupToggleAction() {
nightModeToggle.setOnMouseClicked(e -> {
boolean newDark = !themeManager.isDarkMode();
// Update the global theme via ThemeManager
themeManager.setDarkMode(newDark);
themeManager.applyThemeToAll();
// Animate the thumb to its new position (listener will handle icons & final position sync)
syncToggleVisual(newDark, /*animate=*/true);
});
}
/** Update all button icons to match theme.
* darkMode == true => white icons => use *_light.png
* darkMode == false => dark icons => use *_dark.png
*/
private void updateIcons(boolean darkMode) {
String suffix = darkMode ? "_light.png" : "_dark.png";
myProfileButton.setGraphic(makeIcon(ICON_PATH + "my_profile" + suffix));
newGroupButton.setGraphic(makeIcon(ICON_PATH + "new_group" + suffix));
newChannelButton.setGraphic(makeIcon(ICON_PATH + "new_channel" + suffix));
contactsButton.setGraphic(makeIcon(ICON_PATH + "contacts" + suffix));
savedMessagesButton.setGraphic(makeIcon(ICON_PATH + "saved_messages" + suffix));
settingsButton.setGraphic(makeIcon(ICON_PATH + "settings" + suffix));
telegramFeaturesButton.setGraphic(makeIcon(ICON_PATH + "telegram_features" + suffix));
telegramQnAButton.setGraphic(makeIcon(ICON_PATH + "telegram_qna" + suffix));
// Night-mode moon icon
Image moon = loadImage(ICON_PATH + "night_mode" + suffix);
if (moon != null) nightModeIcon.setImage(moon);
}
/** Keep the toggles CSS class and thumb position in sync with the current mode. */
private void syncToggleVisual(boolean darkMode, boolean animate) {
// CSS class "on" on the track
if (darkMode) {
if (!nightModeToggle.getStyleClass().contains("on")) {
nightModeToggle.getStyleClass().add("on");
}
} else {
nightModeToggle.getStyleClass().remove("on");
}
// Compute target X for the thumb
double offX = 2;
double onX = Math.max(2, nightModeToggle.getWidth() - toggleThumb.getWidth() - 4);
double targetX = darkMode ? onX : offX;
if (animate) {
TranslateTransition tt = new TranslateTransition(Duration.millis(200), toggleThumb);
tt.setToX(targetX);
tt.play();
} else {
toggleThumb.setTranslateX(targetX);
}
}
// --- helpers -------------------------------------------------------------
private Image loadImage(String path) {
URL res = getClass().getResource(path);
if (res == null) {
System.err.println("Resource not found: " + path);
return null;
}
return new Image(res.toExternalForm());
}
private ImageView makeIcon(String path) {
ImageView iv = new ImageView();
Image img = loadImage(path);
if (img != null) {
iv.setImage(img);
iv.setFitWidth(22);
iv.setFitHeight(22);
iv.setPreserveRatio(true);
}
return iv;
}
// Sidebar actions
private void openMyProfile() {
try {
// 1. Ask server for profile info
JSONObject request = new JSONObject().put("action", "get_user_profile");
JSONObject response = ActionHandler.sendWithResponse(request);
if (response == null || !"success".equals(response.optString("status"))) {
showAlert("Error", "Failed to load profile", Alert.AlertType.ERROR);
return;
}
JSONObject profile = response.optJSONObject("data");
if (profile == null) {
showAlert("Error", "Malformed profile data", Alert.AlertType.ERROR);
return;
}
// Extract fields
String profileName = profile.optString("profile_name", "Unknown");
String bio = profile.optString("bio", "");
String userId = profile.optString("user_id", "");
String imageUrl = profile.optString("profile_picture_url", null);
// 2. Load overlay FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/my_profile.fxml"));
Node profileOverlay = loader.load();
// 3. Pass real data to controller
MyProfileController controller = loader.getController();
controller.setProfileData(profileName, "online", bio, userId, imageUrl);
// 4. Show overlay
MainController.getInstance().showOverlay(profileOverlay);
} catch (IOException e) {
e.printStackTrace();
showAlert("Error", "Error opening profile", Alert.AlertType.ERROR);
}
}
private void createNewGroup() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/new_group.fxml"));
Node groupOverlay = loader.load();
// Show overlay (like MyProfile, Contacts, etc.)
MainController.getInstance().showOverlay(groupOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void createNewChannel() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/new_channel.fxml"));
Node channelOverlay = loader.load();
// Show overlay (like MyProfile, Contacts, etc.)
MainController.getInstance().showOverlay(channelOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openContacts() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/contacts.fxml"));
Node contactsOverlay = loader.load();
// Show overlay on top of everything
MainController.getInstance().showOverlay(contactsOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openSavedMessages() {
try {
// 1) درخواست به سرور
JSONObject req = new JSONObject().put("action", "get_or_create_saved_messages");
JSONObject res = ActionHandler.sendWithResponse(req);
if (res == null || !"success".equals(res.optString("status"))) {
String msg = (res != null) ? res.optString("message", "Unknown error") : "No response";
showAlert("Saved Messages", "Failed: " + msg, Alert.AlertType.ERROR);
return;
}
JSONObject data = res.optJSONObject("data");
if (data == null) {
showAlert("Saved Messages", "Malformed response.", Alert.AlertType.ERROR);
return;
}
// 2) داده‌ها
String chatIdStr = data.optString("chat_id", null);
if (chatIdStr == null || chatIdStr.isBlank()) {
showAlert("Saved Messages", "Missing chat_id.", Alert.AlertType.ERROR);
return;
}
UUID chatId = UUID.fromString(chatIdStr);
String name = data.optString("name", "Saved Messages");
String chatType = data.optString("chat_type", "private");
boolean isSaved = data.optBoolean("is_saved_messages", true);
// اگر آیکن اختصاصی برای Saved داری، این مسیر رو بده؛ وگرنه null بذار:
String savedIcon = ICON_PATH + (themeManager.isDarkMode() ? "saved_messages_light.png" : "saved_messages_dark.png");
// اگر چنین آیکنی نداری، می‌تونی null بدی تا همان image_url خالی بماند:
// String savedIcon = null;
// 3) درج/به‌روزرسانی در Session و بیاور اول لیست
ChatEntry entry = org.to.telegramfinalproject.Client.Session
.upsertSavedMessages(chatId, name, chatType, savedIcon);
// 4) وضعیت‌های فعلی سشن برای ناوبری
org.to.telegramfinalproject.Client.Session.currentChatId = chatId.toString();
org.to.telegramfinalproject.Client.Session.currentChatType = chatType;
org.to.telegramfinalproject.Client.Session.currentChatEntry = entry;
org.to.telegramfinalproject.Client.Session.backToChatList = false;
// 5) ریفِرش ظاهری لیست چت‌ها (اگر متدی برای این داری، صدا بزن)
try {
MainController.getInstance().refreshChatListUI(); // اگر متد دیگری داری عوضش کن
} catch (Throwable ignore) { }
// 6) باز کردن چت
// --- مسیر اول: اگر متدی داری که با ChatEntry باز می‌کند:
boolean opened = false;
try {
MainController.getInstance().openChat(entry);
MainController.getInstance().closeSidebar();
opened = true;
} catch (Throwable t) {
// مسیر دوم: اگر با id/type باز می‌کنی، یا اول info می‌گیری:
try {
ActionHandler.requestChatInfo(String.valueOf(chatId), chatType);
// اگر متد آشکار برای باز کردن با id داری، اینجا صدا بزن:
// MainController.getInstance().openChatById(chatId, chatType);
opened = true;
} catch (Throwable t2) {
t2.printStackTrace();
}
}
if (!opened) {
// اگر هیچ‌کدام نداشت، حداقل پیغام بده که چت ساخته و آماده است:
System.out.println("Saved Messages ready. Open manually with currentChatId/currentChatType.");
}
} catch (Exception ex) {
ex.printStackTrace();
showAlert("Saved Messages", "Error: " + ex.getMessage(), Alert.AlertType.ERROR);
}
}
private void openSettings() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/settings.fxml"));
Node settingsOverlay = loader.load();
// Show overlay on top of everything
MainController.getInstance().showOverlay(settingsOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openTelegramFeatures() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/telegram_features.fxml"));
Node featuresOverlay = loader.load();
MainController.getInstance().showOverlay(featuresOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openTelegramQnA() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/to/telegramfinalproject/Fxml/telegram_qna.fxml"));
Node qnaOverlay = loader.load();
MainController.getInstance().showOverlay(qnaOverlay);
} catch (IOException e) {
e.printStackTrace();
}
}
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/default_user_profile.png");
}
if (pic != null) profileImage.setImage(pic);
}
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);
// اگر مسیر نسبی سرور (مثل /avatars/...) است
String fileUri = AvatarLocalResolver.resolve(src);
if (fileUri != null) return new Image(fileUri, true);
// در غیر این صورت، فرض URL کامل
return new Image(src, true);
} catch (Exception ignored) {
return null;
}
}
private void showAlert(String title, String message, Alert.AlertType type) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(null); // no big header, just the message
alert.setContentText(message);
// optional: style it to fit your dark/light theme
if (alert.getDialogPane().getScene() != null) {
ThemeManager.getInstance().registerScene(alert.getDialogPane().getScene());
}
alert.showAndWait();
}
}
@@ -0,0 +1,68 @@
package org.to.telegramfinalproject.UI;//package org.to.telegramfinalproject.UI;
//
//
//import javafx.application.Application;
//import javafx.fxml.FXMLLoader;
//import javafx.scene.Scene;
//import javafx.scene.image.Image;
//import javafx.stage.Stage;
//
//import java.io.IOException;
//
//public class TelegramApplication extends Application {
// @Override
// public void start(Stage stage) throws IOException {
// FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/main.fxml"));
// Scene scene = new Scene(fxmlLoader.load(), 1480, 820);
//
// scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm());
//
// stage.setTitle("Telegram");
// stage.setScene(scene);
//
// // Add icon to the stage
// Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png"));
// stage.getIcons().add(icon);
//
// stage.show();
// }
// public static void main(String[] args) {
// launch();
// }
//
//}
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.to.telegramfinalproject.UI.AppRouter;
import java.io.IOException;
public class TelegramApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
// First: intro.fxml
FXMLLoader fx = new FXMLLoader(
TelegramApplication.class.getResource("/org/to/telegramfinalproject/Fxml/intro.fxml"));
Scene scene = new Scene(fx.load(), 1480, 820);
scene.getStylesheets().add(
getClass().getResource("/org/to/telegramfinalproject/CSS/light_theme.css").toExternalForm()
);
stage.setTitle("Telegram");
stage.getIcons().add(new Image(
TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png")
));
stage.setScene(scene);
stage.show();
AppRouter.init(stage, scene);
}
public static void main(String[] args) { launch(); }
}

Some files were not shown because too many files have changed in this diff Show More