Merge remote-tracking branch 'origin/Main-UI' into Main-UI
This commit is contained in:
@@ -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;
|
||||
@@ -749,7 +773,7 @@ public class ActionHandler {
|
||||
|
||||
if (existing != null) {
|
||||
openChat(existing);
|
||||
} else {
|
||||
} else {
|
||||
ChatEntry preview = new ChatEntry();
|
||||
preview.setId(String.valueOf(uuid));
|
||||
preview.setDisplayId(selected.getString("id"));
|
||||
@@ -858,13 +882,13 @@ public class ActionHandler {
|
||||
|
||||
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
|
||||
case "create_group":
|
||||
case "create_channel":
|
||||
if (response.has("data")) {
|
||||
JSONObject chatJson = response.getJSONObject("data");
|
||||
JSONObject chatJson = response.getJSONObject("data");
|
||||
|
||||
ChatEntry chat = new ChatEntry(
|
||||
UUID.fromString(chatJson.getString("internal_id")),
|
||||
@@ -879,12 +903,12 @@ public class ActionHandler {
|
||||
|
||||
|
||||
refreshChatList();
|
||||
System.out.println("✅ Created and opening chat...");
|
||||
refreshChatList();
|
||||
openChat(chat);
|
||||
}
|
||||
System.out.println("✅ Created and opening chat...");
|
||||
refreshChatList();
|
||||
openChat(chat);
|
||||
}
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
case "get_messages":
|
||||
JSONArray messages = response.getJSONObject("data").getJSONArray("messages");
|
||||
@@ -4191,6 +4215,4 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class IncomingMessageListener implements Runnable {
|
||||
JSONObject response = new JSONObject(line);
|
||||
System.out.println("📥 Received raw line: " + line);
|
||||
|
||||
//if it has reqID answer
|
||||
//if it has reqID answer
|
||||
if (response.has("request_id")) {
|
||||
String requestId = response.getString("request_id");
|
||||
System.out.println("📬 Response with request_id: " + requestId);
|
||||
@@ -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;
|
||||
|
||||
@@ -310,5 +310,4 @@ public class TelegramClient {
|
||||
return listener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -645,7 +645,7 @@ public class MessageDatabase {
|
||||
(UUID) rs.getObject("forwarded_from"),
|
||||
rs.getBoolean("is_deleted_globally"),
|
||||
(rs.getTimestamp("edited_at") != null) ? rs.getTimestamp("edited_at").toLocalDateTime() : null
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1339,4 +1339,4 @@ public class MessageDatabase {
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private");
|
||||
|
||||
ChatEntry entry = new ChatEntry(
|
||||
ChatEntry entry = new ChatEntry(
|
||||
chat.getChat_id(), // internal_id = chat_id
|
||||
isSelf ? "Saved Messages" : otherUser.getUser_id(), // id/display
|
||||
isSelf ? "Saved Messages" : otherUser.getProfile_name(), // name
|
||||
@@ -1654,7 +1654,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
String chatType = requestJson.getString("chat_type");
|
||||
UUID chatId = UUID.fromString(requestJson.getString("chat_id"));
|
||||
userId = UUID.fromString(requestJson.getString("user_id"));
|
||||
userId = UUID.fromString(requestJson.getString("user_id"));
|
||||
|
||||
boolean success = false;
|
||||
|
||||
@@ -1723,7 +1723,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
try {
|
||||
UUID channelId = UUID.fromString(requestJson.getString("channel_id"));
|
||||
userId = currentUser.getInternal_uuid();
|
||||
userId = currentUser.getInternal_uuid();
|
||||
|
||||
JSONObject permissions = ChannelDatabase.getChannelPermissions(channelId, userId);
|
||||
|
||||
@@ -2157,7 +2157,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
case "send_message" : {
|
||||
response = handleSendMessage(requestJson);
|
||||
response = handleSendMessage(requestJson);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2676,7 +2672,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
case "get_blocked_users": {
|
||||
userId = currentUser.getInternal_uuid();
|
||||
userId = currentUser.getInternal_uuid();
|
||||
var list = ContactDatabase.getBlockedUsers(userId);
|
||||
org.json.JSONObject data = new org.json.JSONObject();
|
||||
data.put("blocked_users", list);
|
||||
@@ -2685,7 +2681,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
case "verify_password": {
|
||||
userId = currentUser.getInternal_uuid();
|
||||
userId = currentUser.getInternal_uuid();
|
||||
String cur = requestJson.getString("current_password");
|
||||
User user = userDatabase.findByInternalUUID(userId);
|
||||
boolean ok = PasswordHashing.verify(cur, user.getPassword());
|
||||
@@ -2696,7 +2692,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
case "update_username": {
|
||||
userId = currentUser.getInternal_uuid();
|
||||
userId = currentUser.getInternal_uuid();
|
||||
String cur = requestJson.getString("current_password");
|
||||
String newUsername = requestJson.getString("new_username");
|
||||
boolean useBCrypt = true;
|
||||
@@ -2717,7 +2713,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
case "update_password": {
|
||||
userId = currentUser.getInternal_uuid();
|
||||
userId = currentUser.getInternal_uuid();
|
||||
String cur = requestJson.getString("current_password");
|
||||
String newPass = requestJson.getString("new_password");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
ChatEntry ce = new ChatEntry();
|
||||
ce.setType("private");
|
||||
ce.setName(r.title);
|
||||
ce.setDisplayId(r.displayId);
|
||||
try { ce.setOtherUserId(r.uuid); } catch (Exception ignore) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
ce.setType("private");
|
||||
|
||||
openChat(ce);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,50 +79,80 @@
|
||||
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>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
<!-- Expanding input -->
|
||||
<TextArea fx:id="messageInput"
|
||||
HBox.hgrow="ALWAYS"
|
||||
promptText="Write a message..."
|
||||
styleClass="msg-input"
|
||||
wrapText="true"
|
||||
prefRowCount="1"
|
||||
minHeight="40"
|
||||
maxHeight="120"
|
||||
VBox.vgrow="NEVER"/>
|
||||
|
||||
<!-- Send -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
<!-- Expanding input -->
|
||||
<TextArea fx:id="messageInput"
|
||||
HBox.hgrow="ALWAYS"
|
||||
promptText="Write a message..."
|
||||
styleClass="msg-input"
|
||||
wrapText="true"
|
||||
prefRowCount="1"
|
||||
minHeight="40"
|
||||
maxHeight="120"
|
||||
VBox.vgrow="NEVER" />
|
||||
|
||||
<!-- Send -->
|
||||
<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>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
|
||||
Reference in New Issue
Block a user