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

This commit is contained in:
Asal Lotfi
2025-09-04 00:20:20 +03:30
32 changed files with 2837 additions and 544 deletions
+5
View File
@@ -44,11 +44,16 @@ dependencies {
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 {
+4
View File
@@ -11,6 +11,10 @@ module org.to.telegramfinalproject {
requires eu.hansolo.tilesfx;
requires org.json;
requires java.sql;
requires java.desktop;
requires spark.core;
requires javax.servlet.api;
requires mp3agic;
opens org.to.telegramfinalproject to javafx.fxml;
exports org.to.telegramfinalproject;
exports org.to.telegramfinalproject.Client;
@@ -446,7 +446,31 @@ public class ActionHandler {
send(req);
}
public void createGroupUI(String id, String name, String url, String user ){
JSONObject req = new JSONObject();
req.put("action", "create_group");
req.put("user_id", user);
req.put("group_id", id);
req.put("group_name", name);
req.put("image_url", url.isBlank() ? JSONObject.NULL : url);
send(req);
}
public void createChannelUI(String id, String name, String url , String user){
JSONObject req = new JSONObject();
req.put("action", "create_channel");
req.put("user_id", user);
req.put("channel_id", id);
req.put("channel_name", name);
req.put("image_url", url.isBlank() ? JSONObject.NULL : url);
send(req);
}
public void createChannel() {
String channelId = null;
@@ -4192,5 +4216,3 @@ public class ActionHandler {
}
@@ -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;
}
}
}
@@ -89,16 +89,24 @@ public class IncomingMessageListener implements Runnable {
private boolean isRealTimeEvent(String action) {
return switch (action) {
case "new_message", "message_edited", "message_deleted_global",
"user_status_changed", "added_to_group", "added_to_channel",
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" , "message_reacted" , "message_unreacted","chat_updated" -> true;
"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();
@@ -166,6 +174,35 @@ public class IncomingMessageListener implements Runnable {
});
}
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);
});
}
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);
});
}
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);
});
}
case "chat_updated" -> {
var data = response.getJSONObject("data");
@@ -189,8 +226,7 @@ public class IncomingMessageListener implements Runnable {
case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted"
, "blocked_by_user", "unblocked_by_user", "message_seen" -> {
case "blocked_by_user", "unblocked_by_user", "message_seen" -> {
displayRealTimeMessage(action, msg);
}
@@ -478,4 +514,16 @@ public class IncomingMessageListener implements Runnable {
} 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;
}
}
@@ -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);
}
}
}
@@ -28,7 +28,7 @@ public class Session {
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");
@@ -110,4 +110,7 @@ public class Session {
.filter(ChatEntry::isArchived)
.toList();
}
public void setDownloadIndex(DownloadsIndex idx){ this.downloadsIndex = idx; }
}
@@ -255,234 +255,7 @@ public class SidebarHandler {
actionHandler.showContactList();
}
// public void getSavedMessagesData(String userId) {
// try {
// // Step 1: Create the request
// JSONObject request = new JSONObject();
// request.put("action", "get_saved_messages");
// request.put("user_id", userId);
//
// // Step 2: Send request and wait for response
// JSONObject response = ActionHandler.sendWithResponse(request);
//
// // Step 3: Check the response
// if (!response.optString("status", "fail").equals("success")) {
// System.out.println("Failed to open Saved Messages chat: " + response.optString("message", "Unknown error"));
// return;
// }
//
// // Step 4: Extract "data" object
// JSONObject data = response.getJSONObject("data");
// UUID chatId = UUID.fromString(data.getString("chat_id"));
// JSONArray messagesArray = data.getJSONArray("messages");
//
// // Step 5: Parse messages
// List<Message> messages = new ArrayList<>();
// if (!messagesArray.isEmpty()) {
// for (int i = 0; i < messagesArray.length(); i++) {
// JSONObject msgJson = messagesArray.getJSONObject(i);
//
// // Safely extract optional UUIDs
// UUID replyToId = null;
// String replyToIdStr = msgJson.optString("reply_to_id", null);
// if (replyToIdStr != null && !replyToIdStr.equals("null")) {
// replyToId = UUID.fromString(replyToIdStr);
// }
//
// UUID originalMessageId = null;
// String originalMessageIdStr = msgJson.optString("original_message_id", null);
// if (originalMessageIdStr != null && !originalMessageIdStr.equals("null")) {
// originalMessageId = UUID.fromString(originalMessageIdStr);
// }
//
// UUID forwardedBy = null;
// String forwardedByStr = msgJson.optString("forwarded_by", null);
// if (forwardedByStr != null && !forwardedByStr.equals("null")) {
// forwardedBy = UUID.fromString(forwardedByStr);
// }
//
// UUID forwardedFrom = null;
// String forwardedFromStr = msgJson.optString("forwarded_from", null);
// if (forwardedFromStr != null && !forwardedFromStr.equals("null")) {
// forwardedFrom = UUID.fromString(forwardedFromStr);
// }
//
// Message msg = new Message(
// UUID.fromString(msgJson.getString("message_id")),
// UUID.fromString(msgJson.getString("sender_id")),
// msgJson.getString("receiver_type"),
// UUID.fromString(msgJson.getString("receiver_id")),
// msgJson.getString("content"),
// msgJson.getString("message_type"),
// LocalDateTime.parse(msgJson.getString("send_at").replace(" ", "T")),
// msgJson.getString("status"),
// replyToId,
// msgJson.getBoolean("is_edited"),
// originalMessageId,
// forwardedBy,
// forwardedFrom,
// msgJson.getBoolean("is_deleted_globally"),
// LocalDateTime.parse(msgJson.getString("edited_at").replace(" ", "T"))
// );
//
// messages.add(msg);
// }
// }
//
// // Step 6: Add to active chats if not already present
// boolean alreadyExists = Session.activeChats.stream()
// .anyMatch(entry -> entry.getId().equals(chatId));
// if (!alreadyExists) {
// ChatEntry savedEntry = new ChatEntry(
// chatId,
// "Saved-Messages",
// "Saved Messages",
// "📌", // or use a URL string if you have an icon for saved messages
// "private",
// messages.isEmpty() ? null : messages.get(messages.size() - 1).getSend_at()
// );
// savedEntry.setSavedMessages(true);
// Session.activeChats.add(savedEntry);
// }
//
// // Step 7: Show chat
// showSavedMessages(chatId, messages);
//
// } catch (Exception e) {
// System.out.println("An error occurred while retrieving Saved Messages.");
// e.printStackTrace();
// }
// }
// private void showSavedMessages(UUID chatId, List<Message> messages) {
// Scanner scanner = new Scanner(System.in);
//
// System.out.println("==== Saved Messages ====");
// if (messages == null || messages.isEmpty()) {
// System.out.println("No messages yet.");
// } else {
// for (Message msg : messages) {
// System.out.println("[" + msg.getSend_at() + "] " + msg.getContent());
// }
// }
//
// System.out.println("\n(Type your message below, or type 0 to exit)");
//
// while (true) {
// System.out.print("You: ");
// String content = scanner.nextLine().trim();
// if (content.equals("0")) {
// System.out.println("Exiting Saved Messages.");
// break;
// }
//
// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
// String messageType = scanner.nextLine().trim().toUpperCase();
// Set<String> allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE", "AUDIO");
// while (!allowedTypes.contains(messageType)) {
// System.out.print("Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
// messageType = scanner.nextLine().trim().toUpperCase();
// }
//
// // Attachments (اختیاری)
// JSONArray attachmentsArray = new JSONArray();
// System.out.print("Do you want to attach files? (yes/no): ");
// if (scanner.nextLine().equalsIgnoreCase("yes")) {
// while (true) {
// System.out.print("File URL: ");
// String fileUrl = scanner.nextLine().trim();
//
// if (fileUrl.isEmpty()) {
// System.out.println("URL can not be empty. Try again.");
// continue;
// }
// if (fileUrl.contains(" ")) {
// System.out.println("URL cannot contain spaces. Try again.");
// continue;
// }
// if (!fileUrl.matches("^(http|https)://.*$")) {
// System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link.");
// continue;
// }
//
// System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): ");
// String fileType = scanner.nextLine().trim().toUpperCase();
// Set<String> allowedFileTypes = Set.of("IMAGE", "VIDEO", "FILE", "AUDIO");
// while (!allowedFileTypes.contains(fileType)) {
// System.out.print("Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): ");
// fileType = scanner.nextLine().trim().toUpperCase();
// }
//
// JSONObject fileJson = new JSONObject();
// fileJson.put("file_url", fileUrl);
// fileJson.put("file_type", fileType);
// attachmentsArray.put(fileJson);
//
// System.out.print("Add another file? (yes/no): ");
// if (!scanner.nextLine().equalsIgnoreCase("yes")) break;
// }
// }
//
// // درخواست مطابق هندلر send_message
// JSONObject request = new JSONObject();
// request.put("action", "send_message");
// request.put("receiver_type", "private");
// request.put("receiver_id", chatId.toString()); // chat_id
// request.put("content", content);
// request.put("message_type", messageType);
// if (attachmentsArray.length() > 0) {
// request.put("attachments", attachmentsArray);
// }
//
// JSONObject response = ActionHandler.sendWithResponse(request);
// if (!"success".equalsIgnoreCase(response.optString("status"))) {
// System.out.println("Failed to send message: " + response.optString("message", "Unknown error"));
// } else {
// System.out.println("Message sent.");
//
// Message justSent = new Message(
// UUID.fromString(response.getJSONObject("data").getString("message_id")),
// /* senderId */ userUUID,
// /* receiverId */ chatId,
// /* type */ "private",
// /* content */ content,
// /* msgType */ messageType,
// /* send_at */ java.time.LocalDateTime.now()
// );
// messages.add(justSent);
// System.out.println("[" + justSent.getSend_at() + "] " + justSent.getContent());
// }
// }
// }
// public void openSavedMessages() {
// JSONObject req = new JSONObject().put("action", "get_or_create_saved_messages");
// JSONObject res = sendWithResponse(req);
// if (res == null || !"success".equals(res.optString("status"))) {
// System.out.println("❌ Could not open Saved Messages: " + res.optString("message",""));
// return;
// }
//
// ActionHandler.requestChatList();
// String chatId = res.getJSONObject("data").getString("chat_id");
//
// JSONObject mreq = new JSONObject()
// .put("action", "get_messages")
// .put("receiver_type", "private")
// .put("receiver_id", chatId)
// .put("offset", 0)
// .put("limit", 50);
//
// JSONObject mres = sendWithResponse(mreq);
// JSONArray msgs = (mres != null && mres.has("data"))
// ? mres.getJSONObject("data").optJSONArray("messages")
// : new JSONArray();
//
// List<Message> messages = parseMessages(msgs); // تبدیل JSON → Message
// showSavedMessages(UUID.fromString(chatId), messages);
// }
//
@@ -774,7 +547,6 @@ public class SidebarHandler {
}
private String padBoxLine(String text, int width) {
// عرض: width، دو طرف │ │
final int inner = width - 2;
if (text.length() > inner) {
text = text.substring(0, inner - 1) + "";
@@ -804,58 +576,5 @@ public class SidebarHandler {
return approved ;
}
private List<Message> parseMessages(JSONArray msgs) {
List<Message> list = new ArrayList<>();
if (msgs == null) return list;
for (int i = 0; i < msgs.length(); i++) {
try {
JSONObject obj = msgs.getJSONObject(i);
UUID messageId = UUID.fromString(obj.getString("message_id"));
UUID senderId = UUID.fromString(obj.getString("sender_id"));
String receiverType= obj.getString("receiver_type"); // "private" | "group" | "channel"
UUID receiverId = UUID.fromString(obj.getString("receiver_id")); // برای private = chat_id
String content = obj.optString("content", "");
String messageType = obj.optString("message_type", "TEXT");
LocalDateTime sent = LocalDateTime.parse(obj.getString("send_at"));
Message m = new Message(
messageId, senderId, receiverId, receiverType, content, messageType, sent
);
if (obj.has("status") && !obj.isNull("status")) {
try { m.setStatus(obj.getString("status")); } catch (Exception ignore) {}
}
if (obj.has("reply_to_id") && !obj.isNull("reply_to_id")) {
try { m.setReply_to_id(UUID.fromString(obj.getString("reply_to_id"))); } catch (Exception ignore) {}
}
if (obj.has("forwarded_by") && !obj.isNull("forwarded_by")) {
try { m.setForwarded_by(UUID.fromString(obj.getString("forwarded_by"))); } catch (Exception ignore) {}
}
if (obj.has("forwarded_from") && !obj.isNull("forwarded_from")) {
try { m.setForwarded_from(UUID.fromString(obj.getString("forwarded_from"))); } catch (Exception ignore) {}
}
// // ضمیمه‌ها (اگر در مدل Message متد addAttachment داری)
// if (obj.has("attachments") && !obj.isNull("attachments")) {
// try {
// JSONArray atts = obj.getJSONArray("attachments");
// for (int j = 0; j < atts.length(); j++) {
// JSONObject a = atts.getJSONObject(j);
// String fileUrl = a.getString("file_url");
// String fileType = a.getString("file_type");
// FileAttachment fa = new FileAttachment(fileUrl, fileType);
// try { m.addAttachment(fa); } catch (Exception ignore) {}
// }
// } catch (Exception ignore) {}
// }
list.add(m);
} catch (Exception perItem) {
perItem.printStackTrace();
}
}
return list;
}
}
@@ -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);
}
}
@@ -143,7 +143,7 @@ import java.util.concurrent.LinkedBlockingQueue;
public class TelegramClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8000;
private static final int SERVER_PORT = 8080;
private static TelegramClient instance;
@@ -311,4 +311,3 @@ public class TelegramClient {
}
}
@@ -195,7 +195,7 @@ public class ChannelDatabase {
""";
try (Connection conn = ConnectionDb.connect()) {
// مرحله اول: ساخت کانال
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, channel.getChannel_id());
stmt.setString(2, channel.getChannel_name());
@@ -208,9 +208,8 @@ public class ChannelDatabase {
if (!rs.next()) return false;
UUID internalUUID = (UUID) rs.getObject("internal_uuid");
channel.setInternal_uuid(internalUUID); // اختیاری برای پیگیری بعدی
channel.setInternal_uuid(internalUUID);
// مرحله دوم: افزودن کاربر به لیست سابسکرایبرها
PreparedStatement subStmt = conn.prepareStatement(subscriberSql);
subStmt.setObject(1, internalUUID);
subStmt.setObject(2, creatorId);
@@ -1,6 +1,7 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONObject;
import org.to.telegramfinalproject.Models.MediaRow;
import java.sql.Connection;
import java.sql.PreparedStatement;
@@ -8,6 +9,7 @@ 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 {
@@ -73,4 +75,8 @@ public class MessageReactionDatabase {
return counts; // مثال: {"❤️":2,"👍":1}
}
}
@@ -388,5 +388,26 @@ public class PrivateChatDatabase {
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;
}
}
}
@@ -19,6 +19,7 @@ public class Contact {
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;}
@@ -1,19 +1,162 @@
package org.to.telegramfinalproject.Models;
public class FileAttachment {
private String fileUrl;
private String fileType;
import org.json.JSONObject;
import java.util.Objects;
import java.util.UUID;
public FileAttachment(String fileUrl, String fileType) {
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 String getFileUrl() {
return fileUrl;
public FileAttachment(String fileUrl, String fileType) {
this(fileUrl, fileType, null, null, null, null, null, null, null);
}
public String getFileType() {
return fileType;
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,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;
}
@@ -2474,8 +2474,8 @@ public class ClientHandler implements Runnable {
.put("excerpt", excerpt));
List<UUID> receivers = Receivers.resolveFor(receiverType, receiverId, senderId);
//RealTimeEventDispatcher.sendNewMessage(message, receivers, "reply", meta);
RealTimeEventDispatcher.sendNewMessageFiltered(message, receivers, senderId, "reply", meta);
receivers = Receivers.resolveFor(receiverType, receiverId, /*exclude*/ null);
RealTimeEventDispatcher.sendNewMessage(message, receivers, "reply", meta);
response = saved ?
@@ -2532,12 +2532,8 @@ public class ClientHandler implements Runnable {
.put("sender_id", original.getSender_id().toString())
.put("sender_name", userDatabase.findByInternalUUID(original.getSender_id()).getProfile_name()));
List<UUID> receivers = Receivers.resolveFor(targetChatType, targetChatId, currentUser.getInternal_uuid());
//RealTimeEventDispatcher.sendNewMessage(forwarded, receivers, "forward", meta);
RealTimeEventDispatcher.sendNewMessageFiltered(forwarded, receivers, senderId, "forward", meta);
} else {
response = new ResponseModel("error", "Failed to forward message.");
List<UUID> receivers = Receivers.resolveFor(targetChatType, targetChatId, /*exclude*/ null);
RealTimeEventDispatcher.sendNewMessage(forwarded, receivers, "forward", meta);
}
break;
}
@@ -7,7 +7,8 @@ import java.net.ServerSocket;
import java.net.Socket;
public class MainServer {
private static final int PORT = 8000;
private static final int PORT = 8080;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
@@ -1,10 +1,7 @@
package org.to.telegramfinalproject.Server;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import org.to.telegramfinalproject.Database.ContactDatabase;
import org.to.telegramfinalproject.Database.GroupDatabase;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Database.*;
import org.to.telegramfinalproject.Models.Message;
import org.to.telegramfinalproject.Models.User;
@@ -12,7 +9,9 @@ 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 {
@@ -425,4 +424,81 @@ public class RealTimeEventDispatcher {
}
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,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;
}
}
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
}
@@ -40,7 +40,7 @@ public class LoginController {
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
try {
connection = new ClientConnection("localhost", 8000);
connection = new ClientConnection("localhost", 8080);
} catch (Exception e) {
System.out.println("Could not connect to server: " + e.getMessage());
}
@@ -40,6 +40,7 @@ public class MainController {
GLOBAL,
CHAT
}
private ChatViewMode currentMode = ChatViewMode.NORMAL; // حالت فعلی: NORMAL/NEEDS_JOIN/NEEDS_ADD_CONTACT
private SearchMode currentSearchMode = SearchMode.GLOBAL;
private UUID currentChatId; // if in CHAT mode, which chat to search in
@@ -429,6 +430,9 @@ public class MainController {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml"));
Node chatPage = loader.load();
ChatPageController controller = loader.getController();
controller.showChat(chat);
@@ -445,6 +449,30 @@ public class MainController {
}
}
private void openChatWithMode(ChatEntry chat, ChatViewMode mode) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml"));
Node chatPage = loader.load();
ChatPageController controller = loader.getController();
controller.showChat(chat, mode); // متد جدید در ChatPageController
this.chatPageController = controller;
Session.currentChatId = chat.getId().toString();
chatDisplayArea.getChildren().setAll(chatPage);
chat.setUnreadCount(0);
ChatItemController item = itemControllers.get(chat.getId());
if (item != null) item.setUnread(0);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@FXML
private void toggleSidebar() {
if (isSidebarOpen) {
@@ -855,39 +883,52 @@ public class MainController {
private void openSearchResult(SearchResult r) {
switch (r.type) {
case USER: {
// 1) اگه قبلاً چت پرایوت با این یوزر داری، از همون استفاده کن
UUID existingChatId = findExistingPrivateChatId(r.uuid);
java.util.UUID chatId = findExistingPrivateChatId(r.uuid);
if (chatId == null) {
chatId = fetchOrCreatePrivateChat(r.uuid);
if (chatId == null) {
System.out.println("❌ Failed to create/find private chat.");
return;
}
}
org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry();
ce.setId(chatId.toString()); // internal chat_id
ce.setDisplayId(r.displayId); // username
ce.setName(r.title); // profile_name
ChatEntry ce = new ChatEntry();
ce.setType("private");
ce.setName(r.title);
ce.setDisplayId(r.displayId);
try { ce.setOtherUserId(r.uuid); } catch (Exception ignore) {}
openChat(ce);
ChatViewMode mode;
if (existingChatId != null) {
ce.setId(existingChatId.toString());
mode = ChatViewMode.NORMAL;
} else {
// هنوز چتی وجود ندارد Preview (بدون ساخت چت)
// برای Preview از uuid خودِ طرف مقابل به‌عنوان id موقت استفاده می‌کنیم
ce.setId(r.uuid.toString());
mode = isContact(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_ADD_CONTACT;
}
openChatWithMode(ce, mode);
break;
}
case GROUP:
case CHANNEL: {
org.to.telegramfinalproject.Models.ChatEntry existing =
findExistingChat(r.uuid, r.receiverType);
if (existing != null) {
openChat(existing);
openChatWithMode(existing, ChatViewMode.NORMAL);
} else {
org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry();
ce.setId(r.uuid.toString()); // internal_uuid group/channel
ce.setDisplayId(r.displayId); // group_id/channel_id
ce.setName(r.title);
ce.setType(r.receiverType);
openChat(ce);
//openChat(ce);
ChatViewMode mode;
try {
mode = isInAnyChatList(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_JOIN;
} catch (Exception e) {
mode = ChatViewMode.NEEDS_JOIN;
}
openChatWithMode(ce, mode);
}
break;
}
@@ -991,4 +1032,87 @@ public class MainController {
return false;
}
//Search
private boolean isInAnyChatList(UUID chatId) {
var lists = List.of(
Session.chatList != null ? Session.chatList : List.<ChatEntry>of(),
Session.activeChats != null ? Session.activeChats : List.<ChatEntry>of(),
Session.archivedChats != null ? Session.archivedChats : List.<ChatEntry>of()
);
for (var lst : lists) {
for (var c : lst) {
try {
if (chatId.equals(UUID.fromString(c.getId().toString()))) return true;
} catch (Exception ignore) {}
}
}
return false;
}
private boolean isContact(UUID userUuid) {
if (Session.contactEntries == null) return false;
try {
for (var c : Session.contactEntries) {
UUID id = c.getContactId();
if (userUuid.equals(id)) return true;
}
} catch (Exception ignore) {}
return false;
}
private ChatViewMode computeMode(ChatEntry ce) {
try {
UUID id = UUID.fromString(ce.getId().toString());
if (isInAnyChatList(id)) return ChatViewMode.NORMAL;
} catch (Exception ignore) {}
String t = ce.getType();
if ("group".equalsIgnoreCase(t) || "channel".equalsIgnoreCase(t)) {
return ChatViewMode.NEEDS_JOIN;
}
if ("private".equalsIgnoreCase(t)) {
UUID other = null;
try { other = ce.getOtherUserId(); } catch (Exception ignore) {}
return (other != null && isContact(other))
? ChatViewMode.NORMAL
: ChatViewMode.NEEDS_ADD_CONTACT;
}
return ChatViewMode.NORMAL;
}
public void onJoinedOrAdded(ChatEntry ce) {
try {
UUID id = UUID.fromString(ce.getId().toString());
if (!isInAnyChatList(id)) {
if (Session.chatList == null) Session.chatList = new ArrayList<>();
Session.chatList.add(ce);
}
// اگر activeChats استفاده می‌کنی:
if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) {
Session.activeChats.add(ce);
}
} catch (Exception ignore) {}
// (اختیاری) مرتب‌سازی بر اساس زمان آخرین پیام
Comparator<ChatEntry> byTimeDesc = (a,b) -> {
LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime();
if (t1 == null && t2 == null) return 0;
if (t1 == null) return 1;
if (t2 == null) return -1;
return t2.compareTo(t1);
};
if (Session.chatList != null) Session.chatList.sort(byTimeDesc);
if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc);
refreshChatListUI();
}
}
@@ -73,4 +73,29 @@ public class ChannelPermissionUtil {
}
return false;
}
public static boolean isUserInChannel(UUID userId, UUID channelId) {
final String SQL = "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(SQL)) {
ps.setObject(1, channelId, java.sql.Types.OTHER); // 👈 مهم برای Postgres UUID
ps.setObject(2, userId, java.sql.Types.OTHER);
System.out.println("[SQL] isUserInChannel ch=" + channelId + " user=" + userId
+ " db=" + conn.getMetaData().getURL());
try (ResultSet rs = ps.executeQuery()) {
boolean ok = rs.next();
System.out.println("[SQL] isUserInChannel -> " + ok);
return ok;
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
}
+9
View File
@@ -156,5 +156,14 @@ CREATE TABLE IF NOT EXISTS message_attachments (
);
--Run this part in your pg
ALTER TABLE message_attachments
ADD COLUMN IF NOT EXISTS media_key UUID;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
UPDATE message_attachments
SET media_key = gen_random_uuid()
WHERE media_key IS NULL;
@@ -0,0 +1,76 @@
/* دقیقاً فقط روی این دو دکمه اعمال می‌شود */
#joinAction, #addContactAction {
-fx-background-color: transparent, transparent, transparent, transparent;
-fx-background-insets: 0,0,0,0;
-fx-background-radius: 0,0,0,0;
-fx-border-color: transparent;
-fx-border-width: 0;
-fx-text-fill: #1e88e5; /* آبی لینک */
-fx-font-weight: 700;
-fx-font-size: 12px;
-fx-padding: 12 0 12 0;
-fx-cursor: hand;
-fx-effect: null;
}
#joinAction:hover, #addContactAction:hover {
-fx-underline: true;
}
#joinAction:focused, #addContactAction:focused {
-fx-underline: true;
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
}
#joinAction:armed, #addContactAction:armed {
-fx-opacity: .85;
}
/* ظرف بنر پایین */
.chat-footer-banner {
-fx-background-color: transparent;
}
/* دکمهٔ لینک‌مانند (مشترک) */
.footer-link-btn {
-fx-background-color: transparent;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-padding: 6 0 6 0; /* نازک مثل لینک */
-fx-border-color: transparent;
-fx-font-size: 14px;
-fx-font-weight: 700;
-fx-text-fill: #1a73e8; /* پیش‌فرض آبی (برای Join/Add) */
}
/* کانتینر بنر پایین چت */
.chat-footer-banner {
-fx-background-color: transparent;
-fx-alignment: center;
}
/* متن آبی برای حالت Read-only */
.chat-footer-banner .banner-text-blue {
-fx-text-fill: #1E88E5; /* آبی */
-fx-font-weight: 700;
-fx-background-color: transparent;
}
/* لینک‌استایل دکمه‌ها در بنر */
.chat-footer-banner .footer-link-btn {
-fx-background-color: transparent;
-fx-text-fill: #1E88E5; /* آبی پیش‌فرض */
-fx-font-weight: 700;
-fx-padding: 6 12;
-fx-background-insets: 0;
-fx-cursor: hand;
}
.chat-footer-banner .footer-link-btn:hover {
-fx-underline: true;
}
/* نسخه قرمز برای UNBLOCK */
.chat-footer-banner .footer-link-btn.danger {
-fx-text-fill: #D32F2F; /* قرمز */
}
@@ -0,0 +1,109 @@
/* رنگ‌ها */
:root {
-tg-bg: #ffffff;
-tg-sheet: #ffffff;
-tg-shadow: rgba(0,0,0,.25);
-tg-sep: rgba(0,0,0,.06);
-tg-text: #0f141a;
-tg-subtext: #6e7b87;
-tg-danger: #e53935;
-tg-primary: #2481cc;
-tg-chip-bg: #f5f7fa;
}
/* ACTION SHEET */
.tg-action-sheet {
-fx-background-color: -tg-sheet;
-fx-background-radius: 16;
-fx-effect: dropshadow(gaussian, -tg-shadow, 24, 0.26, 0, 4);
-fx-padding: 0;
-fx-border-radius: 16;
}
.tg-reaction-bar {
-fx-background-color: transparent;
-fx-spacing: 12;
}
.tg-reaction {
-fx-font-size: 22px;
-fx-cursor: hand;
-fx-padding: 2 4 2 4;
-fx-background-radius: 12;
}
.tg-reaction:hover {
-fx-background-color: -tg-chip-bg;
}
.tg-menu { -fx-background-color: transparent; }
.tg-item {
-fx-background-color: transparent;
-fx-background-radius: 12;
}
.tg-item:hover {
-fx-background-color: -tg-chip-bg;
}
.tg-item .tg-label {
-fx-text-fill: -tg-text;
-fx-font-size: 16px;
}
.tg-item.danger .tg-label {
-fx-text-fill: -tg-danger;
}
.tg-sep {
-fx-background-color: -tg-sep;
-fx-min-height: 1px;
-fx-pref-height: 1px;
-fx-max-height: 1px;
-fx-background-insets: 0 14 0 14;
}
/* DELETE SHEET */
.tg-delete-sheet {
-fx-background-color: -tg-bg;
-fx-background-radius: 16;
-fx-effect: dropshadow(gaussian, -tg-shadow, 28, 0.28, 0, 4);
}
.tg-delete-title {
-fx-font-size: 18px;
-fx-text-fill: -tg-text;
}
.tg-delete-check {
-fx-text-fill: -tg-text;
-fx-font-size: 14px;
}
/* Buttons */
.tg-btn-secondary {
-fx-background-color: transparent;
-fx-text-fill: -tg-primary;
-fx-font-size: 14px;
-fx-padding: 8 14 8 14;
-fx-background-radius: 10;
}
.tg-btn-secondary:hover {
-fx-background-color: -tg-chip-bg;
}
.tg-btn-danger {
-fx-background-color: -tg-danger;
-fx-text-fill: white;
-fx-font-size: 14px;
-fx-padding: 8 16 8 16;
-fx-background-radius: 10;
}
.tg-btn-danger:hover { -fx-opacity: .9; }
/* دارک‌مود (اگر CSS سوییچ داری، این کلس را به Scene اضافه کن) */
.root.dark .tg-action-sheet,
.root.dark .tg-delete-sheet { -fx-background-color: #1f2a33; }
.root.dark {
-tg-bg: #1f2a33;
-tg-sheet: #22303a;
-tg-text: #e8f1f8;
-tg-subtext: #9bb2c3;
-tg-sep: rgba(255,255,255,.08);
-tg-chip-bg: rgba(255,255,255,.06);
-tg-shadow: rgba(0,0,0,.45);
}
@@ -6,17 +6,19 @@
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<VBox xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.to.telegramfinalproject.UI.ChatPageController"
spacing="0"
styleClass="chat-root">
<stylesheets>
<URL value="@/org/to/telegramfinalproject/CSS/chat.css"/>
</stylesheets>
<!-- HEADER: avatar + name/status (left) | search + more (right) -->
<HBox fx:id="chatHeader" spacing="10" styleClass="chat-header">
<padding>
<Insets top="8" right="12" bottom="8" left="12"/>
</padding>
<padding><Insets top="8" right="12" bottom="8" left="12"/></padding>
<!-- Avatar -->
<ImageView fx:id="userAvatar" fitWidth="36" fitHeight="36" preserveRatio="true"/>
@@ -33,9 +35,7 @@
<Button fx:id="searchInChatButton" styleClass="icon-btn" onAction="#openSearchPanel">
<graphic>
<ImageView fx:id="searchIcon" fitWidth="20" fitHeight="20" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/search_light.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/search_light.png"/></image>
</ImageView>
</graphic>
</Button>
@@ -44,9 +44,7 @@
<Button fx:id="moreButton" styleClass="icon-btn">
<graphic>
<ImageView fx:id="moreIcon" fitWidth="18" fitHeight="18" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/more_light.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/more_light.png"/></image>
</ImageView>
</graphic>
<contextMenu>
@@ -55,18 +53,14 @@
<MenuItem fx:id="viewProfileItem" text="View profile">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/view_profile_light.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/view_profile_light.png"/></image>
</ImageView>
</graphic>
</MenuItem>
<MenuItem fx:id="deleteChatItem" text="Delete chat" style="-fx-text-fill: red;">
<graphic>
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/></image>
</ImageView>
</graphic>
</MenuItem>
@@ -85,26 +79,23 @@
styleClass="chat-scroll">
<content>
<VBox fx:id="messageContainer" spacing="10">
<padding>
<Insets top="10" right="10" bottom="10" left="10"/>
</padding>
<padding><Insets top="10" right="10" bottom="10" left="10"/></padding>
</VBox>
</content>
</ScrollPane>
<!-- INPUT BAR -->
<HBox fx:id="inputBar" spacing="8" styleClass="chat-input-bar">
<padding>
<Insets top="8" right="12" bottom="8" left="12"/>
</padding>
<!-- ===== FOOTER: فقط یکی از این پنل‌ها نمایش داده می‌شود ===== -->
<!-- حالت عادی: کامپوزر پیام -->
<VBox fx:id="composerPane" spacing="0" visible="true" managed="true">
<HBox spacing="8" styleClass="chat-input-bar">
<padding><Insets top="8" right="12" bottom="8" left="12"/></padding>
<!-- Attachment -->
<Button fx:id="attachmentButton" styleClass="icon-btn">
<graphic>
<ImageView fx:id="attachmentIcon" fitWidth="20" fitHeight="20" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/attachment_light.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/attachment_light.png"/></image>
</ImageView>
</graphic>
</Button>
@@ -124,11 +115,44 @@
<Button fx:id="sendButton" styleClass="send-btn">
<graphic>
<ImageView fx:id="sendIcon" fitWidth="20" fitHeight="20" preserveRatio="true">
<image>
<Image url="@/org/to/telegramfinalproject/Icons/send_cyan2.png"/>
</image>
<image><Image url="@/org/to/telegramfinalproject/Icons/send_cyan2.png"/></image>
</ImageView>
</graphic>
</Button>
</HBox>
</VBox>
<VBox fx:id="joinPane" alignment="CENTER" visible="false" managed="false" styleClass="chat-input-bar">
<padding><Insets top="12" right="12" bottom="12" left="12"/></padding>
<Button fx:id="joinButton" id="joinAction"
text="JOIN CHANNEL" onAction="#onJoinClicked"/>
</VBox>
<VBox fx:id="addContactPane" alignment="CENTER" visible="false" managed="false" styleClass="chat-input-bar">
<padding><Insets top="12" right="12" bottom="12" left="12"/></padding>
<Button fx:id="addContactButton" id="addContactAction"
text="ADD CONTACT" onAction="#onAddContactClicked"/>
</VBox>
<VBox fx:id="readOnlyPane" spacing="0" alignment="CENTER"
visible="false" managed="false" styleClass="chat-footer-banner" >
<padding><Insets top="10" right="12" bottom="10" left="12"/></padding>
<Label fx:id="readOnlyLabel"
text="You are not allowed to send messages."
styleClass="footer-link-btn"/>
</VBox>
<VBox fx:id="blockedPane" spacing="0" alignment="CENTER"
visible="false" managed="false" styleClass="chat-footer-banner">
<padding><Insets top="12" right="12" bottom="12" left="12"/></padding>
<Button fx:id="unblockBtn"
text="UNBLOCK"
onAction="#onUnblockClicked"
styleClass="footer-link-btn danger"/>
</VBox>
</VBox>