Profile logic for UI

This commit is contained in:
2025-09-04 20:45:38 +03:30
parent 89e2033ac2
commit 9c61b27004
11 changed files with 674 additions and 115 deletions
@@ -1,5 +1,6 @@
package org.to.telegramfinalproject.Client; package org.to.telegramfinalproject.Client;
import javafx.application.Platform;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Database.PrivateChatDatabase; 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.SearchRequestModel;
import org.to.telegramfinalproject.Models.SearchResultModel; import org.to.telegramfinalproject.Models.SearchResultModel;
import java.io.BufferedReader; import java.io.*;
import java.io.DataOutputStream; import java.nio.file.Files;
import java.io.IOException; import java.nio.file.Path;
import java.io.PrintWriter; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ArrayBlockingQueue; 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<JSONObject> 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<JSONObject> 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<JSONObject> 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<java.util.List<org.to.telegramfinalproject.Models.ChatEntry>> 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) {}
}
} }
@@ -36,34 +36,43 @@ public class IncomingMessageListener implements Runnable {
try { try {
System.out.println("👂 Real-Time Listener started."); System.out.println("👂 Real-Time Listener started.");
String line; while (true) {
while ((line = in.readLine()) != null) {
if (TelegramClient.mediaBusy.get()) { if (TelegramClient.mediaBusy.get()) {
try { Thread.sleep(15); } catch (InterruptedException ignored) {} try { Thread.sleep(15); } catch (InterruptedException ignored) {}
continue; 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); System.out.println("📥 Received raw line: " + line);
//if it has reqID answer // 1) اول message_id را روت کن (برای ACK نهایی مدیا)
String mid = response.optString("message_id", ""); String mid = response.optString("message_id", "");
if (!mid.isEmpty()) { if (!mid.isEmpty()) {
BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid); BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid);
if (q != null) { if (q != null) {
q.put(response); q.put(response);
continue; // این پیام مصرف شد continue; // مصرف شد
} }
} }
// 2) بعد request_id را روت کن (برای INIT و بقیه درخواست‌ها)
if (response.has("request_id")) { if (response.has("request_id")) {
String requestId = response.getString("request_id"); String requestId = response.getString("request_id");
System.out.println("📬 Response with request_id: " + requestId); System.out.println("📬 Response with request_id: " + requestId);
System.out.println("📬 Full response: " + response.toString(2)); System.out.println("📬 Full response: " + response.toString(2));
BlockingQueue<JSONObject> queue = TelegramClient.pendingResponses.get(requestId); BlockingQueue<JSONObject> queue = TelegramClient.pendingResponses.get(requestId);
if (queue != null) { if (queue != null) {
queue.put(response); 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..."); System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue...");
TelegramClient.responseQueue.put(response); TelegramClient.responseQueue.put(response);
} }
continue; continue;
} }
// 3) رویدادهای real-time
//if it has action check it
if (response.has("action")) { if (response.has("action")) {
String action = response.getString("action"); String action = response.getString("action");
System.out.println("🎯 [Listener] Action received: " + response.toString(2)); System.out.println("🎯 [Listener] Action received: " + response.toString(2));
@@ -88,9 +94,12 @@ public class IncomingMessageListener implements Runnable {
} else { } else {
TelegramClient.responseQueue.put(response); TelegramClient.responseQueue.put(response);
} }
continue;
}
} else if (response.has("status") && response.has("message")) { // 4) سایر پاسخ‌های عمومی
TelegramClient.responseQueue.put(response); // general answer if (response.has("status") && response.has("message")) {
TelegramClient.responseQueue.put(response);
} else { } else {
TelegramClient.responseQueue.put(response); // fallback TelegramClient.responseQueue.put(response); // fallback
} }
@@ -222,9 +222,6 @@ public class TelegramClient {
if (socket != null && socket.isConnected() && !socket.isClosed()) return; if (socket != null && socket.isConnected() && !socket.isClosed()) return;
socket = new Socket(SERVER_HOST, SERVER_PORT); 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(); InputStream rawIn = socket.getInputStream();
OutputStream rawOut = socket.getOutputStream(); 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) { private void startListenerOnce(IncomingMessageListener.UIMode mode) {
if (listenerStarted) return; if (listenerStarted) return;
listenerStarted = true; listenerStarted = true;
Thread listenerThread = new Thread( listener = new IncomingMessageListener(in, mode);
new IncomingMessageListener(in, mode), Thread listenerThread = new Thread(listener, "socket-listener");
"socket-listener"
);
listenerThread.setDaemon(true); listenerThread.setDaemon(true);
listenerThread.start(); listenerThread.start();
} }
@@ -223,16 +223,58 @@ public class ChannelDatabase {
} }
public static boolean insertChannel(UUID internalUUID, String channelId, String channelName, UUID creatorId, String imageUrl, LocalDateTime createdAt) { // 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 (?, ?, ?, ?, ?, ?)"; // 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.setObject(1, internalUUID);
stmt.setString(2, channelId); stmt.setString(2, channelId);
stmt.setString(3, channelName); stmt.setString(3, channelName);
stmt.setObject(4, creatorId); 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(); stmt.executeUpdate();
return true; return true;
} catch (SQLException e) { } catch (SQLException e) {
@@ -7,11 +7,11 @@ import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
public class ChannelService { 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(); UUID internalUUID = UUID.randomUUID();
LocalDateTime now = LocalDateTime.now(); 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) { if (inserted) {
ChannelDatabase.addSubscriber(internalUUID, creatorUUID,"owner"); ChannelDatabase.addSubscriber(internalUUID, creatorUUID,"owner");
@@ -970,8 +970,8 @@ public class ClientHandler implements Runnable {
UUID creatorUUID = UUID.fromString(userIdStr); UUID creatorUUID = UUID.fromString(userIdStr);
// اگر سرویس‌ات ورودی توضیح را می‌پذیرد، از متد اورلودشده استفاده کن: // اگر سرویس‌ات ورودی توضیح را می‌پذیرد، از متد اورلودشده استفاده کن:
// boolean created = ChannelService.createChannel(channelId, channelName, creatorUUID, imageUrl, description); 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);
if (created) { if (created) {
Channel createdChannel = ChannelDatabase.findByChannelId(channelId); 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 String err(String m) { return new JSONObject().put("status","error").put("message",m).toString(); }
private static boolean isAllowedImageMime(String mime) { private static boolean isAllowedImageMime(String mime) {
@@ -3344,8 +3348,8 @@ public class ClientHandler implements Runnable {
private boolean updateProfileImageUrl(String targetType, UUID targetId, String url) { private boolean updateProfileImageUrl(String targetType, UUID targetId, String url) {
String sql = switch (targetType) { String sql = switch (targetType) {
case "user" -> "UPDATE users SET image_url=? WHERE internal_uuid=?"; case "user" -> "UPDATE users SET image_url=? WHERE internal_uuid=?";
case "channel" -> "UPDATE channels SET image_url=? WHERE id=?"; case "channel" -> "UPDATE channels SET image_url=? WHERE internal_uuid=?";
case "group" -> "UPDATE groups SET image_url=? WHERE id=?"; case "group" -> "UPDATE groups SET image_url=? WHERE internal_uuid=?";
default -> null; default -> null;
}; };
if (sql == null) return false; if (sql == null) return false;
@@ -117,4 +117,28 @@ public class ChatItemController {
unreadCount.setManaged(show); unreadCount.setManaged(show);
if (show) unreadCount.setText(String.valueOf(unread)); 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();
}
}
} }
@@ -30,6 +30,8 @@ import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import static org.to.telegramfinalproject.Client.Session.currentChatId;
public class ChatPageController { public class ChatPageController {
// ===== messages area ===== // ===== 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 { private static final class ForwardTarget {
@@ -437,6 +437,9 @@ public class MainController {
ChatPageController controller = loader.getController(); ChatPageController controller = loader.getController();
controller.showChat(chat); controller.showChat(chat);
@@ -103,12 +103,12 @@ public class NewChannelController {
} }
private void onCreateChannel() { private void onCreateChannel() {
String name = nz(channelNameField.getText()).trim(); String name = nz(channelNameField.getText()).trim();
String dispId = nz(channelIdField.getText()).trim(); String dispId = nz(channelIdField.getText()).trim();
String description = nz(channelDescField.getText()).trim(); String description = nz(channelDescField.getText()).trim();
boolean ok = true; 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 (dispId.isEmpty()) { channelIdField.getStyleClass().add("error"); channelIdLabel.getStyleClass().add("error"); ok = false; }
if (!ok) return; if (!ok) return;
@@ -118,70 +118,93 @@ public class NewChannelController {
return; return;
} }
// TODO: اگر آپلود تصویر داری، فایل را آپلود کن و URL نهایی را اینجا بفرست // در مرحله ساخت، image_url را خالی بفرست؛ بعداً آپلود می‌کنیم
final String imageUrl = null;
JSONObject req = new JSONObject() JSONObject req = new JSONObject()
.put("action", "create_channel") .put("action", "create_channel")
.put("channel_id", dispId) // آیدی نمایشی/عمومی .put("channel_id", dispId)
.put("channel_name", name) .put("channel_name", name)
.put("user_id", me) // internal_uuid سازنده .put("user_id", me)
.put("image_url", imageUrl) // اختیاری .put("image_url", (Object) null)
.put("description", description); // اختیاری (سرور اگر ساپورت نکند نادیده می‌گیرد) .put("description", description);
createButton.setDisable(true);
new Thread(() -> { new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req); try {
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) { JSONObject resp = ActionHandler.sendWithResponse(req);
Platform.runLater(() -> showToast("Create failed: " + if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
(resp == null ? "no response" : resp.optString("message","")))); Platform.runLater(() -> {
return; createButton.setDisable(false);
} showToast("Create failed: " + (resp == null ? "no response" : resp.optString("message","")));
});
JSONObject data = resp.optJSONObject("data"); return;
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.");
} }
});
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(); }).start();
} }
private void showToast(String msg) { private void showToast(String msg) {
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(overlayRoot.getScene().getWindow()); a.initOwner(overlayRoot.getScene().getWindow());
@@ -14,12 +14,16 @@ import javafx.scene.layout.Pane;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Client.ActionHandler; import org.to.telegramfinalproject.Client.ActionHandler;
import org.to.telegramfinalproject.Client.Session; import org.to.telegramfinalproject.Client.Session;
import org.to.telegramfinalproject.Client.TelegramClient;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import java.io.File; import java.io.*;
import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; 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 { public class NewGroupController {
@@ -83,34 +87,25 @@ public class NewGroupController {
String groupId = groupIdField.getText() == null ? "" : groupIdField.getText().trim(); String groupId = groupIdField.getText() == null ? "" : groupIdField.getText().trim();
boolean ok = true; boolean ok = true;
if (groupName.isEmpty()) { if (groupName.isEmpty()) { groupNameField.getStyleClass().add("error"); groupNameLabel.getStyleClass().add("error"); ok = false; }
groupNameField.getStyleClass().add("error"); if (groupId.isEmpty()) { groupIdField.getStyleClass().add("error"); groupIdLabel.getStyleClass().add("error"); ok = false; }
groupNameLabel.getStyleClass().add("error");
ok = false;
}
if (groupId.isEmpty()) {
groupIdField.getStyleClass().add("error");
groupIdLabel.getStyleClass().add("error");
ok = false;
}
if (!ok) return; if (!ok) return;
// ساخت گروه روی سرور final String me = Session.getUserUUID();
final String me = Session.getUserUUID(); // internal_uuid
if (me == null || me.isBlank()) { if (me == null || me.isBlank()) {
showToast("Cannot create group: current user UUID missing."); showToast("Cannot create group: current user UUID missing.");
return; return;
} }
// اگر آپلود تصویر داری، اینجا عکس رو آپلود کن و imageUrl واقعی بفرست (TODO) // در مرحلهٔ ساخت، image_url را خالی بفرست (بعداً آپلود می‌کنیم)
final String imageUrl = null; final String imageUrl = null;
JSONObject req = new JSONObject() JSONObject req = new JSONObject()
.put("action", "create_group") .put("action", "create_group")
.put("group_id", groupId) .put("group_id", groupId)
.put("group_name", groupName) .put("group_name", groupName)
.put("user_id", me) // internal_uuid سازنده .put("user_id", me)
.put("image_url", imageUrl); // اختیاری .put("image_url", imageUrl);
new Thread(() -> { new Thread(() -> {
JSONObject resp = ActionHandler.sendWithResponse(req); JSONObject resp = ActionHandler.sendWithResponse(req);
@@ -131,21 +126,37 @@ public class NewGroupController {
String returnedDisp = data.optString("id", groupId); String returnedDisp = data.optString("id", groupId);
String returnedImg = data.optString("image_url", ""); 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(() -> { Platform.runLater(() -> {
ChatEntry entry = ChatEntry.fromServer( ChatEntry entry = ChatEntry.fromServer(
internalId, internalId,
"group", "group",
returnedName, returnedName,
returnedDisp, returnedDisp,
returnedImg, imageUrlToUse,
/*isOwner*/ true, /*isOwner*/ true,
/*isAdmin*/ true /*isAdmin*/ true
); );
MainController.getInstance().addChatAndSelect(entry); 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(() -> { Platform.runLater(() -> {
try { try {
FXMLLoader loader = new FXMLLoader(getClass().getResource( FXMLLoader loader = new FXMLLoader(getClass().getResource(
@@ -154,10 +165,7 @@ public class NewGroupController {
AddMembersController controller = loader.getController(); AddMembersController controller = loader.getController();
controller.setGroupInfo(internalId, returnedName, returnedDisp, groupImageFile); controller.setGroupInfo(internalId, returnedName, returnedDisp, groupImageFile);
// این اُورلی را روی UI نشان بده
MainController.getInstance().showOverlay(addMembersOverlay); MainController.getInstance().showOverlay(addMembersOverlay);
// این اُورلی NewGroup را ببند
MainController.getInstance().closeOverlay(overlayRoot); MainController.getInstance().closeOverlay(overlayRoot);
} catch (IOException ex) { } catch (IOException ex) {
ex.printStackTrace(); ex.printStackTrace();
@@ -167,10 +175,14 @@ public class NewGroupController {
}).start(); }).start();
} }
private void showToast(String msg) { private void showToast(String msg) {
// جایگزینش کن با سیستم نوتی شما // جایگزینش کن با سیستم نوتی شما
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK); Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
a.initOwner(overlayRoot.getScene().getWindow()); a.initOwner(overlayRoot.getScene().getWindow());
a.show(); a.show();
} }
} }