From 9c61b27004c31623deadf62c744c099a5f6eba2f Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 4 Sep 2025 20:45:38 +0330 Subject: [PATCH] Profile logic for UI --- .../Client/ActionHandler.java | 408 +++++++++++++++++- .../Client/IncomingMessageListener.java | 33 +- .../Client/TelegramClient.java | 21 +- .../Database/ChannelDatabase.java | 52 ++- .../Server/ChannelService.java | 4 +- .../Server/ClientHandler.java | 12 +- .../UI/ChatItemController.java | 24 ++ .../UI/ChatPageController.java | 35 ++ .../UI/MainController.java | 3 + .../UI/NewChannelController.java | 139 +++--- .../UI/NewGroupController.java | 58 ++- 11 files changed, 674 insertions(+), 115 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 1f3845a..04b128b 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1,5 +1,6 @@ package org.to.telegramfinalproject.Client; +import javafx.application.Platform; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Database.PrivateChatDatabase; @@ -9,10 +10,12 @@ import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchResultModel; -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.PrintWriter; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; @@ -4227,4 +4230,401 @@ public class ActionHandler { } + public void sendMessageInteractive(UUID receiverId, String receiverType) { + Scanner sc = new Scanner(System.in); + + System.out.print("Type (TEXT / IMAGE / AUDIO): "); + String type = sc.nextLine().trim().toUpperCase(); + while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { + System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); + type = sc.nextLine().trim().toUpperCase(); + } + + System.out.print("Text (optional for media; empty = no caption): "); + String text = sc.nextLine(); + + if ("TEXT".equals(type)) { + sendTextMessage(receiverId, receiverType, text); + } else { + System.out.print("File path: "); + String path = sc.nextLine().trim(); + File f = new File(path); + if (!f.isFile()) { + System.out.println("❌ File not found"); + return; + } + try { + sendMediaMessage(receiverId, receiverType, type, f, text); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ Media send failed: " + e.getMessage()); + } + } + } + + private void sendTextMessage(UUID receiverId, String receiverType, String content) { + JSONObject req = new JSONObject() + .put("action", "send_message") + .put("receiver_type", receiverType) + .put("receiver_id", receiverId.toString()) + .put("message_type", "TEXT") + .put("content", content == null ? "" : content); + + JSONObject resp = sendWithResponse(req); + if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { + System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id","")); + } else { + System.out.println("❌ Failed: " + (resp != null ? resp.optString("message") : "no response")); + } + } + + + public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { + if (file == null) { + System.out.println("❌ File is null"); + return; + } + if (!file.exists()) { + System.out.println("❌ File not found: " + file.getAbsolutePath()); + return; + } + if (file.isDirectory()) { + System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath()); + return; + } + + final UUID messageId = UUID.randomUUID(); + + try { + String mime = detectMime(file, type.toUpperCase()); + if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; + + JSONObject header = new JSONObject() + .put("message_id", messageId.toString()) + .put("sender_id", TelegramClient.loggedInUserId.toString()) + .put("receiver_type", receiverType) // private|group|channel + .put("receiver_id", receiverId.toString()) + .put("message_type", type.toUpperCase()) // IMAGE | AUDIO + .put("file_name", file.getName()) + .put("mime_type", mime) + .put("text", caption == null ? "" : caption); + + byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + long contentLen = file.length(); + + BlockingQueue q = new LinkedBlockingQueue<>(1); + TelegramClient.pendingResponses.put(messageId.toString(), q); + + try { + + outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); + outBin.flush(); + + // 2) binary frame: magic + headerLen + header + contentLen + content + outBin.writeInt(0x4D444D31); // "MDM1" + outBin.writeInt(headerBytes.length); // headerLen (int) + outBin.write(headerBytes); // header + outBin.writeLong(contentLen); // contentLen (long) + + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int n; + while ((n = fis.read(buf)) != -1) { + outBin.write(buf, 0, n); + } + } + outBin.flush(); + + JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); + if (ack == null) { + System.out.println("❌ Media ACK timeout for " + messageId); + return; + } + + String status = ack.optString("status", "error"); + if ("success".equalsIgnoreCase(status)) { + System.out.println("✅ Media sent. id=" + ack.optString("message_id") + + " url=" + ack.optString("file_url")); + } else { + System.out.println("❌ Media failed: " + ack.optString("message")); + } + + } finally { + TelegramClient.pendingResponses.remove(messageId.toString()); + } + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ sendMediaMessage error: " + e.getMessage()); + } + } + + + public static String detectMime(File f, String typeUpper /* IMAGE or AUDIO */) { + try { + String m = java.nio.file.Files.probeContentType(f.toPath()); + if (m != null) return m; + } catch (Exception ignored) {} + String name = f.getName().toLowerCase(); + if (name.endsWith(".png")) return "image/png"; + if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg"; + if (name.endsWith(".gif")) return "image/gif"; + if (name.endsWith(".mp3")) return "audio/mpeg"; + if (name.endsWith(".wav")) return "audio/wav"; + if (name.endsWith(".ogg")) return "audio/ogg"; + return typeUpper.equals("IMAGE") ? "image/*" : "audio/*"; + } + + + + private static String safeName(String s) { + if (s == null) return "unknown"; + s = s.replace("\\", "/"); + if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); + s = s.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); + if (s.isEmpty() || s.equals(".") || s.equals("..")) s = "unknown"; + return s; + } + + private static String accountFolderName() { + JSONObject me = Session.currentUser; + String acc = me.optString("username", + me.optString("user_id", + me.optString("profile_name", + me.optString("internal_uuid", "me")))); + return safeName(acc); + } + + private static String chatFolderName(ChatEntry chat) { + String name = chat.getName(); + if (name == null || name.isBlank()) { + name = chat.getDisplayId() != null && !chat.getDisplayId().isBlank() + ? chat.getDisplayId() + : String.valueOf(chat.getId()); + } + return safeName(name); + } + + + public void uploadAvatar(File file) { + if (file == null || !file.exists() || !file.isFile()) { + System.out.println("❌ Invalid file"); + return; + } + + final UUID requestId = UUID.randomUUID(); + + try { + String mime = detectMime(file, "IMAGE"); + if (mime == null) mime = "image/*"; + + // header مثل مدیا، ولی برای آواتار + JSONObject header = new JSONObject() + .put("message_id", requestId.toString()) // برای مچ ACK + .put("target_type", "user") // یا channel/group + // .put("target_id", "…") // اگر channel/group بود + .put("file_name", file.getName()) + .put("mime_type", mime); + + byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + long contentLen = file.length(); + + // صف پاسخ (مثل sendMediaMessage) + BlockingQueue q = new LinkedBlockingQueue<>(1); + TelegramClient.pendingResponses.put(requestId.toString(), q); + + try { + // 🔹 سوئیچ به حالت آواتار (مثل "MEDIA\n") + outBin.write("AVATAR\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); + outBin.flush(); + + // 🔹 فریم باینری: مجیک + headerLen + header + contentLen + content + outBin.writeInt(0x41565431); // "AVT1" + outBin.writeInt(headerBytes.length); + outBin.write(headerBytes); + outBin.writeLong(contentLen); + + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; int n; + while ((n = fis.read(buf)) != -1) outBin.write(buf, 0, n); + } + outBin.flush(); + + // 🔹 انتظار ACK (مثل مدیا) + JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); + if (ack == null) { + System.out.println("❌ Avatar ACK timeout for " + requestId); + return; + } + + if ("success".equalsIgnoreCase(ack.optString("status"))) { + String url = ack.optString("display_url", null); + System.out.println("✅ Avatar uploaded. url=" + url); + // (اختیاری) رفرش UI + شکستن کش: + // MainController.getInstance().refreshMyAvatar(url + "?v=" + System.currentTimeMillis()); + } else { + System.out.println("❌ Avatar failed: " + ack.optString("message")); + } + } finally { + TelegramClient.pendingResponses.remove(requestId.toString()); + } + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ uploadAvatar error: " + e.getMessage()); + } + } + + + private static final Path UPLOADS_ROOT = + Paths.get(System.getProperty("app.uploads.root", "uploads")) + .toAbsolutePath().normalize(); + + private String prepareAvatar(String input) { + if (input == null || input.isBlank()) return null; + + // اگر URL کامل است، دست نزن + if (input.startsWith("http://") || input.startsWith("https://") || input.startsWith("file:")) { + return input; + } + + try { + Path src = Paths.get(input).toAbsolutePath().normalize(); + if (!Files.exists(src)) { + System.out.println("⚠️ Image file not found: " + src); + return null; + } + + String ext = getExt(src.getFileName().toString()); + String day = LocalDate.now().toString(); + String fileName = java.util.UUID.randomUUID() + (ext.isEmpty() ? ".jpg" : ext); + + Path destDir = UPLOADS_ROOT.resolve("avatars").resolve(day); + Files.createDirectories(destDir); + Path dest = destDir.resolve(fileName); + Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING); + + // مقدار نسبی که سرور/کلاینت‌های دیگر هم می‌فهمند + return "/avatars/" + day + "/" + fileName; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public void uploadAvatarFor(String targetType, UUID targetId, File file) { + if (file == null || !file.exists() || !file.isFile()) { + System.out.println("❌ Invalid file"); + return; + } + + final UUID reqId = UUID.randomUUID(); + + String lastStatus; + String lastMessage; + try { + String mime = detectMime(file, "IMAGE"); + if (mime == null) mime = "image/*"; + + JSONObject header = new JSONObject() + .put("request_id", reqId.toString()) // ← هماهنگ با pendingResponses + .put("target_type", targetType.toLowerCase()); // user | group | channel + if (targetId != null) header.put("target_id", targetId.toString()); + header.put("file_name", file.getName()) + .put("mime_type", mime); + + byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + long contentLen = file.length(); + + BlockingQueue q = new LinkedBlockingQueue<>(1); + // رجیستر با هر دو کلید تا هر نوع ACK روت شود + TelegramClient.pendingResponses.put(reqId.toString(), q); + TelegramClient.pendingResponses.put(header.optString("message_id", reqId.toString()), q); + + try { + TelegramClient.mediaBusy.set(true); // ← حین باینری JSON نخونیم + + // سوئیچ پروتکل + TelegramClient.getInstance().getOutBin() + .write("AVATAR\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); + + DataOutputStream outBin = TelegramClient.getInstance().getOutBin(); + outBin.flush(); + + // فریم: MAGIC + headerLen + header + contentLen + content + outBin.writeInt(0x41565431); // "AVT1" + outBin.writeInt(headerBytes.length); + outBin.write(headerBytes); + outBin.writeLong(contentLen); + + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int n; + while ((n = fis.read(buf)) != -1) outBin.write(buf, 0, n); + } + outBin.flush(); + } finally { + TelegramClient.mediaBusy.set(false); + } + + // انتظار ACK (هر کدوم از کلیدها بیاد، Listener می‌فرسته به صف q) + JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); + if (ack == null) { + System.out.println("❌ Avatar ACK timeout for " + reqId); + return; + } + + + if ("success".equalsIgnoreCase(ack.optString("status"))) { + String url = ack.optString("display_url", null); + lastStatus = "success"; + lastMessage = url != null ? url : ""; + + if (url != null && targetId != null) { + String busted = url + (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis(); + + // به‌روزرسانی State و UI: چت‌لیست + هدر + Platform.runLater(() -> { + var mc = org.to.telegramfinalproject.UI.MainController.getInstance(); + }); + } + } else { + System.out.println("❌ Avatar failed: " + ack.optString("message")); + lastStatus = "error"; + lastMessage = ack.optString("message", "failed"); + } + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ uploadAvatarFor error: " + e.getMessage()); + lastStatus = "error"; + lastMessage = e.getMessage(); + } finally { + TelegramClient.pendingResponses.remove(reqId.toString()); + } + } + + private static String getExt(String name) { + int i = name.lastIndexOf('.'); + return (i >= 0) ? name.substring(i) : ""; + } + + private void updateChatAvatarLocally(String targetType, UUID chatId, String newUrl) { + try { + + java.util.function.Consumer> upd = + list -> list.stream() + .filter(c -> c.getId().equals(chatId)) + .findFirst() + .ifPresent(c -> c.setImageUrl(newUrl)); + + upd.accept(Session.chatList); + upd.accept(Session.activeChats); + upd.accept(Session.archivedChats); + + } catch (Exception ignore) {} + } + + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index abc20f6..803fc78 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -36,34 +36,43 @@ public class IncomingMessageListener implements Runnable { try { System.out.println("👂 Real-Time Listener started."); - String line; - while ((line = in.readLine()) != null) { + 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; + } - JSONObject response = new JSONObject(line); System.out.println("📥 Received raw line: " + line); - //if it has reqID answer + // 1) اول message_id را روت کن (برای ACK نهایی مدیا) String mid = response.optString("message_id", ""); if (!mid.isEmpty()) { BlockingQueue q = TelegramClient.pendingResponses.get(mid); if (q != null) { q.put(response); - continue; // این پیام مصرف شد + 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 queue = TelegramClient.pendingResponses.get(requestId); if (queue != null) { queue.put(response); @@ -71,13 +80,10 @@ public class IncomingMessageListener implements Runnable { System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue..."); TelegramClient.responseQueue.put(response); } - continue; } - - - //if it has action check it + // 3) رویدادهای real-time if (response.has("action")) { String action = response.getString("action"); System.out.println("🎯 [Listener] Action received: " + response.toString(2)); @@ -88,9 +94,12 @@ public class IncomingMessageListener implements Runnable { } else { TelegramClient.responseQueue.put(response); } + continue; + } - } else if (response.has("status") && response.has("message")) { - TelegramClient.responseQueue.put(response); // general answer + // 4) سایر پاسخ‌های عمومی + if (response.has("status") && response.has("message")) { + TelegramClient.responseQueue.put(response); } else { TelegramClient.responseQueue.put(response); // fallback } diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 51bd3b7..6d68f5a 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -222,9 +222,6 @@ public class TelegramClient { if (socket != null && socket.isConnected() && !socket.isClosed()) return; socket = new Socket(SERVER_HOST, SERVER_PORT); - in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); - out = new PrintWriter(socket.getOutputStream(), true); - InputStream rawIn = socket.getInputStream(); OutputStream rawOut = socket.getOutputStream(); @@ -256,14 +253,24 @@ public class TelegramClient { // } +// 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; - Thread listenerThread = new Thread( - new IncomingMessageListener(in, mode), - "socket-listener" - ); + listener = new IncomingMessageListener(in, mode); + Thread listenerThread = new Thread(listener, "socket-listener"); listenerThread.setDaemon(true); listenerThread.start(); } diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java index 4429721..23b57d6 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java @@ -223,16 +223,58 @@ public class ChannelDatabase { } - 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 (?, ?, ?, ?, ?, ?)"; +// 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)) { - 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); + + 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) { diff --git a/src/main/java/org/to/telegramfinalproject/Server/ChannelService.java b/src/main/java/org/to/telegramfinalproject/Server/ChannelService.java index 7901d62..43560a1 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ChannelService.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ChannelService.java @@ -7,11 +7,11 @@ import java.time.LocalDateTime; import java.util.UUID; public class ChannelService { - public static boolean createChannel(String channelId, String channelName, UUID creatorUUID, String imageUrl) { + 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, now); + boolean inserted = ChannelDatabase.insertChannel(internalUUID, channelId, channelName, creatorUUID, imageUrl, description,now); if (inserted) { ChannelDatabase.addSubscriber(internalUUID, creatorUUID,"owner"); diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 701dd2c..58c31d1 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -970,8 +970,8 @@ public class ClientHandler implements Runnable { UUID creatorUUID = UUID.fromString(userIdStr); // اگر سرویس‌ات ورودی توضیح را می‌پذیرد، از متد اورلودشده استفاده کن: - // boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl, description); - boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl); + boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl, description); + //boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl); if (created) { Channel createdChannel = ChannelDatabase.findByChannelId(channelId); @@ -3319,6 +3319,10 @@ public class ClientHandler implements Runnable { } } + private boolean hasManagePermission(UUID currentUserId, String targetType, UUID targetId) { + return true; + } + private static String err(String m) { return new JSONObject().put("status","error").put("message",m).toString(); } private static boolean isAllowedImageMime(String mime) { @@ -3344,8 +3348,8 @@ public class ClientHandler implements Runnable { private boolean updateProfileImageUrl(String targetType, UUID targetId, String url) { String sql = switch (targetType) { case "user" -> "UPDATE users SET image_url=? WHERE internal_uuid=?"; - case "channel" -> "UPDATE channels SET image_url=? WHERE id=?"; - case "group" -> "UPDATE groups SET image_url=? WHERE id=?"; + case "channel" -> "UPDATE channels SET image_url=? WHERE internal_uuid=?"; + case "group" -> "UPDATE groups SET image_url=? WHERE internal_uuid=?"; default -> null; }; if (sql == null) return false; diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java index 4b61f3a..fc734c2 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatItemController.java @@ -117,4 +117,28 @@ public class ChatItemController { unreadCount.setManaged(show); if (show) unreadCount.setText(String.valueOf(unread)); } + + // ChatItemController.java + @FXML private ImageView avatarImage; + // اگر نوع چت نیاز داری برای دیفالت‌ها (private|group|channel) + private String chatType; // اگر داری از setChatData بگیرش و نگه دار + + 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(); + } + } + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 1767e88..5665701 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -30,6 +30,8 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.*; +import static org.to.telegramfinalproject.Client.Session.currentChatId; + public class ChatPageController { // ===== messages area ===== @@ -2354,6 +2356,39 @@ private void addBubble( } } + // ChatPageController.java + + public void onChatAvatarUpdated(UUID chatId, String newUrl) { + // اگه چت فعلی چیز دیگریه، کاری نکن + if (currentChat == null || chatId == null || newUrl == null || newUrl.isBlank()) return; + if (!currentChat.getId().equals(chatId)) return; + + // اگر از نخ غیر JavaFX صدا زده شد، امنش کنیم + if (!Platform.isFxApplicationThread()) { + Platform.runLater(() -> onChatAvatarUpdated(chatId, newUrl)); + return; + } + + // State داخلی entry را هم آپدیت کن + currentChat.setImageUrl(newUrl); + + // آواتار هدر را ست کن (با resolver خودت) + try { + Image im = org.to.telegramfinalproject.Client.AvatarLocalResolver.load(newUrl); + if (im != null) { + userAvatar.setImage(im); + } else { + // fallback اگر لود نشد + setDefaultHeaderAvatarByType(currentChat.getType()); + } + } catch (Exception ignore) { + setDefaultHeaderAvatarByType(currentChat.getType()); + } + + // مطمئن شو همچنان دایره‌ایه + try { AvatarFX.circleClip(userAvatar, 36); } catch (Throwable ignored) {} + } + private static final class ForwardTarget { diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index 38f2236..94f4f35 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -437,6 +437,9 @@ public class MainController { + + + ChatPageController controller = loader.getController(); controller.showChat(chat); diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java index 2bbb95f..620b4ab 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/NewChannelController.java @@ -103,12 +103,12 @@ public class NewChannelController { } private void onCreateChannel() { - String name = nz(channelNameField.getText()).trim(); - String dispId = nz(channelIdField.getText()).trim(); + 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 (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; @@ -118,70 +118,93 @@ public class NewChannelController { return; } - // TODO: اگر آپلود تصویر داری، فایل را آپلود کن و URL نهایی را اینجا بفرست - final String imageUrl = null; - + // در مرحله ساخت، image_url را خالی بفرست؛ بعداً آپلود می‌کنیم JSONObject req = new JSONObject() .put("action", "create_channel") - .put("channel_id", dispId) // آیدی نمایشی/عمومی + .put("channel_id", dispId) .put("channel_name", name) - .put("user_id", me) // internal_uuid سازنده - .put("image_url", imageUrl) // اختیاری - .put("description", description); // اختیاری (سرور اگر ساپورت نکند نادیده می‌گیرد) + .put("user_id", me) + .put("image_url", (Object) null) + .put("description", description); + + createButton.setDisable(true); new Thread(() -> { - JSONObject resp = ActionHandler.sendWithResponse(req); - if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { - Platform.runLater(() -> showToast("Create failed: " + - (resp == null ? "no response" : resp.optString("message","")))); - 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", name); - String returnedDisp = data.optString("id", dispId); - String returnedImg = data.optString("image_url", ""); - - // 1) چت کانال تازه‌ساخته را به سایدبار اضافه کن و انتخاب کن - Platform.runLater(() -> { - ChatEntry entry = ChatEntry.fromServer( - internalId, - "channel", - returnedName, - returnedDisp, - returnedImg, - /*isOwner*/ true, - /*isAdmin*/ true - ); - MainController.getInstance().addChatAndSelect(entry); - }); - - // 2) اُورلی افزودن سابسکرایبر را باز کن و internal_id را پاس بده - Platform.runLater(() -> { - 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."); + try { + JSONObject resp = ActionHandler.sendWithResponse(req); + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { + Platform.runLater(() -> { + createButton.setDisable(false); + showToast("Create failed: " + (resp == null ? "no response" : resp.optString("message",""))); + }); + 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()); diff --git a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java index 79ea078..40df1fb 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/NewGroupController.java @@ -14,12 +14,16 @@ 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.File; -import java.io.IOException; +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 { @@ -83,34 +87,25 @@ public class NewGroupController { 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 (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(); // internal_uuid + final String me = Session.getUserUUID(); if (me == null || me.isBlank()) { showToast("Cannot create group: current user UUID missing."); return; } - // اگر آپلود تصویر داری، اینجا عکس رو آپلود کن و imageUrl واقعی بفرست (TODO) + // در مرحلهٔ ساخت، 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) // ← internal_uuid سازنده - .put("image_url", imageUrl); // ← اختیاری + .put("user_id", me) + .put("image_url", imageUrl); new Thread(() -> { JSONObject resp = ActionHandler.sendWithResponse(req); @@ -131,21 +126,37 @@ public class NewGroupController { String returnedDisp = data.optString("id", groupId); String returnedImg = data.optString("image_url", ""); - // 1) به‌صورت لوکال یک ChatEntry بساز و به سایدبار اضافه و سِلکت کن + // اگر عکس انتخاب شده، حالا آپلود کن (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, - returnedImg, + imageUrlToUse, /*isOwner*/ true, /*isAdmin*/ true ); MainController.getInstance().addChatAndSelect(entry); + + // اگر URL جدید داریم، می‌تونی یک bust-cache بزنی: + // MainController.getInstance().refreshChatAvatar(internalId, imageUrlToUse + "?v=" + System.currentTimeMillis()); }); - // 2) پنجره‌ی Add Members را باز کن و internal_id گروه را پاس بده + // 2) پنجره Add Members Platform.runLater(() -> { try { FXMLLoader loader = new FXMLLoader(getClass().getResource( @@ -154,10 +165,7 @@ public class NewGroupController { AddMembersController controller = loader.getController(); controller.setGroupInfo(internalId, returnedName, returnedDisp, groupImageFile); - // این اُورلی را روی UI نشان بده MainController.getInstance().showOverlay(addMembersOverlay); - - // این اُورلی NewGroup را ببند MainController.getInstance().closeOverlay(overlayRoot); } catch (IOException ex) { ex.printStackTrace(); @@ -167,10 +175,14 @@ public class NewGroupController { }).start(); } + private void showToast(String msg) { // جایگزینش کن با سیستم نوتی شما Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); a.initOwner(overlayRoot.getScene().getWindow()); a.show(); } + + + }