diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 853dfdb..604c393 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Database.PrivateChatDatabase; +import org.to.telegramfinalproject.Database.ContactDatabase; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; @@ -501,6 +502,10 @@ public class ActionHandler { entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); } + if (chat.has("is_saved_messages")) { + entry.setSavedMessages(chat.getBoolean("is_saved_messages")); + } + chatList.add(entry); } @@ -558,7 +563,10 @@ public class ActionHandler { c.getString("contact_displayId"), c.getString("profile_name"), c.optString("image_url", ""), - c.optBoolean("is_blocked", false) + c.optBoolean("is_blocked", false), + c.isNull("last_seen") + ? null + : LocalDateTime.parse(c.getString("last_seen")) ); Session.contactEntries.add(entry); } @@ -839,7 +847,8 @@ public class ActionHandler { System.out.println("3. Create Channel"); System.out.println("4. Create group"); System.out.println("5. View contacts"); - System.out.println("6. Logout"); + System.out.println("6. Show sidebar menu"); + System.out.println("7. Logout"); System.out.print("Choose an option: "); String choice = scanner.nextLine(); @@ -853,7 +862,8 @@ public class ActionHandler { case "3" -> createChannel(); case "4" -> createGroup(); case "5" -> showContactList(); - case "6" -> { + case "6" -> showSidebarMenu(); + case "7" -> { logout(); return; } @@ -862,119 +872,10 @@ public class ActionHandler { } } - public void showContactList() { - List contacts = Session.contactEntries; - if (contacts.isEmpty()) { - System.out.println("šŸ“­ You have no contacts."); - return; - } - - System.out.println("šŸ‘„ Your Contacts:"); - for (int i = 0; i < contacts.size(); i++) { - System.out.println((i + 1) + ". " + contacts.get(i)); - } - - System.out.print("Select a contact (0 to go back): "); - int choice = scanner.nextInt(); - scanner.nextLine(); - - if (choice == 0) return; - if (choice < 1 || choice > contacts.size()) { - System.out.println("āŒ Invalid choice."); - return; - } - - ContactEntry selected = contacts.get(choice - 1); - System.out.println("\nšŸ“‡ What do you want to do with " + selected.getProfileName() + "?"); - System.out.println("1. View Profile"); - System.out.println("2. Send Message"); - System.out.print("Enter your choice: "); - int action = scanner.nextInt(); - scanner.nextLine(); - - switch (action) { - case 1 -> viewProfile(selected.getContactId()); - case 2 -> startPrivateChat(selected); - default -> System.out.println("āŒ Invalid option."); - } - } - - - // public void showContactList() { -// System.out.println("1. View All Contacts"); -// System.out.println("2. Search Contacts"); -// System.out.println("Choose an option: (0 to go back)"); -// int option = scanner.nextInt(); -// scanner.nextLine(); -// -// // Handle invalid input -// while (option < 0 || option > 2) { -// System.out.println("Invalid choice. Try again: "); -// option = scanner.nextInt(); -// scanner.nextLine(); -// } -// -// List contacts; -// -// if (option == 0) { -// return; -// } -// else if (option == 1) { -// contacts = Session.contactEntries; -// -// } else if (option == 2) { -// contacts = new ArrayList<>(); -// -// System.out.print("Enter name or user ID to search: "); -// String searchTerm = scanner.nextLine(); -// -// // Handle invalid input -// while (searchTerm.isEmpty()) { -// System.out.print("Search key can not be empty. Try again: "); -// searchTerm = scanner.nextLine(); -// } -// -// // Send a request to server -// JSONObject request = new JSONObject(); -// request.put("action", "search_contacts"); -// request.put("user_id", Session.getUserUUID()); -// request.put("search_term", searchTerm); -// -// JSONObject response = ActionHandler.sendWithResponse(request); -// -// if (!response.optString("status", "fail").equals("success")) { -// System.out.println("Failed to search contacts: " + response.optString("message", "Unknown error")); -// return; -// } -// -// JSONObject data = response.getJSONObject("data"); -// JSONArray contactsJson = data.getJSONArray("contacts"); -// -// for (int i = 0; i < contactsJson.length(); i++) { -// JSONObject contact = contactsJson.getJSONObject(i); -// -// UUID contactId = UUID.fromString(contact.getString("contact_id")); -// String userId = contact.getString("user_id"); -// String profileName = contact.getString("profile_name"); -// String imageUrl = contact.optString("image_url", ""); -// boolean isBlocked = contact.getBoolean("is_blocked"); -// String lastSeenString = contact.getString("last_seen"); -// LocalDateTime lastSeen = null; -// if (lastSeenString != null) { -// lastSeen = LocalDateTime.parse(lastSeenString); -// } -// -// contacts.add(new ContactEntry(contactId, userId, profileName, imageUrl, isBlocked)); -// } -// -// } else { -// System.out.println("āŒ Invalid choice."); -// return; -// } -// +// List contacts = Session.contactEntries; // if (contacts.isEmpty()) { -// System.out.println("šŸ“­ No contacts found."); +// System.out.println("šŸ“­ You have no contacts."); // return; // } // @@ -997,35 +898,144 @@ public class ActionHandler { // System.out.println("\nšŸ“‡ What do you want to do with " + selected.getProfileName() + "?"); // System.out.println("1. View Profile"); // System.out.println("2. Send Message"); -// System.out.println("3. Remove Contact"); // System.out.print("Enter your choice: "); // int action = scanner.nextInt(); // scanner.nextLine(); +// // switch (action) { // case 1 -> viewProfile(selected.getContactId()); // case 2 -> startPrivateChat(selected); -// case 3 -> { -// // Send a request to server -// JSONObject request = new JSONObject(); -// request.put("action", "remove_contact"); -// request.put("user_id", Session.getUserUUID()); // Current user -// request.put("contact_id", selected.getContactId().toString()); // Contact to remove -// -// JSONObject response = ActionHandler.sendWithResponse(request); -// -// if ("success".equals(response.optString("status"))) { -// System.out.println("āœ… Contact removed successfully."); -// Session.contactEntries.remove(selected); // Remove from local session list -// } else { -// System.out.println("āŒ Failed to remove contact: " + -// response.optString("message", "Unknown error")); -// } -// } -// // default -> System.out.println("āŒ Invalid option."); // } // } + public void showContactList() { + System.out.println("1. View All Contacts"); + System.out.println("2. Search Contacts"); + System.out.println("Choose an option: (0 to go back)"); + int option = scanner.nextInt(); + scanner.nextLine(); + + // Handle invalid input + while (option < 0 || option > 2) { + System.out.println("Invalid choice. Try again: "); + option = scanner.nextInt(); + scanner.nextLine(); + } + + List contacts; + + if (option == 0) { + return; + } + else if (option == 1) { + contacts = Session.contactEntries; + + } else if (option == 2) { + contacts = new ArrayList<>(); + + System.out.print("Enter name or user ID to search: "); + String searchTerm = scanner.nextLine(); + + // Handle invalid input + while (searchTerm.isEmpty()) { + System.out.print("Search key can not be empty. Try again: "); + searchTerm = scanner.nextLine(); + } + + // Send a request to server + JSONObject request = new JSONObject(); + request.put("action", "search_contacts"); + request.put("user_id", Session.getUserUUID()); + request.put("search_term", searchTerm); + + JSONObject response = ActionHandler.sendWithResponse(request); + + if (!response.optString("status", "fail").equals("success")) { + System.out.println("Failed to search contacts: " + response.optString("message", "Unknown error")); + return; + } + + JSONObject data = response.getJSONObject("data"); + JSONArray contactsJson = data.getJSONArray("contacts"); + + for (int i = 0; i < contactsJson.length(); i++) { + JSONObject contact = contactsJson.getJSONObject(i); + + UUID contactId = UUID.fromString(contact.getString("contact_id")); + String userId = contact.getString("user_id"); + String contact_displayId = contact.getString("contact_display_id"); + String profileName = contact.getString("profile_name"); + String imageUrl = contact.optString("image_url", ""); + boolean isBlocked = contact.getBoolean("is_blocked"); + String lastSeenString = contact.getString("last_seen"); + LocalDateTime lastSeen = null; + if (lastSeenString != null) { + lastSeen = LocalDateTime.parse(lastSeenString); + } + + contacts.add(new ContactEntry(contactId, userId, contact_displayId, profileName, imageUrl, isBlocked, lastSeen)); + } + + } else { + System.out.println("āŒ Invalid choice."); + return; + } + + if (contacts.isEmpty()) { + System.out.println("šŸ“­ No contacts found."); + return; + } + + System.out.println("šŸ‘„ Your Contacts:"); + for (int i = 0; i < contacts.size(); i++) { + System.out.println((i + 1) + ". " + contacts.get(i)); + } + + System.out.print("Select a contact (0 to go back): "); + int choice = scanner.nextInt(); + scanner.nextLine(); + + if (choice == 0) return; + if (choice < 1 || choice > contacts.size()) { + System.out.println("āŒ Invalid choice."); + return; + } + + ContactEntry selected = contacts.get(choice - 1); + System.out.println("\nšŸ“‡ What do you want to do with " + selected.getProfileName() + "?"); + System.out.println("1. View Profile"); + System.out.println("2. Send Message"); + System.out.println("3. Remove Contact"); + System.out.print("Enter your choice: "); + int action = scanner.nextInt(); + scanner.nextLine(); + + switch (action) { + case 1 -> viewProfile(selected.getContactId()); + case 2 -> startPrivateChat(selected); + case 3 -> { + // Send a request to server + JSONObject request = new JSONObject(); + request.put("action", "remove_contact"); + request.put("user_id", Session.getUserUUID()); // Current user + request.put("contact_id", selected.getContactId().toString()); // Contact to remove + + JSONObject response = ActionHandler.sendWithResponse(request); + + if ("success".equals(response.optString("status"))) { + System.out.println("āœ… Contact removed successfully."); + Session.contactEntries.remove(selected); // Remove from local session list + } else { + System.out.println("āŒ Failed to remove contact: " + + response.optString("message", "Unknown error")); + } + } + + default -> System.out.println("āŒ Invalid option."); + } + } + private void viewProfile(UUID targetId) { JSONObject req = new JSONObject(); @@ -1129,13 +1139,35 @@ public class ActionHandler { System.out.println("\nYour Chats:"); System.out.println("0. šŸ“¦ Archived Chats"); + // Track index dynamically + int index = 1; + + // Check if Saved Messages exists in the list + int savedMessagesIndex = -1; for (int i = 0; i < Session.activeChats.size(); i++) { ChatEntry entry = Session.activeChats.get(i); + + if (entry.isSavedMessages()) { + savedMessagesIndex = index; + System.out.println(index + ". šŸ“¦ Saved Messages Chat"); + index++; + break; + } + } + + // Print the rest of the chats + for (int i = 0; i < Session.activeChats.size(); i++) { + ChatEntry entry = Session.activeChats.get(i); + if (entry.isSavedMessages()) { + continue; // Already printed above + } + String time = (entry.getLastMessageTime() == null) ? "No messages yet" : entry.getLastMessageTime().toString(); - System.out.println((i + 1) + ". [" + entry.getType() + "] " + + System.out.println(index + ". [" + entry.getType() + "] " + entry.getName() + " - Last: " + time); + index++; } System.out.print("Select a chat by number: "); @@ -1146,14 +1178,33 @@ public class ActionHandler { return; } - int index = choice - 1; + if (choice == savedMessagesIndex) { + new SidebarHandler(scanner, this).getSavedMessagesData(Session.getUserUUID()); + return; + } - if (index < 0 || index >= Session.activeChats.size()) { + // Adjust for Saved Messages if it was in the list + int baseIndex = (savedMessagesIndex != -1 && choice > savedMessagesIndex) ? 1 : 0; + int chatIndex = choice - 1 - baseIndex; + + if (chatIndex < 0 || chatIndex >= Session.activeChats.size()) { System.out.println("Invalid selection."); return; } - ChatEntry selected = Session.activeChats.get(index); + // Find the actual index of the chat, skipping the saved_messages entry + int actualIndex = 0; + for (int i = 0; i < Session.activeChats.size(); i++) { + if (Session.activeChats.get(i).getType().equalsIgnoreCase("saved_messages")) { + continue; // Skip saved_messages + } + if (actualIndex == chatIndex) { + break; + } + actualIndex++; + } + + ChatEntry selected = Session.activeChats.get(actualIndex); openChat(selected); } @@ -3414,8 +3465,6 @@ public class ActionHandler { // } - - public void sendMessage(UUID chatId, String receiverType) { Scanner scanner = new Scanner(System.in); @@ -3880,6 +3929,52 @@ public class ActionHandler { } } + private void showSidebarMenu() { + System.out.println("\n--- Sidebar Menu ---"); + System.out.println("1. View Profile"); + System.out.println("2. New Group"); + System.out.println("3. New Channel"); + System.out.println("4. View Contacts"); + System.out.println("5. Saved messages"); + System.out.println("6. Settings"); + System.out.println("7. Telegram features"); + System.out.println("8. Telegram Q&A"); + System.out.println("0. Back"); + System.out.println("Choose your action: (0 to 8)"); + + // Handle invalid input + int action; + while (true) { + if (scanner.hasNextInt()) { + action = scanner.nextInt(); + scanner.nextLine(); // Clear newline + + if (action >= 0 && action <= 8) { + break; + } + System.out.println("Invalid input. Please enter a number between 0 and 8."); + } else { + scanner.nextLine(); + System.out.println("Invalid input. Try again."); + } + } + + SidebarHandler sidebarHandler = new SidebarHandler(scanner, this); + switch (action) { + case 0 -> { + return; + } + case 1 -> sidebarHandler.handleSidebarAction(SidebarAction.MY_PROFILE); + case 2 -> sidebarHandler.handleSidebarAction(SidebarAction.NEW_GROUP); + case 3 -> sidebarHandler.handleSidebarAction(SidebarAction.NEW_CHANNEL); + case 4 -> sidebarHandler.handleSidebarAction(SidebarAction.CONTACTS); + case 5 -> sidebarHandler.handleSidebarAction(SidebarAction.SAVED_MESSAGES); + case 6 -> sidebarHandler.handleSidebarAction(SidebarAction.SETTINGS); + case 7 -> sidebarHandler.handleSidebarAction(SidebarAction.FEATURES); + case 8 -> sidebarHandler.handleSidebarAction(SidebarAction.Q_AND_A); + default -> System.out.println("Invalid action."); + } + } } diff --git a/src/main/java/org/to/telegramfinalproject/Client/SidebarAction.java b/src/main/java/org/to/telegramfinalproject/Client/SidebarAction.java new file mode 100644 index 0000000..4a16145 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/SidebarAction.java @@ -0,0 +1,12 @@ +package org.to.telegramfinalproject.Client; + +public enum SidebarAction { + MY_PROFILE, + NEW_GROUP, + NEW_CHANNEL, + CONTACTS, + SAVED_MESSAGES, + SETTINGS, + FEATURES, + Q_AND_A +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java new file mode 100644 index 0000000..652b8b7 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java @@ -0,0 +1,465 @@ +package org.to.telegramfinalproject.Client; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.Message; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse; + +public class SidebarHandler { + private final Scanner scanner; + private final ActionHandler actionHandler; + private final String userUUID; + + public SidebarHandler(Scanner scanner, ActionHandler actionHandler) { + this.scanner = scanner; + this.actionHandler = actionHandler; + this.userUUID = Session.getUserUUID(); + + } + + public void handleSidebarAction(SidebarAction action) { + switch (action) { + case MY_PROFILE: + openUserProfile(); + break; + case NEW_GROUP: + createNewGroup(); + break; + case NEW_CHANNEL: + createNewChannel(); + break; + case CONTACTS: + showContacts(); + break; + case SAVED_MESSAGES: + getSavedMessagesData(userUUID); + break; + case SETTINGS: + openSettings(); + break; + case FEATURES: + showTelegramFeatures(); + break; + case Q_AND_A: + showTelegramQA(); + break; + } + } + + private void openUserProfile() { + // Step 1: Request profile info from the server + JSONObject request = new JSONObject(); + request.put("action", "get_user_profile"); + + JSONObject response = sendWithResponse(request); + if (response == null || !response.optString("status", "fail").equals("success")) { + System.out.println("Failed to load profile information."); + return; + } + + JSONObject profile; + try { + profile = response.getJSONObject("data"); + } catch (JSONException e) { + System.out.println("Received malformed profile data from server."); + return; + } + + String profileName = profile.optString("profile_name", "NOT-AVAILABLE"); + String userId = profile.optString("user_id", "NOT-AVAILABLE"); + String status = profile.optString("status", "NOT-AVAILABLE"); + String bio = profile.optString("bio", "NOT-AVAILABLE"); + String profilePictureUrl = profile.optString("profile_picture_url", "NOT-AVAILABLE"); + + // Step 2: Show profile info + System.out.println("===== My Profile ====="); + System.out.println("Profile Picture URL : " + profilePictureUrl); + System.out.println("Profile Name : " + profileName); + System.out.println("User ID : @" + userId); + System.out.println("Status : " + status); + System.out.println("Bio : " + bio); + System.out.println("======================"); + + // Step 3: Ask if user wants to edit anything + System.out.println("Do you want to edit any of these? (yes/no)"); + while (true) { + String choice = scanner.nextLine().trim().toLowerCase(); + if (choice.equals("no")) return; + if (!choice.equals("yes")) { + System.out.println("Invalid input. Please enter 'yes' or 'no'."); + continue; + } + + // Step 4: Show editable fields + System.out.println("Which field do you want to edit?"); + System.out.println("1. Profile Name"); + System.out.println("2. User ID"); + System.out.println("3. Bio"); + System.out.println("4. Profile Picture URL"); + System.out.println("0. Cancel"); + + String option = scanner.nextLine().trim(); + switch (option) { + case "1" -> editProfileName(); + case "2" -> editUserId(); + case "3" -> editBio(); + case "4" -> editProfilePictureUrl(); + case "0" -> { return; } + default -> System.out.println("Invalid choice. Try again."); + } + + // Re-fetch and re-display profile after editing + openUserProfile(); + return; + } + } + + private void editProfileName() { + System.out.println("Enter your new profile name:"); + String newProfileName = scanner.nextLine().trim(); + + // Step 1: Validate input + while (newProfileName.trim().isEmpty()) { + System.out.println("Profile name cannot be empty. Enter another name."); + newProfileName = scanner.nextLine().trim(); + } + + // Step 2: Create request + JSONObject request = new JSONObject(); + request.put("action", "edit_profile_name"); + request.put("new_profile_name", newProfileName); + + // Step 3: Send request and receive response + JSONObject response = sendWithResponse(request); + + // Step 4: Handle response + if (response == null || !response.optString("status", "fail").equals("success")) { + System.out.println("Failed to update profile name."); + } else { + System.out.println("Profile name updated successfully!"); + } + } + + private void editUserId() { + while (true) { + System.out.println("Enter your new user ID: "); + String newUserId = scanner.nextLine().trim(); + + if (newUserId.isEmpty()) { + System.out.println("User ID cannot be empty."); + continue; + } + + if (newUserId.contains(" ")) { + System.out.println("User ID cannot contain spaces."); + continue; + } + + if (!newUserId.matches("^[a-zA-Z0-9_]+$")) { + System.out.println("User ID can only contain letters, digits, and underscores."); + continue; + } + + // Send to server + JSONObject request = new JSONObject(); + request.put("action", "edit_user_id"); + request.put("new_user_id", newUserId); + JSONObject response = sendWithResponse(request); + + if (response.getString("status").equals("success")) { + System.out.println("User ID updated successfully."); + break; + } else { + // Server-side error message (e.g., ID already exists) + System.out.println(response.getString("message")); + } + } + } + + private void editBio() { + System.out.println("Enter your new bio:"); + String newBio = scanner.nextLine().trim(); + + // Limit the bio length + if (newBio.length() > 70) { + System.out.println("Bio cannot be more than 70 characters."); + return; + } + + JSONObject request = new JSONObject(); + request.put("action", "edit_bio"); + request.put("new_bio", newBio); + + JSONObject response = sendWithResponse(request); + + if (response.getString("status").equals("success")) { + System.out.println("Bio updated successfully."); + } else { + System.out.println(response.getString("message")); + } + } + + private void editProfilePictureUrl() { + while (true) { + System.out.println("Enter new profile picture URL (or leave empty to remove):"); + String newImageUrl = scanner.nextLine().trim(); + + // URL validation + if (newImageUrl.contains(" ")) { + System.out.println("URL cannot contain spaces."); + continue; + } + + if (!newImageUrl.isEmpty() && !newImageUrl.matches("^(http|https)://.*$")) { + System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link."); + continue; + } + + JSONObject request = new JSONObject(); + request.put("action", "edit_profile_picture"); + request.put("new_image_url", newImageUrl); + JSONObject response = sendWithResponse(request); + + if (response.getString("status").equals("success")) { + System.out.println("Bio updated successfully."); + } else { + System.out.println(response.getString("message")); + } + break; + } + } + + private void createNewGroup() { + actionHandler.createGroup(); + } + + private void createNewChannel() { + actionHandler.createChannel(); + } + + private void showContacts() { + actionHandler.showContactList(); + } + + public void 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 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 messages) { + Scanner scanner = new Scanner(System.in); + + System.out.println("==== Saved Messages ===="); + + // Show previous messages + if (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().toUpperCase(); + Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedTypes.contains(messageType)) { + System.out.print("Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE / AUDIO): "); + messageType = scanner.nextLine().toUpperCase(); + } + + // Attaching Files + 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(); + + // URL validation + if (fileUrl.isEmpty()) { + System.out.print("URL can not be empty. Try again."); + continue; + } + + if (fileUrl.contains(" ")) { + System.out.println("URL cannot contain spaces. Try again."); + continue; + } + + if (!fileUrl.isEmpty() && !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().toUpperCase(); + + Set allowedFileTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedFileTypes.contains(fileType)) { + System.out.print("Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): "); + fileType = scanner.nextLine().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; + } + } + + // Prepare the request JSON + JSONObject request = new JSONObject(); + request.put("action", "send_saved_messages"); + request.put("message_id", UUID.randomUUID().toString()); + request.put("sender_id", userUUID); + request.put("receiver_type", "private"); + request.put("receiver_id", userUUID); // saved messages = to yourself + request.put("content", content); + request.put("message_type", "TEXT"); + request.put("status", "READ"); + request.put("reply_to_id", JSONObject.NULL); + request.put("is_edited", false); + request.put("original_message_id", JSONObject.NULL); + request.put("forwarded_by", JSONObject.NULL); + request.put("forwarded_from", JSONObject.NULL); + request.put("is_deleted_globally", JSONObject.NULL); + request.put("edited_at", JSONObject.NULL); + + // Send the message and wait for response + JSONObject response = ActionHandler.sendWithResponse(request); + + if (!response.optString("status", "fail").equals("success")) { + System.out.println("Failed to send message: " + response.optString("message", "Unknown error")); + } else { + System.out.println("Message sent."); + } + } + } + + private void openSettings() { + System.out.println("āš™ļø Opening settings..."); + } + + private void showTelegramFeatures() { + System.out.println("🌟 Showing Telegram features..."); + } + + private void showTelegramQA() { + System.out.println("ā“ Showing Q&A..."); + } +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java index 9a488c5..3801302 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -5,6 +5,7 @@ import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.User; import java.sql.*; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -46,7 +47,7 @@ public class ContactDatabase { - public boolean removeContact(UUID user_id, UUID contact_id) { + public static boolean removeContact(UUID user_id, UUID contact_id) { String sql = "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?"; try (Connection connection = getConnection()) { PreparedStatement stmt = connection.prepareStatement(sql); @@ -172,35 +173,6 @@ public class ContactDatabase { return false; } - public List searchContacts(UUID user_id, String searchTerm) { - List results = new ArrayList<>(); //ILIKE case-insensitive - String sql = """ - SELECT c.contact_id, c.added_at, c.is_blocked - FROM contacts c - JOIN users u ON c.contact_id = u.internal_uuid - WHERE c.user_id = ? AND (u.user_id ILIKE ? OR u.profile_name ILIKE ?) - """; - try (Connection connection = getConnection()) { - PreparedStatement stmt = connection.prepareStatement(sql); - stmt.setObject(1, user_id); - stmt.setString(2, "%" + searchTerm + "%"); - stmt.setString(3, "%" + searchTerm + "%"); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - UUID contactId = (UUID) rs.getObject("contact_id"); - boolean isBlocked = rs.getBoolean("is_blocked"); - Timestamp addedAt = rs.getTimestamp("added_at"); - Contact contact = new Contact(user_id, contactId); - contact.setIs_blocked(isBlocked); - contact.setAdd_at(addedAt.toLocalDateTime()); - results.add(contact); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return results; - } - public static boolean deleteChatOneSide(UUID currentUserId, UUID otherUserId) { String sql = """ @@ -306,6 +278,50 @@ public class ContactDatabase { return entries; } + public static List searchContacts(UUID userId, String searchTerm) { + List results = new ArrayList<>(); + String sql = """ + SELECT u.internal_uuid, u.user_id, c.is_blocked + FROM contacts c + JOIN users u ON c.contact_id = u.internal_uuid + WHERE c.user_id = ? + AND (u.user_id ILIKE ? OR u.profile_name ILIKE ?) + """; + + try (Connection connection = getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + + stmt.setObject(1, userId); + stmt.setString(2, "%" + searchTerm + "%"); + stmt.setString(3, "%" + searchTerm + "%"); + + ResultSet rs = stmt.executeQuery(); + + userDatabase userDB = new userDatabase(); + while (rs.next()) { + UUID contactId = (UUID) rs.getObject("internal_uuid"); + String displayId = rs.getString("user_id"); + String contact_displayId = userDB.getUserId(contactId); + String profileName = userDB.getProfileName(contactId); + String imageUrl = userDB.getProfilePicture(contactId); + boolean isBlocked = rs.getBoolean("is_blocked"); + + String lastSeenString = userDB.getLastSeen(contactId); + + // Convert to LocalDateTime + LocalDateTime lastSeen = null; + if (!"Unknown".equals(lastSeenString)) { + lastSeen = LocalDateTime.parse(lastSeenString); + } + + results.add(new ContactEntry(contactId, displayId, contact_displayId, profileName, imageUrl, isBlocked, lastSeen)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return results; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 72e691d..7c8a35b 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -505,25 +505,27 @@ public class MessageDatabase { stmt.setObject(i++, userId); // 5) group: gm.user_id stmt.setObject(i++, userId); // 6) channel: cs.user_id - try (ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - messages.add(new Message( - UUID.fromString(rs.getString("message_id")), - rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, - rs.getString("receiver_type"), - UUID.fromString(rs.getString("receiver_id")), - rs.getString("content"), - rs.getString("message_type"), - rs.getTimestamp("send_at").toLocalDateTime(), - rs.getString("status"), - rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, - rs.getBoolean("is_edited"), - rs.getBoolean("is_deleted_globally"), - rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, - rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, - rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null - )); - } + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Message message = new Message( + UUID.fromString(rs.getString("message_id")), + rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, + rs.getString("receiver_type"), + UUID.fromString(rs.getString("receiver_id")), + rs.getString("content"), + rs.getString("message_type"), + rs.getTimestamp("send_at").toLocalDateTime(), + rs.getString("status"), + rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, + rs.getBoolean("is_edited"), + rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, + rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, + rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null, + rs.getBoolean("is_deleted_globally"), + rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null + ); + + messages.add(message); } } catch (SQLException e) { e.printStackTrace(); @@ -626,7 +628,6 @@ public class MessageDatabase { rs.getString("status"), (UUID) rs.getObject("reply_to_id"), rs.getBoolean("is_edited"), -// rs.getBoolean("is_deleted_globally"), (UUID) rs.getObject("original_message_id"), (UUID) rs.getObject("forwarded_by"), (UUID) rs.getObject("forwarded_from"), @@ -1096,8 +1097,57 @@ public class MessageDatabase { return messages; } + public static boolean insertSavedMessage(Message message) { + String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type, send_at, status, reply_to_id, is_edited, edited_at," + + " original_message_id, forwarded_by, forwarded_from, is_deleted_globally) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, message.getMessage_id()); // message_id + ps.setObject(2, message.getSender_id()); // sender_id + ps.setString(3, message.getReceiver_type()); // receiver_type (private) + ps.setObject(4, message.getReceiver_id()); // receiver_id (same as sender_id for saved messages) + ps.setString(5, message.getContent()); // content + ps.setString(6, message.getMessage_type()); // message_type + ps.setTimestamp(7, Timestamp.valueOf(message.getSend_at())); // send_at + ps.setString(8, message.getStatus()); // status + if (message.getReply_to_id() != null) + ps.setObject(9, message.getReply_to_id()); // reply_to_id + else + ps.setNull(9, Types.OTHER); + ps.setBoolean(10, message.isIs_edited()); // is_edited + + if (message.getEdited_at() != null) + ps.setTimestamp(11, Timestamp.valueOf(message.getEdited_at())); // edited_at + else + ps.setNull(11, Types.TIMESTAMP); + + if (message.getOriginal_message_id() != null) + ps.setObject(12, message.getOriginal_message_id()); // original_message_id + else + ps.setNull(12, Types.OTHER); + + if (message.getForwarded_by() != null) + ps.setObject(13, message.getForwarded_by()); // forwarded_by + else + ps.setNull(13, Types.OTHER); + + if (message.getForwarded_from() != null) + ps.setObject(14, message.getForwarded_from()); // forwarded_from + else + ps.setNull(14, Types.OTHER); + + ps.setBoolean(15, message.getIs_deleted_globally()); // is_deleted_globally + + return ps.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } } diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index e82b57c..6b2ab1f 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -7,6 +7,10 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.UUID; public class PrivateChatDatabase { @@ -29,6 +33,34 @@ public class PrivateChatDatabase { return new ArrayList<>(); } + public static UUID getOrCreateSavedMessagesChat(UUID userId) { + String query = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; + try (Connection conn = ConnectionDb.connect()) { + PreparedStatement stmt = conn.prepareStatement(query); + stmt.setObject(1, userId); + stmt.setObject(2, userId); // Saved Messages is self-chat + + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("chat_id"); // Chat already exists + } else { + // Create a new chat + UUID chatId = UUID.randomUUID(); + String insert = "INSERT INTO private_chat (chat_id, user1_id, user2_id) VALUES (?, ?, ?)"; + try (PreparedStatement insertStmt = conn.prepareStatement(insert)) { + insertStmt.setObject(1, chatId); + insertStmt.setObject(2, userId); + insertStmt.setObject(3, userId); + insertStmt.executeUpdate(); + return chatId; + } + } + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + public static UUID findChatIdByUsers(UUID user1, UUID user2) { UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2; diff --git a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index 949629c..57adbf8 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -152,7 +152,7 @@ public class userDatabase { } public boolean save(User user) { - String query = "INSERT INTO users (user_id, internal_uuid, username, password, profile_name) VALUES (?, ?, ?, ?, ?)"; + String query = "INSERT INTO users (user_id, internal_uuid, username, password, profile_name, bio, image_url) VALUES (?, ?, ?, ?, ?, ?, ?)"; try { boolean var5; @@ -165,6 +165,8 @@ public class userDatabase { stmt.setString(3, user.getUsername()); stmt.setString(4, user.getPassword()); stmt.setString(5, user.getProfile_name()); + stmt.setString(6, user.getBio()); + stmt.setString(7, user.getImage_url()); var5 = stmt.executeUpdate() > 0; } @@ -223,7 +225,7 @@ public class userDatabase { } private User extractUser(ResultSet rs) throws SQLException { - return new User(rs.getString("user_id"), UUID.fromString(rs.getString("internal_uuid")), rs.getString("username"), rs.getString("password"), rs.getString("profile_name")); + return new User(rs.getString("user_id"), UUID.fromString(rs.getString("internal_uuid")), rs.getString("username"), rs.getString("password"), rs.getString("profile_name"), rs.getString("bio"), rs.getString("image_url")); } public boolean deleteByUUID(UUID uuid) { @@ -288,7 +290,9 @@ public class userDatabase { UUID.fromString(rs.getString("internal_uuid")), rs.getString("username"), rs.getString("password"), - rs.getString("profile_name") + rs.getString("profile_name"), + rs.getString("bio"), + rs.getString("image_url") ); user.setBio(rs.getString("bio")); @@ -349,6 +353,61 @@ public class userDatabase { } } + public static String getProfileName(UUID userId) { + String sql = "SELECT profile_name FROM users WHERE internal_uuid = ?"; + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + String profileName = rs.getString("profile_name"); + return profileName; + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return "Unknown"; + } + + public static String getProfilePicture(UUID userId) { + String sql = "SELECT image_url FROM users WHERE internal_uuid = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + String imageUrl = rs.getString("image_url"); + return imageUrl; + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return "Unknown"; + } + + public static String getUserId(UUID userId) { + String sql = "SELECT user_id FROM users WHERE internal_uuid = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + String user_id = rs.getString("user_id"); + return user_id; + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return "Unknown"; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 8176576..8f57ff8 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -14,6 +14,7 @@ public class ChatEntry { private LocalDateTime lastMessageTime; private boolean archived = false; private UUID otherUser; + private boolean savedMessages = false; private boolean isOwner = false; @@ -140,4 +141,12 @@ public class ChatEntry { public UUID getOtherUserId(){ return otherUser; } + + public boolean isSavedMessages() { + return savedMessages; + } + + public void setSavedMessages(boolean savedMessages) { + this.savedMessages = savedMessages; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java index a61bc6e..0a3a896 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java @@ -1,7 +1,7 @@ package org.to.telegramfinalproject.Models; +import java.time.LocalDateTime; import org.json.JSONObject; - import java.util.UUID; public class ContactEntry { @@ -10,8 +10,10 @@ public class ContactEntry { private String profileName; private String imageUrl; private boolean isBlocked; + private LocalDateTime lastSeenTime; private String contact_displayId; + public ContactEntry(UUID contactId, String userId, String profileName, String imageUrl, boolean isBlocked) { this.contactId = contactId; this.userId = userId; @@ -20,13 +22,14 @@ public class ContactEntry { this.isBlocked = isBlocked; } - public ContactEntry(UUID contactId, String userId,String contact_displayId , String profileName, String imageUrl, boolean isBlocked){ + public ContactEntry(UUID contactId, String userId,String contact_displayId , String profileName, String imageUrl, boolean isBlocked, LocalDateTime lastSeenTime){ this.contactId = contactId; this.userId = userId; this.contact_displayId = contact_displayId; this.profileName = profileName; this.imageUrl = imageUrl; this.isBlocked = isBlocked; + this.lastSeenTime = lastSeenTime; } public UUID getContactId() { @@ -49,9 +52,25 @@ public class ContactEntry { return isBlocked; } + public LocalDateTime getLastSeenTime() { + return lastSeenTime; + } + + public void setLastSeenTime(LocalDateTime lastSeenTime) { + this.lastSeenTime = lastSeenTime; + } + + public String getContact_displayId() { + return contact_displayId; + } + + public void setContact_displayId(String contact_displayId) { + this.contact_displayId = contact_displayId; + } + @Override public String toString() { - return profileName + " ( @" + contact_displayId + ")" + (isBlocked ? " [Blocked]" : ""); + return profileName + " (@" + contact_displayId + ")" + (isBlocked ? " [Blocked]" : "") + " Last Seen:" + (lastSeenTime != null ? " " + lastSeenTime.toString() : ""); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index 049d582..7bb8791 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -185,7 +185,7 @@ public class JsonUtil { obj.put("last_message_time", entry.getLastMessageTime() == null ? JSONObject.NULL : entry.getLastMessageTime().toString()); obj.put("is_owner", entry.isOwner()); obj.put("is_admin", entry.isAdmin()); - + obj.put("is_saved_messages", entry.isSavedMessages()); jsonArray.put(obj); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index 336998c..a519e33 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -16,15 +16,14 @@ public class Message { private String status; private UUID reply_to_id; private boolean is_edited; - private boolean is_deleted_globally; private UUID original_message_id; private UUID forwarded_by; private UUID forwarded_from; private List attachments; + private boolean is_deleted_globally; + private LocalDateTime edited_at; private transient String sender_name; private transient String receiver_name; - private LocalDateTime edited_at; - // āœ… Full Constructor @@ -69,6 +68,7 @@ public class Message { this.edited_at = edited_at; } + // āœ… Short Constructors //for normal messages public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, @@ -190,13 +190,24 @@ public class Message { this.receiver_name = receiver_name; } - public LocalDateTime getEdited_at() { - return edited_at; - } + public LocalDateTime getEdited_at() { + return edited_at; + } public void setEdited_at(LocalDateTime edited_at) { this.edited_at = edited_at; } + public boolean getIs_deleted_globally() { + return is_deleted_globally; + } +// +// public void setEdited_at(LocalDateTime edited_at) { +// this.edited_at = edited_at; +// } +// +// public LocalDateTime getEdited_at() { +// return edited_at; +// } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java b/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java index 036a801..d0e3cb1 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java +++ b/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java @@ -2,7 +2,7 @@ package org.to.telegramfinalproject.Models; public class SearchResultModel { private final String type; - private final String id; // ← UUID ŁˆŲ§Ł‚Ų¹ŪŒ برای Ų¹Ł…Ł„ŪŒŲ§ŲŖ + private final String id; // ← UUID private final String displayId; // ← user_id یا group_id برای Ł†Ł…Ų§ŪŒŲ“ private String name; private final String content; diff --git a/src/main/java/org/to/telegramfinalproject/Models/User.java b/src/main/java/org/to/telegramfinalproject/Models/User.java index 73ac635..a8d9e29 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/User.java +++ b/src/main/java/org/to/telegramfinalproject/Models/User.java @@ -22,12 +22,14 @@ public class User { private List unreadMessages; private List chatList; - public User(String user_id, UUID internal_uuid, String username, String password, String profile_name) { + public User(String user_id, UUID internal_uuid, String username, String password, String profile_name, String bio, String image_url) { this.user_id = user_id; this.internal_uuid = internal_uuid; this.username = username; this.password = password; this.profile_name = profile_name; + this.bio = bio; + this.image_url = image_url; } public void setUser_id(String user_id) { diff --git a/src/main/java/org/to/telegramfinalproject/Server/AuthService.java b/src/main/java/org/to/telegramfinalproject/Server/AuthService.java index 1534c46..f50da87 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/AuthService.java +++ b/src/main/java/org/to/telegramfinalproject/Server/AuthService.java @@ -20,7 +20,7 @@ public class AuthService { } else { UUID uuid = UUID.randomUUID(); password = PasswordHashing.hash(password); - User user = new User(user_id, uuid, username, password, profile_name); + User user = new User(user_id, uuid, username, password, profile_name, "", ""); return this.userDb.save(user); } } else { diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 6ca7e2c..121772f 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1,5 +1,6 @@ package org.to.telegramfinalproject.Server; +import javafx.geometry.Side; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Database.*; @@ -118,6 +119,7 @@ public class ClientHandler implements Runnable { c.put("profile_name", target.getProfile_name()); c.put("image_url", target.getImage_url()); + c.put("last_seen", Contact.getLast_seen()); contactList.put(c); } @@ -182,6 +184,10 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); + if (currentUser.getInternal_uuid() == otherId) { + entry.setSavedMessages(true); + } + if (archivedChatIds.contains(chat.getChat_id())) { archivedChatList.add(entry); chatList.add(entry); @@ -718,6 +724,8 @@ public class ClientHandler implements Runnable { LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); + boolean isSavedMessages = chat.getUser1_id().equals(currentUser.getInternal_uuid()) + && chat.getUser2_id().equals(currentUser.getInternal_uuid()); ChatEntry entry = new ChatEntry( chat.getChat_id(), @@ -731,6 +739,11 @@ public class ClientHandler implements Runnable { ); entry.setOtherUserId(otherId); + // Mark it as saved messages if it's the special self-chat + if (isSavedMessages) { + entry.setSavedMessages(true); + } + if (archivedChatIds.contains(chat.getChat_id())) { archivedChatList.add(entry); chatList.add(entry); @@ -2412,8 +2425,103 @@ public class ClientHandler implements Runnable { break; } + case "get_user_profile": { + // Get the current user's UUID + UUID user_UUID = this.currentUser.getInternal_uuid(); + // Get the profile JSON from SidebarService + JSONObject data = SidebarService.getUserProfile(user_UUID); + if (data == null) { + response = new ResponseModel("error", "Failed to retrieve user profile."); + } else { + response = new ResponseModel("success", "User profile retrieved successfully.", data); + } + break; + } + + case "edit_profile_name": { + // Validate input + if (!requestJson.has("new_profile_name")) { + response = new ResponseModel("error", "Missing new profile name."); + break; + } + + UUID user_UUID = this.currentUser.getInternal_uuid(); + String newProfileName = requestJson.getString("new_profile_name"); + + response = SidebarService.updateProfileName(user_UUID, newProfileName); + break; + } + + case "edit_user_id": { + // Validate input + if (!requestJson.has("new_user_id")) { + response = new ResponseModel("error", "Missing new user ID."); + break; + } + + UUID user_UUID = this.currentUser.getInternal_uuid(); + String newUserId = requestJson.getString("new_user_id"); + + response = SidebarService.updateUserId(user_UUID, newUserId); + break; + } + + case "edit_bio": { + // Validate input + if (!requestJson.has("new_bio")) { + response = new ResponseModel("error", "Missing new bio."); + break; + } + + UUID user_UUID = this.currentUser.getInternal_uuid(); + String newBio = requestJson.getString("new_bio").trim(); + + response = SidebarService.updateBio(user_UUID, newBio); + break; + } + + case "edit_profile_picture": { + // Validate input + if (!requestJson.has("new_image_url")) { + response = new ResponseModel("error", "Missing new image url."); + break; + } + + UUID user_UUID = this.currentUser.getInternal_uuid(); + String newImageUrl = requestJson.getString("new_image_url").trim(); + + response = SidebarService.updateProfilePicture(user_UUID, newImageUrl); + break; + } + + case "get_saved_messages": { + UUID user_Id = UUID.fromString(requestJson.getString("user_id")); +// response = SidebarService.handleGetSavedMessages(user_Id); + break; + } + + case "send_saved_messages": { + response = SidebarService.handleSendMessage(requestJson); + break; + } + + case "search_contacts": { + String user_id = requestJson.getString("user_id"); + String search_term = requestJson.getString("search_term"); + + response = SidebarService.handleSearchContacts(user_id, search_term); + break; + } + + case "remove_contact": { + UUID user_id = UUID.fromString(requestJson.getString("user_id")); + UUID contactId = UUID.fromString(requestJson.getString("contact_id")); + + response = SidebarService.handleRemoveContact(user_id, contactId); + break; + } default: response = new ResponseModel("error", "Unknown action: " + action); diff --git a/src/main/java/org/to/telegramfinalproject/Server/SidebarService.java b/src/main/java/org/to/telegramfinalproject/Server/SidebarService.java new file mode 100644 index 0000000..a10035e --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/SidebarService.java @@ -0,0 +1,297 @@ +package org.to.telegramfinalproject.Server; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Database.ContactDatabase; +import org.to.telegramfinalproject.Database.MessageDatabase; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; +import org.to.telegramfinalproject.Database.userDatabase; +import org.to.telegramfinalproject.Models.ContactEntry; +import org.to.telegramfinalproject.Models.Message; +import org.to.telegramfinalproject.Models.ResponseModel; +import org.to.telegramfinalproject.Models.User; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +public class SidebarService { + private static userDatabase userDB = new userDatabase(); + + // Returns current user's profile data (name, bio, status, etc.) + public static JSONObject getUserProfile(UUID userUUID) { + User user = userDB.findByInternalUUID(userUUID); + + if (user == null) { + return null; + } + + JSONObject profile = new JSONObject(); + profile.put("user_id", user.getUser_id()); + profile.put("profile_name", user.getProfile_name()); + profile.put("bio", user.getBio() != null ? user.getBio() : ""); + profile.put("status", "ONLINE"); + profile.put("profile_picture_url", user.getImage_url()); + + return profile; + } + + // Changes the user's profile picture + public static ResponseModel updateProfilePicture(UUID userUUID, String newImageUrl) { + User user = userDB.findByInternalUUID(userUUID); + if (user == null) { + return new ResponseModel("error", "User not found."); + } + + String currentUrl = user.getImage_url(); + if (Objects.equals(currentUrl, newImageUrl)) { + return new ResponseModel("error", "No changes made. Profile picture is the same."); + } + + user.setImage_url(newImageUrl); // Can be null + boolean success = userDB.updateByUUID(userUUID, user); + + if (success) { + return new ResponseModel("success", "Profile picture updated successfully."); + } else { + user.setImage_url(currentUrl); + return new ResponseModel("error", "Failed to update profile picture."); + } + } + + // Changes user's bio + public static ResponseModel updateBio(UUID userUUID, String newBio) { + if (newBio.length() > 70) { + return new ResponseModel("error", "Bio is too long."); + } + + User user = userDB.findByInternalUUID(userUUID); + if (user == null) { + return new ResponseModel("error", "User not found."); + } + + String currentBio = user.getBio(); + if (Objects.equals(currentBio, newBio)) { + return new ResponseModel("error", "No changes made. Bio is the same."); + } + + user.setBio(newBio); + boolean success = userDB.updateByUUID(userUUID, user); + + if (success) { + return new ResponseModel("success", "Bio updated successfully."); + } else { + user.setBio(currentBio); + return new ResponseModel("error", "Failed to update bio."); + } + } + + // Changes user-id + public static ResponseModel updateUserId(UUID userUUID, String newUserId) { + if (newUserId == null || newUserId.trim().isEmpty()) { + return new ResponseModel("error", "User ID cannot be empty."); + } + + newUserId = newUserId.trim(); + + if (newUserId.contains(" ")) { + return new ResponseModel("error", "User ID cannot contain spaces."); + } + + if (!newUserId.matches("^[a-zA-Z0-9_]+$")) { + return new ResponseModel("error", "User ID can only contain letters, digits, and underscores."); + } + + if (userDB.findByUserId(newUserId) != null) { + return new ResponseModel("error", "This user ID is already taken."); + } + + User user = userDB.findByInternalUUID(userUUID); + if (user == null) { + return new ResponseModel("error", "User not found."); + } + + String currentUserId = user.getUser_id(); + if (Objects.equals(currentUserId, newUserId)) { + return new ResponseModel("error", "No changes made. User ID is the same."); + } + + user.setUser_id(newUserId); + + boolean saved = userDB.updateByUUID(userUUID, user); + if (saved) { + return new ResponseModel("success", "User ID updated successfully."); + } else { + user.setUser_id(currentUserId); + return new ResponseModel("error", "Failed to update user ID due to server error."); + } + } + + // Changes user's profile name + public static ResponseModel updateProfileName(UUID userUUID, String newProfileName) { + if (newProfileName == null || newProfileName.trim().isEmpty()) { + return new ResponseModel("error", "Invalid input."); // Invalid input (empty or just spaces) + } + + // Fetch the user from database + User user = userDB.findByInternalUUID(userUUID); + if (user == null) { + return new ResponseModel("error", "User not found."); // User doesn't exist + } + + String oldName = user.getProfile_name(); + if (Objects.equals(oldName, newProfileName)) { + return new ResponseModel("error", "No changes made. Profile name is the same."); + } + + // Only set the profile name *after* successful DB update + user.setProfile_name(newProfileName); + + boolean saved = userDB.updateByUUID(userUUID, user); + + if (saved) { + return new ResponseModel("success", "Profile name updated successfully."); + } else { + user.setProfile_name(oldName); + return new ResponseModel("error", "Failed to update profile name."); + } + } + + // Search in user's contact list + public static ResponseModel handleSearchContacts(String userUUID, String searchTerm) { + if (searchTerm == null || searchTerm.trim().isEmpty()) { + return new ResponseModel("error", "Search term cannot be empty."); + } + + List searchResult = ContactDatabase.searchContacts(UUID.fromString(userUUID), searchTerm); + + if (searchResult.isEmpty()) { + return new ResponseModel("error", "No contacts found."); + } + + JSONObject data = new JSONObject(); + JSONArray contacts = new JSONArray(); + + for (ContactEntry entry : searchResult) { + contacts.put(new JSONObject() + .put("contact_id", entry.getContactId()) + .put("user_id", entry.getUserId()) + .put("contact_display_id", entry.getContact_displayId()) + .put("profile_name", entry.getProfileName()) + .put("image_url", entry.getImageUrl()) + .put("is_blocked", entry.isBlocked()) + .put("last_seen", entry.getLastSeenTime()) + ); + } + data.put("contacts", contacts); + + if (searchResult.isEmpty()) { + return new ResponseModel("error", "No contacts found."); + } + + return new ResponseModel("success", "Search contacts successfully.", data); + } + + // Remove a contact in user's contact list + public static ResponseModel handleRemoveContact(UUID userUUID, UUID contactId) { + if (userUUID == null || contactId == null) { + return new ResponseModel("error", "Invalid input."); + } + + boolean removed = ContactDatabase.removeContact(userUUID, contactId); + + if (removed) { + return new ResponseModel("success", "Contact removed successfully."); + } else { + return new ResponseModel("error", "Contact not found."); + } + } + + // Get saved messages data +// public static ResponseModel handleGetSavedMessages(UUID userId) { +// try { +// +// UUID chatId = PrivateChatDatabase.getOrCreateSavedMessagesChat(userId); +// if (chatId == null) { +// return new ResponseModel("error", "Failed to create or find saved messages chat."); +// } +// +// List messages = MessageDatabase.privateChatHistory(chatId); +// +// JSONArray messageArray = new JSONArray(); +// if (!messages.isEmpty()) { +// for (Message msg : messages) { +// JSONObject msgJson = new JSONObject(); +// msgJson.put("message_id", msg.getMessage_id().toString()); +// msgJson.put("sender_id", msg.getSender_id().toString()); +// msgJson.put("receiver_type", msg.getReceiver_type()); +// msgJson.put("receiver_id", msg.getReceiver_id().toString()); +// msgJson.put("content", msg.getContent()); +// msgJson.put("message_type", msg.getMessage_type()); +// msgJson.put("send_at", msg.getSend_at().toString()); // LocalDateTime +// msgJson.put("status", msg.getStatus()); +// msgJson.put("reply_to_id", msg.getReply_to_id() != null ? msg.getReply_to_id().toString() : JSONObject.NULL); +// msgJson.put("is_edited", msg.isIs_edited()); +// msgJson.put("original_message_id", msg.getOriginal_message_id() != null ? msg.getOriginal_message_id().toString() : JSONObject.NULL); +// msgJson.put("forwarded_by", msg.getForwarded_by() != null ? msg.getForwarded_by().toString() : JSONObject.NULL); +// msgJson.put("forwarded_from", msg.getForwarded_from() != null ? msg.getForwarded_from().toString() : JSONObject.NULL); +// +// messageArray.put(msgJson); +// } +// } +// +// JSONObject data = new JSONObject(); +// data.put("chat_id", chatId.toString()); +// data.put("messages", messageArray); +// +// return new ResponseModel("success", "Saved messages retrieved successfully", data); +// +// } catch (Exception e) { +// e.printStackTrace(); +// return new ResponseModel("error", "Unexpected server error."); +// } +// } + + // Save messages to DB + public static ResponseModel handleSendMessage(JSONObject requestJson) { + try { + Message message = new Message( + UUID.fromString(requestJson.getString("message_id")), + UUID.fromString(requestJson.getString("sender_id")), + requestJson.getString("receiver_type"), + UUID.fromString(requestJson.getString("receiver_id")), + requestJson.optString("content", null), + requestJson.optString("message_type", "TEXT"), + LocalDateTime.now(), // send_at + requestJson.optString("status", "SEND"), + requestJson.isNull("reply_to_id") ? null : UUID.fromString(requestJson.getString("reply_to_id")), + requestJson.optBoolean("is_edited", false), + requestJson.isNull("original_message_id") ? null : UUID.fromString(requestJson.getString("original_message_id")), + requestJson.isNull("forwarded_by") ? null : UUID.fromString(requestJson.getString("forwarded_by")), + requestJson.isNull("forwarded_from") ? null : UUID.fromString(requestJson.getString("forwarded_from")), + requestJson.optBoolean("is_deleted_globally", false), + requestJson.isNull("edited_at") ? null : + LocalDateTime.ofInstant( + Instant.ofEpochMilli(requestJson.getLong("edited_at")), + ZoneId.systemDefault() + ) + ); + + MessageDatabase.insertSavedMessage(message); + return new ResponseModel("success", "Message saved successfully."); + + } catch (Exception e) { + e.printStackTrace(); + return new ResponseModel("error", "Unexpected server error."); + } + } + + // Changes username + public static boolean updateUserName(String userId, String newUserName) { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java new file mode 100644 index 0000000..9a6915d --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/SidebarMenuController.java @@ -0,0 +1,31 @@ +package org.to.telegramfinalproject.UI; + +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +public class SidebarMenuController { + @FXML private ImageView profileImage; + @FXML private Label usernameLabel; + + @FXML private Button myProfileButton; + @FXML private Button newGroupButton; + @FXML private Button newChannelButton; + @FXML private Button contactsButton; + @FXML private Button savedMessagesButton; + @FXML private Button settingsButton; + @FXML private Button telegramFeaturesButton; + @FXML private Button telegramQnAButton; + + @FXML private ToggleButton nightModeToggle; + + @FXML + public void initialize() { + Image image = new Image(getClass().getResource("/org/to/telegramfinalproject/Images/profile.png").toExternalForm()); + profileImage.setImage(image); + + // Called automatically after FXML is loaded + usernameLabel.setText("Asal"); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java index 47a1970..596c359 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java +++ b/src/main/java/org/to/telegramfinalproject/UI/TelegramApplication.java @@ -12,8 +12,11 @@ import java.io.IOException; public class TelegramApplication extends Application { @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/Intro.fxml")); + FXMLLoader fxmlLoader = new FXMLLoader(TelegramApplication.class.getResource("/org/to/telegramfinalproject/sidebar_menu.fxml")); Scene scene = new Scene(fxmlLoader.load(), 1480, 820); + + scene.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/sidebar_menu.css").toExternalForm()); + stage.setTitle("Telegram"); stage.setScene(scene); stage.show(); diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index f07532e..e5b9720 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -27,24 +27,28 @@ CREATE TABLE IF NOT EXISTS messages ( receiver_id UUID NOT NULL , content TEXT, message_type VARCHAR(20) DEFAULT 'TEXT' CHECK (message_type IN ('TEXT','IMAGE','FILE','VIDEO','AUDIO','STICKER','GIF')), - file_url TEXT, --(Bouns) send_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(20), -- SEND,DELIVERED,READ + status VARCHAR(20) DEFAULT 'SEND', -- SEND,DELIVERED,READ reply_to_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bouns) - is_edited BOOLEAN DEFAULT FALSE, --(Bonus) - original_message_id UUID REFERENCES messages(message_id) - ON DELETE SET NULL, --(Bonus) + is_edited BOOLEAN DEFAULT FALSE, --(Bonus) + edited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + original_message_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bonus) forwarded_by UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) - forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL --(Bouns) + forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) + is_deleted_globally BOOLEAN DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS private_chat ( chat_id UUID PRIMARY KEY, user1_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, user2_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, + user1_deleted BOOLEAN DEFAULT FALSE, + user2_deleted BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_users UNIQUE (user1_id, user2_id) ); + + CREATE TABLE IF NOT EXISTS groups ( internal_uuid UUID PRIMARY KEY, group_id VARCHAR(70) UNIQUE, @@ -67,7 +71,7 @@ CREATE TABLE group_members ( CREATE TABLE IF NOT EXISTS channels ( channel_id VARCHAR(70) UNIQUE, - internal_uuid UUID PRIMARY KEY,, + internal_uuid UUID PRIMARY KEY, channel_name VARCHAR(100), creator_id UUID REFERENCES users(internal_uuid), image_url TEXT, @@ -119,3 +123,38 @@ CREATE TABLE archived_chats ( ); +CREATE TABLE deleted_messages ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + + +CREATE TABLE IF NOT EXISTS message_reactions ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + emoji TEXT NOT NULL, + reacted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + +--Multi attachments +CREATE TABLE IF NOT EXISTS message_attachments ( + attachment_id UUID PRIMARY KEY, + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_type VARCHAR(20) CHECK (file_type IN ('IMAGE','VIDEO','AUDIO','FILE','GIF','STICKER')), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + file_name TEXT, + file_size BIGINT, + mime_type TEXT, + width INT, + height INT, + duration_seconds INT, + thumbnail_url TEXT + +); + + + diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/sidebar_menu.css b/src/main/resources/org/to/telegramfinalproject/CSS/sidebar_menu.css new file mode 100644 index 0000000..061edd5 --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/CSS/sidebar_menu.css @@ -0,0 +1,14 @@ +.sidebar-btn { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-pref-width: 230; + -fx-alignment: CENTER_LEFT; + -fx-padding: 8 0 8 16; +} + +.sidebar-btn:hover { + -fx-background-color: #2f3e50; + -fx-cursor: hand; + -fx-text-fill: white; +} \ No newline at end of file diff --git a/src/main/resources/org/to/telegramfinalproject/Images/profile.png b/src/main/resources/org/to/telegramfinalproject/Images/profile.png new file mode 100644 index 0000000..6aa1089 Binary files /dev/null and b/src/main/resources/org/to/telegramfinalproject/Images/profile.png differ diff --git a/src/main/resources/org/to/telegramfinalproject/sidebar_menu.fxml b/src/main/resources/org/to/telegramfinalproject/sidebar_menu.fxml new file mode 100644 index 0000000..2065f58 --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/sidebar_menu.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +