Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2bfcfcda8 | ||
|
|
bf73e11be4 | ||
|
|
e129c7ff99 | ||
|
|
31b5500cbe | ||
|
|
33832d4159 | ||
|
|
4a4e646940 | ||
|
|
d2b57157d6 | ||
|
|
fa1c9636c1 | ||
|
|
da6687fd98 | ||
|
|
730ddba857 | ||
|
|
41c30aff7b | ||
|
|
7a5eb0a8ab | ||
|
|
1aa422b3c1 | ||
|
|
a4e8ee76ad | ||
|
|
4d7cfecd05 | ||
|
|
98a5d85f55 | ||
|
|
774a570d99 | ||
|
|
94f2bc7d17 | ||
|
|
77ac3d3760 | ||
|
|
6fc2b2e2ed | ||
|
|
021fab4e68 | ||
|
|
f2e8720d36 |
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
## Introduction
|
||||||
|
This is the final project of our [**Advanced Programming**](https://github.com/Advanced-Programming-1403) course at Shahid Beheshti University.
|
||||||
|
It’s a clone of the famous messaging application **Telegram**, developed in **Java** and designed with **JavaFX**.
|
||||||
|
|
||||||
|
The project implements both **client-side** and **server-side** logic, featuring real-time messaging, saved messages, contacts, groups, and channels.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
Here is a list of concepts that were practiced and implemented throughout the project:
|
||||||
|
- Object-Oriented Programming (OOP) concepts
|
||||||
|
- Database design and integration
|
||||||
|
- Multithreading and concurrency control
|
||||||
|
- Socket programming for client-server communication
|
||||||
|
- Designing graphical user interfaces with **JavaFX**
|
||||||
|
- Data handling and persistence with **PostgreSQL**
|
||||||
|
- Real-time events and message synchronization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre Requirements
|
||||||
|
- **Java** (JDK 23 or higher)
|
||||||
|
- **PostgreSQL** (for database)
|
||||||
|
- **JavaFX SDK** (for GUI)
|
||||||
|
- **IntelliJ IDEA**
|
||||||
|
- **Gradle** as the build system
|
||||||
|
|
||||||
|
---
|
||||||
|
## Database design
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
The project has 3 main parts:
|
||||||
|
|
||||||
|
1. **Telegram Client**
|
||||||
|
- JavaFX-based UI replicating Telegram’s layout (intro, login, chats, sidebar, overlays).
|
||||||
|
- Handles sending/receiving requests through sockets.
|
||||||
|
- Displays messages, saved chats, groups, and user profiles.
|
||||||
|
|
||||||
|
2. **Telegram Server**
|
||||||
|
- Multithreaded server managing client connections concurrently.
|
||||||
|
- Processes requests, accesses the database, and returns structured JSON responses.
|
||||||
|
- Includes a server log showing all events and queries in real-time.
|
||||||
|
|
||||||
|
3. **Sockets**
|
||||||
|
- All client-server communication is handled via sockets.
|
||||||
|
- Each request/response follows a structured JSON protocol.
|
||||||
|
- Supports multiple users communicating simultaneously.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating GUI
|
||||||
|
The GUI was developed entirely with [**JavaFX**](https://en.wikipedia.org/wiki/JavaFX), SceneBuilder, and custom CSS.
|
||||||
|
Features include:
|
||||||
|
- Light/Dark theme switching
|
||||||
|
- Responsive sidebar navigation
|
||||||
|
- Overlays (e.g., Saved Messages, My Profile, Blocked Users)
|
||||||
|
- Round avatars and icons styled to match Telegram’s UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Structures
|
||||||
|
Implemented with **PostgreSQL**. Main tables include:
|
||||||
|
- **users** – stores account information
|
||||||
|
- **messages** – stores chat messages with sender, receiver, timestamp, and status
|
||||||
|
- **private_chat** – for one-on-one conversations (including Saved Messages)
|
||||||
|
- **group_chat / channel** – for group and channel features
|
||||||
|
- **message_receipts** – to track read/delivery status
|
||||||
|
|
||||||
|
*(SQL schema files are provided in the project repo)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Presentation
|
||||||
|
<img width="2093" height="1122" alt="Screenshot 2025-09-08 005216" src="https://github.com/user-attachments/assets/b370b2bb-f84a-400d-81c4-f945c8c1e489" />
|
||||||
|
|
||||||
|
<img width="2239" height="1357" alt="Screenshot 2025-09-08 004526" src="https://github.com/user-attachments/assets/70ea1746-0e2e-45d2-a5d2-33c6e15f8dd3" />
|
||||||
|
|
||||||
|
<img width="2248" height="1429" alt="Screenshot 2025-09-08 004157" src="https://github.com/user-attachments/assets/61f1fc0c-452c-42da-bf0e-aa68aee06047" />
|
||||||
|
|
||||||
|
<img width="2147" height="1248" alt="Screenshot 2025-09-08 005228" src="https://github.com/user-attachments/assets/73874dec-ac9c-4948-a421-379cc1583720" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## How to Run the Code
|
||||||
|
1. Clone the repository.
|
||||||
|
2. Set up the PostgreSQL database using the provided `init.sql`.
|
||||||
|
3. In IntelliJ IDEA (with Gradle):
|
||||||
|
- Run `MainServer` first. You should see `🔁 All users set to offline` in the console.
|
||||||
|
- Run `TelegramApplication`. The intro screen (with “Start Messaging”) will appear.
|
||||||
|
4. Login/Register to start chatting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
- Course Instructor: [Dr. Saeed Reza Kheradpisheh](https://www.linkedin.com/in/saeed-reza-kheradpisheh-7a0b18155/)
|
||||||
|
- Project Mentor: [Farid Karimi](https://github.com/Farid-Karimi/)
|
||||||
|
- Team Members:
|
||||||
|
- [Asal Lotfi](https://github.com/AsalLotfi/)
|
||||||
|
- [Partow Roshani](https://github.com/PartowRoshani/)
|
||||||
|
- [Shima Morevej](https://github.com/shimamoravvej/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Date/Time
|
||||||
|
Summer of 2025 (Version 1.0)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- [JavaFX Documentation](https://openjfx.io/)
|
||||||
|
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
||||||
|
- [Gradle Build Tool](https://gradle.org/)
|
||||||
|
- [Telegram Design Reference](https://telegram.org/)
|
||||||
@@ -26,7 +26,7 @@ tasks.withType(JavaCompile) {
|
|||||||
|
|
||||||
application {
|
application {
|
||||||
mainModule = 'org.to.telegramfinalproject'
|
mainModule = 'org.to.telegramfinalproject'
|
||||||
mainClass = 'org.to.telegramfinalproject.HelloApplication'
|
mainClass = 'org.to.telegramfinalproject.Server.MainServer'
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
module org.to.telegramfinalproject {
|
module org.to.telegramfinalproject {
|
||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
|
requires javafx.graphics; // بهتره اضافه باشه
|
||||||
|
requires javafx.media;
|
||||||
requires javafx.web;
|
requires javafx.web;
|
||||||
|
|
||||||
|
requires org.json;
|
||||||
|
requires java.sql;
|
||||||
|
requires java.desktop;
|
||||||
|
|
||||||
|
// لایبرریهای جانبی — اگر واقعاً در مسیر ماژول هستند
|
||||||
requires org.controlsfx.controls;
|
requires org.controlsfx.controls;
|
||||||
requires com.dlsc.formsfx;
|
requires com.dlsc.formsfx;
|
||||||
requires net.synedra.validatorfx;
|
requires net.synedra.validatorfx;
|
||||||
requires org.kordamp.ikonli.javafx;
|
requires org.kordamp.ikonli.javafx;
|
||||||
requires org.kordamp.bootstrapfx.core;
|
requires org.kordamp.bootstrapfx.core;
|
||||||
requires eu.hansolo.tilesfx;
|
requires eu.hansolo.tilesfx;
|
||||||
requires org.json;
|
|
||||||
requires java.sql;
|
|
||||||
requires java.desktop;
|
|
||||||
requires spark.core;
|
|
||||||
requires javax.servlet.api;
|
|
||||||
requires mp3agic;
|
requires mp3agic;
|
||||||
|
requires spark.core; // فقط اگر واقعاً استفاده میکنی
|
||||||
|
requires javax.servlet.api; // فقط اگر واقعاً استفاده میکنی
|
||||||
|
|
||||||
// FXML کنترلرها در این پکیجاند:
|
// کنترلرها/کلاسهایی که FXML به آنها دسترسی بازتابی دارد
|
||||||
opens org.to.telegramfinalproject.UI to javafx.fxml;
|
opens org.to.telegramfinalproject.UI to javafx.fxml;
|
||||||
// اگر FXML از کلاسهای Client هم استفاده میکند:
|
|
||||||
opens org.to.telegramfinalproject.Client to javafx.fxml;
|
opens org.to.telegramfinalproject.Client to javafx.fxml;
|
||||||
|
opens org.to.telegramfinalproject.Models to javafx.fxml; // اگر مدلها داخل FXML مصرف میشوند
|
||||||
|
|
||||||
// فقط اگر لازم است از بیرون به این APIها کامپایل شود:
|
// اگر پکیجهایی را میخواهی خارج از ماژول در دسترس قرار دهی
|
||||||
|
exports org.to.telegramfinalproject.UI;
|
||||||
exports org.to.telegramfinalproject.Client;
|
exports org.to.telegramfinalproject.Client;
|
||||||
// ⚠️ عمداً NOT exporting/opens ریشهی پکیج، چون کلاس ندارد.
|
exports org.to.telegramfinalproject.Models;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -691,4 +691,83 @@ public class ChannelDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JSONObject getChannelInfo(UUID channelId, UUID viewerUuid) throws SQLException {
|
||||||
|
JSONObject result = new JSONObject();
|
||||||
|
|
||||||
|
try (Connection conn = ConnectionDb.connect()) {
|
||||||
|
// === Channel header (name, subscriber count, etc.)
|
||||||
|
String channelQuery = """
|
||||||
|
SELECT c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description,
|
||||||
|
COUNT(s.user_id) as subscriber_count
|
||||||
|
FROM channels c
|
||||||
|
LEFT JOIN channel_subscribers s ON c.internal_uuid = s.channel_id
|
||||||
|
WHERE c.internal_uuid = ?
|
||||||
|
GROUP BY c.internal_uuid, c.channel_id, c.channel_name, c.image_url, c.description
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(channelQuery)) {
|
||||||
|
ps.setObject(1, channelId);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
result.put("internal_uuid", rs.getString("internal_uuid"));
|
||||||
|
result.put("channel_id", rs.getString("channel_id")); // varchar if needed
|
||||||
|
result.put("channel_name", rs.getString("channel_name"));
|
||||||
|
result.put("subscriber_count", rs.getInt("subscriber_count"));
|
||||||
|
result.put("image_url", rs.getString("image_url"));
|
||||||
|
result.put("description", rs.getString("description")); // ✅ here
|
||||||
|
} else {
|
||||||
|
return null; // no such channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Subscribers (id, name, role, status, image)
|
||||||
|
JSONArray subscribersArr = new JSONArray();
|
||||||
|
|
||||||
|
String subscribersQuery = """
|
||||||
|
SELECT u.internal_uuid, u.profile_name, u.user_id, u.image_url,
|
||||||
|
cs.role, u.status, u.last_seen
|
||||||
|
FROM channel_subscribers cs
|
||||||
|
JOIN users u ON cs.user_id = u.internal_uuid
|
||||||
|
WHERE cs.channel_id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(subscribersQuery)) {
|
||||||
|
ps.setObject(1, channelId);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
JSONObject sub = new JSONObject();
|
||||||
|
sub.put("user_id", rs.getString("internal_uuid"));
|
||||||
|
sub.put("profile_name", rs.getString("profile_name"));
|
||||||
|
sub.put("username", rs.getString("user_id"));
|
||||||
|
sub.put("image_url", rs.getString("image_url"));
|
||||||
|
sub.put("role", rs.getString("role"));
|
||||||
|
sub.put("status", rs.getString("status"));
|
||||||
|
sub.put("last_seen", rs.getString("last_seen"));
|
||||||
|
subscribersArr.put(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("subscribers", subscribersArr);
|
||||||
|
|
||||||
|
// === Viewer role (so UI knows if viewer can manage/delete)
|
||||||
|
String roleQuery = """
|
||||||
|
SELECT role
|
||||||
|
FROM channel_subscribers
|
||||||
|
WHERE channel_id = ? AND user_id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(roleQuery)) {
|
||||||
|
ps.setObject(1, channelId);
|
||||||
|
ps.setObject(2, viewerUuid);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
result.put("my_role", rs.getString("role"));
|
||||||
|
} else {
|
||||||
|
result.put("my_role", ""); // not a subscriber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Server;
|
|||||||
import javafx.geometry.Side;
|
import javafx.geometry.Side;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.Session;
|
||||||
import org.to.telegramfinalproject.Database.*;
|
import org.to.telegramfinalproject.Database.*;
|
||||||
import org.to.telegramfinalproject.Models.*;
|
import org.to.telegramfinalproject.Models.*;
|
||||||
import org.to.telegramfinalproject.Security.PasswordHashing;
|
import org.to.telegramfinalproject.Security.PasswordHashing;
|
||||||
@@ -3061,6 +3062,30 @@ public class ClientHandler implements Runnable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "view_channel": {
|
||||||
|
if (currentUser == null) {
|
||||||
|
response = new ResponseModel("error", "Unauthorized. Please login first.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
UUID channelId = UUID.fromString(requestJson.getString("channel_id"));
|
||||||
|
UUID viewerId = currentUser.getInternal_uuid(); // logged-in user
|
||||||
|
|
||||||
|
// Query channel details
|
||||||
|
JSONObject channelData = ChannelDatabase.getChannelInfo(channelId, viewerId);
|
||||||
|
|
||||||
|
if (channelData == null) {
|
||||||
|
response = new ResponseModel("error", "Channel not found.");
|
||||||
|
} else {
|
||||||
|
response = new ResponseModel("success", "Channel info fetched.", channelData);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
response = new ResponseModel("error", "Error processing channel info: " + e.getMessage());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
response = new ResponseModel("error", "Unknown action: " + action);
|
response = new ResponseModel("error", "Unknown action: " + action);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public class AddMembersController {
|
|||||||
@FXML private Button cancelButton;
|
@FXML private Button cancelButton;
|
||||||
@FXML private Button createButton;
|
@FXML private Button createButton;
|
||||||
|
|
||||||
// گروه هدف
|
|
||||||
private UUID groupInternalId;
|
private UUID groupInternalId;
|
||||||
private String groupName;
|
private String groupName;
|
||||||
private String groupDisplayId;
|
private String groupDisplayId;
|
||||||
@@ -42,9 +41,7 @@ public class AddMembersController {
|
|||||||
|
|
||||||
private Mode mode = Mode.CREATE; // default
|
private Mode mode = Mode.CREATE; // default
|
||||||
|
|
||||||
// انتخابها
|
|
||||||
private final Set<Contact> selectedContacts = new HashSet<>();
|
private final Set<Contact> selectedContacts = new HashSet<>();
|
||||||
// همهی کانتکتها
|
|
||||||
private final List<Contact> allContacts = new ArrayList<>();
|
private final List<Contact> allContacts = new ArrayList<>();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -61,7 +58,6 @@ public class AddMembersController {
|
|||||||
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
||||||
});
|
});
|
||||||
|
|
||||||
// آیکن سرچ با تم
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (addMembersCard.getScene() != null) {
|
if (addMembersCard.getScene() != null) {
|
||||||
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
|
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
|
||||||
@@ -70,19 +66,15 @@ public class AddMembersController {
|
|||||||
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
|
||||||
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
||||||
|
|
||||||
// بستن اُورلی
|
|
||||||
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
||||||
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
||||||
|
|
||||||
// دکمه Add (افزودن اعضا)
|
|
||||||
createButton.setOnAction(e -> onAddMembers());
|
createButton.setOnAction(e -> onAddMembers());
|
||||||
|
|
||||||
// سرچ
|
|
||||||
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
|
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
|
||||||
|
|
||||||
Platform.runLater(() -> searchField.requestFocus());
|
Platform.runLater(() -> searchField.requestFocus());
|
||||||
|
|
||||||
// دیتا را از Session بارگذاری کن
|
|
||||||
loadContactsFromSession();
|
loadContactsFromSession();
|
||||||
updateMemberCount();
|
updateMemberCount();
|
||||||
renderContacts(allContacts);
|
renderContacts(allContacts);
|
||||||
@@ -132,7 +124,6 @@ public class AddMembersController {
|
|||||||
allContacts.add(new Contact(id, name, status, imageUrl));
|
allContacts.add(new Contact(id, name, status, imageUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// اگر خالی بود، چیزی نشون نده (یا میتونی دمو بسازی)
|
|
||||||
allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER));
|
allContacts.sort(Comparator.comparing(Contact::getName, String.CASE_INSENSITIVE_ORDER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +197,9 @@ public class AddMembersController {
|
|||||||
|
|
||||||
private Image loadAvatar(String path) {
|
private Image loadAvatar(String path) {
|
||||||
try {
|
try {
|
||||||
// 1) اگر ریسورس داخلی باشد
|
|
||||||
var in = getClass().getResourceAsStream(path);
|
var in = getClass().getResourceAsStream(path);
|
||||||
if (in != null) return new Image(in);
|
if (in != null) return new Image(in);
|
||||||
|
|
||||||
// 2) اگر URL کامل یا file URI
|
|
||||||
return new Image(path, true);
|
return new Image(path, true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return new Image(
|
return new Image(
|
||||||
@@ -233,7 +222,6 @@ public class AddMembersController {
|
|||||||
|
|
||||||
List<String> ids = selectedContacts.stream().map(Contact::getId).toList();
|
List<String> ids = selectedContacts.stream().map(Contact::getId).toList();
|
||||||
|
|
||||||
// تلاش برای batch
|
|
||||||
JSONObject batchReq = new JSONObject()
|
JSONObject batchReq = new JSONObject()
|
||||||
.put("action", "add_members_to_group")
|
.put("action", "add_members_to_group")
|
||||||
.put("group_id", groupInternalId.toString())
|
.put("group_id", groupInternalId.toString())
|
||||||
@@ -248,11 +236,10 @@ public class AddMembersController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: تکبهتک
|
|
||||||
boolean allOk = true;
|
boolean allOk = true;
|
||||||
for (String uid : ids) {
|
for (String uid : ids) {
|
||||||
JSONObject single = new JSONObject()
|
JSONObject single = new JSONObject()
|
||||||
.put("action", "add_member_to_group")
|
.put("action", "add_members_to_group")
|
||||||
.put("group_id", groupInternalId.toString())
|
.put("group_id", groupInternalId.toString())
|
||||||
.put("user_id", uid);
|
.put("user_id", uid);
|
||||||
JSONObject r = ActionHandler.sendWithResponse(single);
|
JSONObject r = ActionHandler.sendWithResponse(single);
|
||||||
@@ -299,9 +286,8 @@ public class AddMembersController {
|
|||||||
a.show();
|
a.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== مدل Contact ==================
|
|
||||||
public static class Contact {
|
public static class Contact {
|
||||||
private final String id; // internal_uuid کاربر
|
private final String id; // internal_uuid
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final String imageUrl;
|
private final String imageUrl;
|
||||||
|
|||||||
@@ -29,20 +29,17 @@ public class AddSubscriberController {
|
|||||||
@FXML private Button skipButton;
|
@FXML private Button skipButton;
|
||||||
@FXML private Button addButton;
|
@FXML private Button addButton;
|
||||||
|
|
||||||
// اطلاعات کانال
|
|
||||||
private UUID channelInternalId;
|
private UUID channelInternalId;
|
||||||
private String channelName;
|
private String channelName;
|
||||||
private String channelDisplayId;
|
private String channelDisplayId;
|
||||||
private File channelImageFile;
|
private File channelImageFile;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
// انتخابها و منبع داده
|
|
||||||
private final Set<Contact> selected = new HashSet<>();
|
private final Set<Contact> selected = new HashSet<>();
|
||||||
private final List<Contact> allContacts = new ArrayList<>();
|
private final List<Contact> allContacts = new ArrayList<>();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
// اسکرول نرم
|
|
||||||
contactsScroll.getStylesheets().add(
|
contactsScroll.getStylesheets().add(
|
||||||
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
||||||
);
|
);
|
||||||
@@ -54,7 +51,6 @@ public class AddSubscriberController {
|
|||||||
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
||||||
});
|
});
|
||||||
|
|
||||||
// تم آیکن
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (addMembersCard.getScene() != null) {
|
if (addMembersCard.getScene() != null) {
|
||||||
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
|
ThemeManager.getInstance().registerScene(addMembersCard.getScene());
|
||||||
@@ -63,22 +59,18 @@ public class AddSubscriberController {
|
|||||||
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, ov, nv) -> updateSearchIcon(nv));
|
||||||
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
||||||
|
|
||||||
// بستن اُورلی
|
|
||||||
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
||||||
skipButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
skipButton.setOnAction(e -> MainController.getInstance().closeOverlay(addMembersCard.getParent()));
|
||||||
addButton.setOnAction(e -> onAddSubscribers());
|
addButton.setOnAction(e -> onAddSubscribers());
|
||||||
|
|
||||||
// سرچ
|
|
||||||
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
|
searchField.textProperty().addListener((obs, ov, nv) -> applyFilter(nv));
|
||||||
Platform.runLater(() -> searchField.requestFocus());
|
Platform.runLater(() -> searchField.requestFocus());
|
||||||
|
|
||||||
// لود کانتکتها از Session
|
|
||||||
loadContactsFromSession();
|
loadContactsFromSession();
|
||||||
updateCount();
|
updateCount();
|
||||||
renderContacts(allContacts);
|
renderContacts(allContacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** NewChannelController این را بعد از ساخت کانال صدا بزند */
|
|
||||||
public void setChannelInfo(UUID internalId, String name, String displayId, File imageFile, String description) {
|
public void setChannelInfo(UUID internalId, String name, String displayId, File imageFile, String description) {
|
||||||
this.channelInternalId = internalId;
|
this.channelInternalId = internalId;
|
||||||
this.channelName = name;
|
this.channelName = name;
|
||||||
@@ -87,7 +79,6 @@ public class AddSubscriberController {
|
|||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
// برای سازگاری با امضای قدیمی (اختیاری)
|
|
||||||
public void setChannelInfo(String name, String id, String description, File image) {
|
public void setChannelInfo(String name, String id, String description, File image) {
|
||||||
this.channelName = name;
|
this.channelName = name;
|
||||||
this.channelDisplayId = id;
|
this.channelDisplayId = id;
|
||||||
@@ -226,7 +217,6 @@ public class AddSubscriberController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: تکبهتک
|
|
||||||
boolean allOk = true;
|
boolean allOk = true;
|
||||||
for (String uid : ids) {
|
for (String uid : ids) {
|
||||||
JSONObject single = new JSONObject()
|
JSONObject single = new JSONObject()
|
||||||
@@ -277,9 +267,8 @@ public class AddSubscriberController {
|
|||||||
a.show();
|
a.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== مدل Contact =====
|
|
||||||
public static class Contact {
|
public static class Contact {
|
||||||
private final String id; // internal_uuid کاربر
|
private final String id; // internal_uuid
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final String imageUrl;
|
private final String imageUrl;
|
||||||
|
|||||||
@@ -27,14 +27,12 @@ public class BlockedUsersController {
|
|||||||
@FXML private ListView<HBox> blockedList;
|
@FXML private ListView<HBox> blockedList;
|
||||||
@FXML private VBox blockedCard;
|
@FXML private VBox blockedCard;
|
||||||
|
|
||||||
// نگاشت برای حذف سریع ردیف بعد از Unblock
|
|
||||||
private final Map<UUID, HBox> rowByUserId = new HashMap<>();
|
private final Map<UUID, HBox> rowByUserId = new HashMap<>();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
backButton.setGraphic(makeIcon("/org/to/telegramfinalproject/Icons/back_button_dark.png"));
|
backButton.setGraphic(makeIcon("/org/to/telegramfinalproject/Icons/back_button_dark.png"));
|
||||||
|
|
||||||
// بارگذاری واقعی از سرور
|
|
||||||
loadBlockedUsers();
|
loadBlockedUsers();
|
||||||
|
|
||||||
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayBackground.getParent()));
|
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(overlayBackground.getParent()));
|
||||||
@@ -72,7 +70,6 @@ public class BlockedUsersController {
|
|||||||
/* ===================== Networking ===================== */
|
/* ===================== Networking ===================== */
|
||||||
|
|
||||||
private void loadBlockedUsers() {
|
private void loadBlockedUsers() {
|
||||||
// درخواست به سرور
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
JSONObject req = new JSONObject().put("action", "get_blocked_users");
|
JSONObject req = new JSONObject().put("action", "get_blocked_users");
|
||||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
@@ -102,8 +99,8 @@ public class BlockedUsersController {
|
|||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
JSONObject req = new JSONObject()
|
JSONObject req = new JSONObject()
|
||||||
.put("action", "toggle_block")
|
.put("action", "toggle_block")
|
||||||
.put("user_id", me) // UUID من
|
.put("user_id", me) // UUID
|
||||||
.put("target_id", targetId.toString()); // UUID طرف
|
.put("target_id", targetId.toString()); // UUID
|
||||||
|
|
||||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
boolean ok = resp != null && "success".equalsIgnoreCase(resp.optString("status"));
|
boolean ok = resp != null && "success".equalsIgnoreCase(resp.optString("status"));
|
||||||
@@ -127,7 +124,6 @@ public class BlockedUsersController {
|
|||||||
blockedList.getItems().clear();
|
blockedList.getItems().clear();
|
||||||
rowByUserId.clear();
|
rowByUserId.clear();
|
||||||
|
|
||||||
// انتظار میره هر آبجکت حداقل: internal_uuid, profile_name, user_id/username/display_id, image_url داشته باشه
|
|
||||||
IntStream.range(0, list.length()).forEach(i -> {
|
IntStream.range(0, list.length()).forEach(i -> {
|
||||||
JSONObject o = list.optJSONObject(i);
|
JSONObject o = list.optJSONObject(i);
|
||||||
if (o == null) return;
|
if (o == null) return;
|
||||||
@@ -229,7 +225,6 @@ public class BlockedUsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showToast(String msg) {
|
private void showToast(String msg) {
|
||||||
// ساده: از Alert استفاده کن؛ اگر Toast اختصاصی داری جایگزینش کن
|
|
||||||
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
|
Alert a = new Alert(Alert.AlertType.INFORMATION, msg, ButtonType.OK);
|
||||||
if (a.getDialogPane().getScene() != null) {
|
if (a.getDialogPane().getScene() != null) {
|
||||||
ThemeManager.getInstance().registerScene(a.getDialogPane().getScene());
|
ThemeManager.getInstance().registerScene(a.getDialogPane().getScene());
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ public class ChangeCredentialsController {
|
|||||||
private boolean passwordVisible = false;
|
private boolean passwordVisible = false;
|
||||||
private boolean confirmVisible = false;
|
private boolean confirmVisible = false;
|
||||||
|
|
||||||
// ↓↓↓ متدی که از CheckPasswordController مقدار میگیرد
|
|
||||||
private String currentPassword;
|
private String currentPassword;
|
||||||
public void setCurrentPassword(String p) { this.currentPassword = p; }
|
public void setCurrentPassword(String p) { this.currentPassword = p; }
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ public class ChangeCredentialsController {
|
|||||||
togglePasswordBtn.setOnAction(e -> togglePasswordVisibility());
|
togglePasswordBtn.setOnAction(e -> togglePasswordVisibility());
|
||||||
toggleConfirmBtn.setOnAction(e -> toggleConfirmVisibility());
|
toggleConfirmBtn.setOnAction(e -> toggleConfirmVisibility());
|
||||||
|
|
||||||
// «ذخیره و بستن» مثل ادیت پروفایل
|
|
||||||
saveButton.setOnAction(e -> saveAndClose());
|
saveButton.setOnAction(e -> saveAndClose());
|
||||||
closeButton.setOnAction(e -> saveAndClose());
|
closeButton.setOnAction(e -> saveAndClose());
|
||||||
cancelButton.setOnAction(e -> saveAndClose());
|
cancelButton.setOnAction(e -> saveAndClose());
|
||||||
@@ -78,7 +76,6 @@ public class ChangeCredentialsController {
|
|||||||
Platform.runLater(() -> usernameField.requestFocus());
|
Platform.runLater(() -> usernameField.requestFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ← از CheckPasswordController بلافاصله بعد از load صدا بزن
|
|
||||||
public void prefillFromSession() {
|
public void prefillFromSession() {
|
||||||
var u = Session.currentUser;
|
var u = Session.currentUser;
|
||||||
if (u == null) return;
|
if (u == null) return;
|
||||||
@@ -112,12 +109,10 @@ public class ChangeCredentialsController {
|
|||||||
toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁");
|
toggleConfirmBtn.setText(confirmVisible ? "👁" : "👁");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ارسال تغییرات (در صورت پر بودن/تغییر) و بستن
|
|
||||||
private void saveAndClose() {
|
private void saveAndClose() {
|
||||||
boolean anyError = false;
|
boolean anyError = false;
|
||||||
boolean sentSomething = false;
|
boolean sentSomething = false;
|
||||||
|
|
||||||
// 1) یوزرنیم
|
|
||||||
var cu = Session.currentUser;
|
var cu = Session.currentUser;
|
||||||
String oldUsername = cu != null ? cu.optString("user_id",
|
String oldUsername = cu != null ? cu.optString("user_id",
|
||||||
cu.optString("username",
|
cu.optString("username",
|
||||||
@@ -129,7 +124,7 @@ public class ChangeCredentialsController {
|
|||||||
|
|
||||||
JSONObject req = new JSONObject()
|
JSONObject req = new JSONObject()
|
||||||
.put("action", "update_username")
|
.put("action", "update_username")
|
||||||
.put("current_password", currentPassword) // با اینکه سرور فعلاً چک نمیکند، میفرستیم
|
.put("current_password", currentPassword)
|
||||||
.put("new_username", newUsername);
|
.put("new_username", newUsername);
|
||||||
|
|
||||||
JSONObject resp = ActionHandler.sendWithResponse(req);
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
@@ -150,7 +145,7 @@ public class ChangeCredentialsController {
|
|||||||
if (confirm.isEmpty() || !confirm.equals(newPwd)) {
|
if (confirm.isEmpty() || !confirm.equals(newPwd)) {
|
||||||
addError(confirmPasswordField, confirmPasswordLabel);
|
addError(confirmPasswordField, confirmPasswordLabel);
|
||||||
addError(visibleConfirmPasswordField, confirmPasswordLabel);
|
addError(visibleConfirmPasswordField, confirmPasswordLabel);
|
||||||
return; // نذار بسته شه تا کاربر درست کنه
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sentSomething = true;
|
sentSomething = true;
|
||||||
@@ -168,7 +163,6 @@ public class ChangeCredentialsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر چیزی نفرستادیم یا همهچیز OK بود → ببند و رفرش Settings
|
|
||||||
if (!sentSomething || !anyError) {
|
if (!sentSomething || !anyError) {
|
||||||
var sc = SettingsController.getInstance();
|
var sc = SettingsController.getInstance();
|
||||||
if (sc != null) sc.populateFromSession();
|
if (sc != null) sc.populateFromSession();
|
||||||
|
|||||||
@@ -1,11 +1,355 @@
|
|||||||
package org.to.telegramfinalproject.UI;
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
|
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||||
|
import org.to.telegramfinalproject.Client.Session;
|
||||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ChannelInfoController {
|
public class ChannelInfoController {
|
||||||
|
|
||||||
|
@FXML private VBox channelCard;
|
||||||
|
@FXML private Pane overlayBackground;
|
||||||
|
@FXML private Button closeButton;
|
||||||
|
|
||||||
|
@FXML private ImageView channelAvatar;
|
||||||
|
@FXML private Label channelName;
|
||||||
|
@FXML private Label subscriberCount;
|
||||||
|
@FXML private Label channelDescription;
|
||||||
|
|
||||||
|
@FXML private Button infoMoreButton;
|
||||||
|
@FXML private ContextMenu infoMoreMenu;
|
||||||
|
@FXML private MenuItem addMembersBtn;
|
||||||
|
@FXML private MenuItem manageChannelBtn;
|
||||||
|
@FXML private MenuItem deleteChannelBtn;
|
||||||
|
@FXML private ImageView moreIcon;
|
||||||
|
@FXML private ImageView manageChannelIcon;
|
||||||
|
@FXML private ImageView addMemberIcon;
|
||||||
|
|
||||||
|
@FXML private VBox subscribersList;
|
||||||
|
@FXML private Label subscribersHeader;
|
||||||
|
@FXML private Button addSubscriberButton;
|
||||||
|
@FXML private ImageView subscribersIcon;
|
||||||
|
@FXML private ScrollPane subscribersScroll;
|
||||||
|
|
||||||
|
private UUID channelId;
|
||||||
|
private String myRole;
|
||||||
|
|
||||||
|
private static final String ICON_PATH = "/org/to/telegramfinalproject/Icons/";
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
closeButton.setOnAction(e ->
|
||||||
|
MainController.getInstance().closeOverlay(channelCard.getParent()));
|
||||||
|
overlayBackground.setOnMouseClicked(e ->
|
||||||
|
MainController.getInstance().closeOverlay(channelCard.getParent()));
|
||||||
|
|
||||||
|
infoMoreButton.setOnAction(e -> {
|
||||||
|
if (infoMoreMenu != null) infoMoreMenu.show(infoMoreButton, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (manageChannelBtn != null) {
|
||||||
|
manageChannelBtn.setOnAction(e -> openManageChannel());
|
||||||
|
}
|
||||||
|
deleteChannelBtn.setOnAction(e -> handleDeleteChannel());
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (channelCard.getScene() != null) {
|
||||||
|
ThemeManager.getInstance().registerScene(channelCard.getScene());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
updateIcons(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateIcons(ThemeManager.getInstance().isDarkMode());
|
||||||
|
|
||||||
|
// Smooth scroll feel
|
||||||
|
subscribersScroll.getStylesheets().add(
|
||||||
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
||||||
|
);
|
||||||
|
subscribersScroll.skinProperty().addListener((obs, oldSkin, newSkin) -> {
|
||||||
|
if (newSkin != null) {
|
||||||
|
ScrollBar vBar = (ScrollBar) subscribersScroll.lookup(".scroll-bar:vertical");
|
||||||
|
if (vBar != null) {
|
||||||
|
subscribersScroll.setOnScroll(event -> {
|
||||||
|
double deltaY = event.getDeltaY() * 0.003;
|
||||||
|
double newValue = vBar.getValue() - deltaY;
|
||||||
|
vBar.setValue(Math.max(0, Math.min(newValue, 1)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addSubscriberButton != null) {
|
||||||
|
addSubscriberButton.setOnAction(e -> openAddSubscriberScene());
|
||||||
|
}
|
||||||
|
if (addMembersBtn != null) {
|
||||||
|
addMembersBtn.setOnAction(e -> openAddSubscriberScene());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setChannelDataFromJson(ChatEntry entry, JSONObject data) {
|
public void setChannelDataFromJson(ChatEntry entry, JSONObject data) {
|
||||||
// read name, status, bio, image url from JSON
|
this.channelId = entry.getId();
|
||||||
// update labels/images accordingly
|
|
||||||
|
channelName.setText(data.optString("channel_name", entry.getName()));
|
||||||
|
subscriberCount.setText(data.optInt("subscriber_count", 0) + " subscribers");
|
||||||
|
channelDescription.setText(data.optString("description", ""));
|
||||||
|
|
||||||
|
// --- Role-based UI ---
|
||||||
|
myRole = data.optString("my_role", "subscriber").toLowerCase();
|
||||||
|
|
||||||
|
deleteChannelBtn.setVisible("owner".equals(myRole));
|
||||||
|
|
||||||
|
boolean canAdd = "owner".equals(myRole) || "admin".equals(myRole);
|
||||||
|
addSubscriberButton.setVisible(canAdd);
|
||||||
|
addSubscriberButton.setManaged(canAdd);
|
||||||
|
if (addMembersBtn != null) {
|
||||||
|
addMembersBtn.setVisible(canAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean showMore = "owner".equals(myRole) || "admin".equals(myRole);
|
||||||
|
infoMoreButton.setVisible(showMore);
|
||||||
|
infoMoreButton.setManaged(showMore);
|
||||||
|
|
||||||
|
// --- Avatar ---
|
||||||
|
String imgUrl = data.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
try {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) channelAvatar.setImage(img);
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
} else {
|
||||||
|
channelAvatar.setImage(new Image(
|
||||||
|
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_channel_profile.png")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Subscribers list ---
|
||||||
|
subscribersList.getChildren().clear();
|
||||||
|
var arr = data.optJSONArray("subscribers");
|
||||||
|
if (arr != null) {
|
||||||
|
subscribersHeader.setText(arr.length() + " SUBSCRIBERS");
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject s = arr.getJSONObject(i);
|
||||||
|
addSubscriberRow(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSubscriberRow(JSONObject s) {
|
||||||
|
HBox row = new HBox(10);
|
||||||
|
row.getStyleClass().add("member-row");
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
ImageView avatar = new ImageView();
|
||||||
|
avatar.setFitWidth(36);
|
||||||
|
avatar.setFitHeight(36);
|
||||||
|
avatar.setPreserveRatio(true);
|
||||||
|
AvatarFX.circleClip(avatar, 36);
|
||||||
|
|
||||||
|
String imgUrl = s.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) avatar.setImage(img);
|
||||||
|
} else {
|
||||||
|
avatar.setImage(new Image(getClass().getResourceAsStream(
|
||||||
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name + status
|
||||||
|
VBox nameBox = new VBox(2);
|
||||||
|
Label name = new Label(s.optString("profile_name", "Unknown"));
|
||||||
|
name.getStyleClass().add("member-name");
|
||||||
|
|
||||||
|
String status;
|
||||||
|
if (s.optBoolean("is_online", false)) {
|
||||||
|
status = "online";
|
||||||
|
} else {
|
||||||
|
status = ChatPageController.getInstance().userStatusText(
|
||||||
|
false,
|
||||||
|
s.optString("last_seen", null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Label statusLbl = new Label(status);
|
||||||
|
statusLbl.getStyleClass().add("member-status");
|
||||||
|
|
||||||
|
nameBox.getChildren().addAll(name, statusLbl);
|
||||||
|
|
||||||
|
// Role
|
||||||
|
Label roleLbl = new Label();
|
||||||
|
String roleStr = s.optString("role", "");
|
||||||
|
if (!roleStr.isBlank()) {
|
||||||
|
roleLbl.setText(roleStr.toLowerCase());
|
||||||
|
roleLbl.getStyleClass().add("member-role");
|
||||||
|
}
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
row.getChildren().addAll(avatar, nameBox, spacer, roleLbl);
|
||||||
|
subscribersList.getChildren().add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openManageChannel() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "view_channel")
|
||||||
|
.put("channel_id", channelId.toString()); // ✅ matches server case
|
||||||
|
|
||||||
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
|
||||||
|
if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
|
||||||
|
Platform.runLater(() -> MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
resp != null ? resp.optString("message") : "Server not responding.",
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject data = resp.optJSONObject("data");
|
||||||
|
if (data == null) {
|
||||||
|
Platform.runLater(() -> MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
"Malformed server response.",
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/manage_channel.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
ManageChannelController controller = loader.getController();
|
||||||
|
controller.setChannelData(data); // ✅ pass JSON to controller
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
"Could not load Manage Channel scene.",
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Platform.runLater(() -> MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
"Error while fetching channel info: " + e.getMessage(),
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAddSubscriberScene() {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml")); // point to new fxml
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
AddSubscriberController controller = loader.getController();
|
||||||
|
controller.setChannelInfo(channelId, channelName.getText(), "", null, "");
|
||||||
|
// passing UUID + name, other fields optional
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error",
|
||||||
|
"Could not load Add Subscribers scene.",
|
||||||
|
Alert.AlertType.ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void handleDeleteChannel() {
|
||||||
|
ChatEntry entry = Session.currentChatEntry;
|
||||||
|
|
||||||
|
if (entry == null || !"channel".equalsIgnoreCase(entry.getType())) {
|
||||||
|
alert(Alert.AlertType.INFORMATION, "This action is only available for channels.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm("Delete Channel",
|
||||||
|
"Are you sure you want to DELETE this channel?\nThis action cannot be undone.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "delete_channel")
|
||||||
|
.put("channel_id", entry.getId().toString());
|
||||||
|
|
||||||
|
JSONObject res = ActionHandler.sendWithResponse(req);
|
||||||
|
|
||||||
|
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
|
||||||
|
alert(Alert.AlertType.INFORMATION, "✅ Channel deleted successfully.");
|
||||||
|
MainController.getInstance().refreshChatListUI();
|
||||||
|
AppRouter.showMain();
|
||||||
|
} else {
|
||||||
|
String msg = (res != null) ? res.optString("message", "Failed to delete channel.") : "null response";
|
||||||
|
alert(Alert.AlertType.ERROR, "❌ " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean confirm(String title, String msg) {
|
||||||
|
Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
a.setTitle(title);
|
||||||
|
return a.showAndWait().filter(btn -> btn == ButtonType.OK).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void alert(Alert.AlertType type, String msg) {
|
||||||
|
new Alert(type, msg, ButtonType.OK).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateIcons(boolean dark) {
|
||||||
|
String suffix = dark ? "_light.png" : "_dark.png";
|
||||||
|
|
||||||
|
moreIcon.setImage(loadImage(ICON_PATH + "more" + suffix));
|
||||||
|
subscribersIcon.setImage(loadImage(ICON_PATH + "channel_subscriber" + suffix));
|
||||||
|
addSubscriberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
|
||||||
|
manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
|
||||||
|
addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageView makeIcon(String path) {
|
||||||
|
ImageView iv = new ImageView();
|
||||||
|
Image img = loadImage(path);
|
||||||
|
if (img != null) {
|
||||||
|
iv.setImage(img);
|
||||||
|
iv.setFitWidth(22);
|
||||||
|
iv.setFitHeight(22);
|
||||||
|
iv.setPreserveRatio(true);
|
||||||
|
}
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Image loadImage(String path) {
|
||||||
|
URL res = getClass().getResource(path);
|
||||||
|
if (res == null) return null;
|
||||||
|
return new Image(res.toExternalForm());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import javafx.scene.image.ImageView;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||||
|
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -120,8 +121,7 @@ public class ChatItemController {
|
|||||||
|
|
||||||
// ChatItemController.java
|
// ChatItemController.java
|
||||||
@FXML private ImageView avatarImage;
|
@FXML private ImageView avatarImage;
|
||||||
// اگر نوع چت نیاز داری برای دیفالتها (private|group|channel)
|
private String chatType;
|
||||||
private String chatType; // اگر داری از setChatData بگیرش و نگه دار
|
|
||||||
|
|
||||||
public void updateAvatar(String url) {
|
public void updateAvatar(String url) {
|
||||||
try {
|
try {
|
||||||
@@ -141,4 +141,10 @@ public class ChatItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String safeTitle(ChatEntry e) {
|
||||||
|
if (e.getName() != null && !e.getName().isBlank()) return e.getName();
|
||||||
|
if (e.getDisplayId() != null && !e.getDisplayId().isBlank()) return e.getDisplayId();
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ public class CheckPasswordController {
|
|||||||
private void validatePassword() {
|
private void validatePassword() {
|
||||||
String entered = (passwordVisible ? visiblePasswordField.getText() : passwordField.getText()).trim();
|
String entered = (passwordVisible ? visiblePasswordField.getText() : passwordField.getText()).trim();
|
||||||
if (entered.isEmpty()) {
|
if (entered.isEmpty()) {
|
||||||
// استایل خطا مثل قبل
|
|
||||||
if (!passwordField.getStyleClass().contains("error"))
|
if (!passwordField.getStyleClass().contains("error"))
|
||||||
passwordField.getStyleClass().add("error");
|
passwordField.getStyleClass().add("error");
|
||||||
if (!visiblePasswordField.getStyleClass().contains("error"))
|
if (!visiblePasswordField.getStyleClass().contains("error"))
|
||||||
@@ -110,7 +109,6 @@ public class CheckPasswordController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// تماس با سرور
|
|
||||||
org.json.JSONObject req = new org.json.JSONObject()
|
org.json.JSONObject req = new org.json.JSONObject()
|
||||||
.put("action", "verify_password")
|
.put("action", "verify_password")
|
||||||
.put("current_password", entered);
|
.put("current_password", entered);
|
||||||
@@ -119,12 +117,11 @@ public class CheckPasswordController {
|
|||||||
boolean ok = (resp != null && "success".equalsIgnoreCase(resp.optString("status")));
|
boolean ok = (resp != null && "success".equalsIgnoreCase(resp.optString("status")));
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
// پاک کردن خطاها
|
|
||||||
passwordField.getStyleClass().remove("error");
|
passwordField.getStyleClass().remove("error");
|
||||||
visiblePasswordField.getStyleClass().remove("error");
|
visiblePasswordField.getStyleClass().remove("error");
|
||||||
passwordLabel.getStyleClass().remove("error");
|
passwordLabel.getStyleClass().remove("error");
|
||||||
|
|
||||||
openChangeCredentials(entered); // ← پسورد فعلی را پاس بده
|
openChangeCredentials(entered);
|
||||||
} else {
|
} else {
|
||||||
if (!passwordField.getStyleClass().contains("error"))
|
if (!passwordField.getStyleClass().contains("error"))
|
||||||
passwordField.getStyleClass().add("error");
|
passwordField.getStyleClass().add("error");
|
||||||
@@ -165,8 +162,8 @@ public class CheckPasswordController {
|
|||||||
Node overlay = loader.load();
|
Node overlay = loader.load();
|
||||||
|
|
||||||
ChangeCredentialsController c = loader.getController();
|
ChangeCredentialsController c = loader.getController();
|
||||||
c.setCurrentPassword(currentPassword); // ← مهم
|
c.setCurrentPassword(currentPassword);
|
||||||
c.prefillFromSession(); // ← پیشپر کردن
|
c.prefillFromSession();
|
||||||
|
|
||||||
MainController.getInstance().showOverlay(overlay);
|
MainController.getInstance().showOverlay(overlay);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
|||||||
@@ -30,15 +30,12 @@ public class ContactsController {
|
|||||||
@FXML private ScrollPane contactsScroll;
|
@FXML private ScrollPane contactsScroll;
|
||||||
@FXML private Button searchIcon;
|
@FXML private Button searchIcon;
|
||||||
|
|
||||||
/** منبع داده UI — با کانتکتهای واقعی پر میشود */
|
|
||||||
private final List<ContactVM> allContacts = new ArrayList<>();
|
private final List<ContactVM> allContacts = new ArrayList<>();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
// 1) لود اولیهی کانتکتها از سشن/سرور
|
|
||||||
loadContactsAndRender();
|
loadContactsAndRender();
|
||||||
|
|
||||||
// 2) سرچ محلی روی لیست
|
|
||||||
searchField.textProperty().addListener((obs, ov, nv) -> {
|
searchField.textProperty().addListener((obs, ov, nv) -> {
|
||||||
String f = nv == null ? "" : nv.trim().toLowerCase();
|
String f = nv == null ? "" : nv.trim().toLowerCase();
|
||||||
List<ContactVM> filtered = allContacts.stream()
|
List<ContactVM> filtered = allContacts.stream()
|
||||||
@@ -49,14 +46,11 @@ public class ContactsController {
|
|||||||
renderContacts(filtered);
|
renderContacts(filtered);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3) فوکوس خودکار روی سرچ
|
|
||||||
Platform.runLater(() -> searchField.requestFocus());
|
Platform.runLater(() -> searchField.requestFocus());
|
||||||
|
|
||||||
// 4) بستن اورلی
|
|
||||||
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
|
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
|
||||||
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(contactsCard.getParent()));
|
||||||
|
|
||||||
// 5) اسکرول نرم
|
|
||||||
contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
|
contactsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
|
||||||
contactsScroll.setPannable(true);
|
contactsScroll.setPannable(true);
|
||||||
contactsScroll.setFitToWidth(true);
|
contactsScroll.setFitToWidth(true);
|
||||||
@@ -66,23 +60,18 @@ public class ContactsController {
|
|||||||
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
contactsScroll.setVvalue(contactsScroll.getVvalue() - deltaY);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6) ثبت صحنه برای ThemeManager
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (contactsCard.getScene() != null) {
|
if (contactsCard.getScene() != null) {
|
||||||
ThemeManager.getInstance().registerScene(contactsCard.getScene());
|
ThemeManager.getInstance().registerScene(contactsCard.getScene());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7) واکنش به تغییر تم
|
|
||||||
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal));
|
ThemeManager.getInstance().darkModeProperty().addListener((obs, oldVal, newVal) -> updateSearchIcon(newVal));
|
||||||
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
updateSearchIcon(ThemeManager.getInstance().isDarkMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// لود داده و رندر لیست
|
|
||||||
// -----------------------
|
|
||||||
private void loadContactsAndRender() {
|
private void loadContactsAndRender() {
|
||||||
// اگر Session.contactEntries از قبل لود شده، از همان استفاده میکنیم.
|
|
||||||
CompletableFuture
|
CompletableFuture
|
||||||
.supplyAsync(() -> {
|
.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -90,10 +79,9 @@ public class ContactsController {
|
|||||||
return new ArrayList<>(Session.contactEntries);
|
return new ArrayList<>(Session.contactEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
// در غیر این صورت از سرور میگیریم (اختیاری)
|
|
||||||
// اگر API «view_contacts» داری، اینجا بفرست:
|
|
||||||
JSONObject req = new JSONObject()
|
JSONObject req = new JSONObject()
|
||||||
.put("action", "view_contacts") // 🔧 اگر نام اکشنات فرق دارد، تغییر بده
|
.put("action", "view_contacts")
|
||||||
.put("user_id", Session.getUserUUID());
|
.put("user_id", Session.getUserUUID());
|
||||||
|
|
||||||
JSONObject res = ActionHandler.sendWithResponse(req);
|
JSONObject res = ActionHandler.sendWithResponse(req);
|
||||||
@@ -122,7 +110,6 @@ public class ContactsController {
|
|||||||
|
|
||||||
fetched.add(new ContactEntry(contactId, userId, contactDisplay, profileName, imageUrl, isBlocked, lastSeen));
|
fetched.add(new ContactEntry(contactId, userId, contactDisplay, profileName, imageUrl, isBlocked, lastSeen));
|
||||||
}
|
}
|
||||||
// اگر میخواهی تو سشن هم نگه داری:
|
|
||||||
if (Session.contactEntries == null) Session.contactEntries = new ArrayList<>();
|
if (Session.contactEntries == null) Session.contactEntries = new ArrayList<>();
|
||||||
Session.contactEntries.clear();
|
Session.contactEntries.clear();
|
||||||
Session.contactEntries.addAll(fetched);
|
Session.contactEntries.addAll(fetched);
|
||||||
@@ -161,7 +148,6 @@ public class ContactsController {
|
|||||||
item.getStyleClass().add("contact-item");
|
item.getStyleClass().add("contact-item");
|
||||||
item.setCursor(Cursor.HAND);
|
item.setCursor(Cursor.HAND);
|
||||||
|
|
||||||
// آواتار
|
|
||||||
ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl));
|
ImageView avatar = new ImageView(loadAvatarSafe(c.imageUrl));
|
||||||
avatar.setFitWidth(58);
|
avatar.setFitWidth(58);
|
||||||
avatar.setFitHeight(58);
|
avatar.setFitHeight(58);
|
||||||
@@ -183,13 +169,20 @@ public class ContactsController {
|
|||||||
|
|
||||||
private Image loadAvatarSafe(String urlOrResource) {
|
private Image loadAvatarSafe(String urlOrResource) {
|
||||||
try {
|
try {
|
||||||
if (urlOrResource != null && urlOrResource.startsWith("/")) {
|
if (urlOrResource != null) {
|
||||||
return new Image(Objects.requireNonNull(getClass().getResourceAsStream(urlOrResource)));
|
if (urlOrResource.startsWith("/")) {
|
||||||
|
return new Image(Objects.requireNonNull(
|
||||||
|
getClass().getResourceAsStream(urlOrResource)));
|
||||||
|
}
|
||||||
|
if (urlOrResource.startsWith("http://") ||
|
||||||
|
urlOrResource.startsWith("https://") ||
|
||||||
|
urlOrResource.startsWith("file:")) {
|
||||||
|
return new Image(urlOrResource, true); // true = لود async
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// اگر URL وب هم داری، میتونی مستقیم Image(url) بسازی
|
|
||||||
return new Image(Objects.requireNonNull(
|
return new Image(Objects.requireNonNull(
|
||||||
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
|
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
|
||||||
} catch (Exception ignore) {
|
} catch (Exception e) {
|
||||||
return new Image(Objects.requireNonNull(
|
return new Image(Objects.requireNonNull(
|
||||||
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
|
getClass().getResourceAsStream("/org/to/telegramfinalproject/Avatars/default_user_profile.png")));
|
||||||
}
|
}
|
||||||
@@ -283,12 +276,10 @@ public class ContactsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// ViewModel سادهی کانتکت
|
|
||||||
// -----------------------
|
|
||||||
private static class ContactVM {
|
private static class ContactVM {
|
||||||
final UUID contactId; // internal UUID (واقعی)
|
final UUID contactId; // internal UUID
|
||||||
final String userId; // @id نمایش (اختیاری)
|
final String userId; // @id
|
||||||
final String profileName;
|
final String profileName;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ public class EditProfileController {
|
|||||||
File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow());
|
File file = fileChooser.showOpenDialog(profileImageView.getScene().getWindow());
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
pickedAvatarFile = file; // ← ذخیره کن برای ارسال هنگام خروج
|
pickedAvatarFile = file;
|
||||||
profileImageView.setImage(new Image(file.toURI().toString())); // پیشنمایش
|
profileImageView.setImage(new Image(file.toURI().toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,16 +192,13 @@ public class EditProfileController {
|
|||||||
if (ActionHandler.instance.wasSuccess()) {
|
if (ActionHandler.instance.wasSuccess()) {
|
||||||
String url = ActionHandler.instance.getLastMessage();
|
String url = ActionHandler.instance.getLastMessage();
|
||||||
if (url != null && !url.isBlank()) {
|
if (url != null && !url.isBlank()) {
|
||||||
// قبلاً اینجا مستقیم bust میزدیم → مشکل ایجاد میکرد
|
|
||||||
// currentUser.put("image_url", url);
|
|
||||||
|
|
||||||
// ✔️ نسخهی امن:
|
|
||||||
String extracted = extractImageUrl(url); // از helper پایین
|
String extracted = extractImageUrl(url);
|
||||||
if (isLikelyImageUrl(extracted)) {
|
if (isLikelyImageUrl(extracted)) {
|
||||||
String finalUrl = addCacheBusterIfHttp(extracted); // فقط برای http/https
|
String finalUrl = addCacheBusterIfHttp(extracted);
|
||||||
Session.currentUser.put("image_url", finalUrl);
|
Session.currentUser.put("image_url", finalUrl);
|
||||||
} else {
|
} else {
|
||||||
// اگر سرور چیز عجیبی مثل "Welcome Farid" برگردوند، نادیده بگیر
|
|
||||||
System.out.println("Upload avatar returned a non-image value: " + url);
|
System.out.println("Upload avatar returned a non-image value: " + url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,15 +264,12 @@ public class EditProfileController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) خودِ کارت Settings را فوراً از Session ریفرش کن
|
|
||||||
if (parentSettings != null) {
|
if (parentSettings != null) {
|
||||||
parentSettings.populateFromSession();
|
parentSettings.populateFromSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) سایدبار بالایی (اسم/آواتار خود کاربر) اگر داری
|
|
||||||
MainController.getInstance().refreshSidebarUserFromSession();
|
MainController.getInstance().refreshSidebarUserFromSession();
|
||||||
|
|
||||||
// 4) بستن اورلی ادیت
|
|
||||||
// MainController.getInstance().goBack(overlayBackground);
|
// MainController.getInstance().goBack(overlayBackground);
|
||||||
MainController.getInstance().closeOverlay(editCard.getParent());
|
MainController.getInstance().closeOverlay(editCard.getParent());
|
||||||
|
|
||||||
@@ -302,7 +296,6 @@ public class EditProfileController {
|
|||||||
|
|
||||||
private boolean isLikelyImageUrl(String s) {
|
private boolean isLikelyImageUrl(String s) {
|
||||||
if (s == null || s.isBlank()) return false;
|
if (s == null || s.isBlank()) return false;
|
||||||
// URL کامل، مسیر نسبی سرور، یا file: و پسوند تصویر
|
|
||||||
return s.startsWith("http://") || s.startsWith("https://")
|
return s.startsWith("http://") || s.startsWith("https://")
|
||||||
|| s.startsWith("file:")
|
|| s.startsWith("file:")
|
||||||
|| s.startsWith("/")
|
|| s.startsWith("/")
|
||||||
@@ -314,19 +307,15 @@ public class EditProfileController {
|
|||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
return url + (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis();
|
return url + (url.contains("?") ? "&" : "?") + "v=" + System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
// برای مسیر لوکال/نسبی اصلاً ?v اضافه نکن (روی ویندوز مشکل میدهد)
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** اگر سرور بهجای URL، JSON یا متن دیگری داد، اینجا URL را دربیار. */
|
|
||||||
private String extractImageUrl(String raw) {
|
private String extractImageUrl(String raw) {
|
||||||
if (raw == null) return null;
|
if (raw == null) return null;
|
||||||
String s = raw.trim();
|
String s = raw.trim();
|
||||||
// اگر برگشتی JSON است (بعضی سرویسها data.display_url برمیگردانند)
|
|
||||||
if (s.startsWith("{") && s.endsWith("}")) {
|
if (s.startsWith("{") && s.endsWith("}")) {
|
||||||
try {
|
try {
|
||||||
org.json.JSONObject j = new org.json.JSONObject(s);
|
org.json.JSONObject j = new org.json.JSONObject(s);
|
||||||
// هر کلیدی که سرویسات استفاده میکند را امتحان کن
|
|
||||||
if (j.has("display_url")) return j.optString("display_url", null);
|
if (j.has("display_url")) return j.optString("display_url", null);
|
||||||
if (j.has("url")) return j.optString("url", null);
|
if (j.has("url")) return j.optString("url", null);
|
||||||
if (j.has("data")) {
|
if (j.has("data")) {
|
||||||
@@ -338,7 +327,7 @@ public class EditProfileController {
|
|||||||
}
|
}
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
return s; // در غیر این صورت همان raw
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ public class GroupInfoController {
|
|||||||
@FXML private MenuItem manageGroupItem;
|
@FXML private MenuItem manageGroupItem;
|
||||||
@FXML private MenuItem deleteGroupItem;
|
@FXML private MenuItem deleteGroupItem;
|
||||||
@FXML private ImageView moreIcon;
|
@FXML private ImageView moreIcon;
|
||||||
|
@FXML private ImageView manageChannelIcon;
|
||||||
|
@FXML private ImageView addMemberIcon;
|
||||||
|
|
||||||
@FXML private VBox membersList;
|
@FXML private VBox membersList;
|
||||||
@FXML private Label membersHeader;
|
@FXML private Label membersHeader;
|
||||||
@@ -289,10 +291,45 @@ public class GroupInfoController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleDeleteGroup() {
|
private void handleDeleteGroup() {
|
||||||
System.out.println("Deleting group...");
|
ChatEntry entry = Session.currentChatEntry;
|
||||||
// TODO: implement backend call
|
|
||||||
|
// if (entry == null || !"group".equalsIgnoreCase(entry.getType())) {
|
||||||
|
// alert(Alert.AlertType.INFORMATION, "This action is only available for groups.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!confirm("Delete Group",
|
||||||
|
"Are you sure you want to DELETE this group?\nThis action cannot be undone.")) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "delete_group")
|
||||||
|
.put("group_id", entry.getId().toString());
|
||||||
|
|
||||||
|
JSONObject res = ActionHandler.sendWithResponse(req);
|
||||||
|
|
||||||
|
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
|
||||||
|
alert(Alert.AlertType.INFORMATION, "✅ Group deleted successfully.");
|
||||||
|
MainController.getInstance().refreshChatListUI();
|
||||||
|
AppRouter.showMain();
|
||||||
|
} else {
|
||||||
|
String msg = (res != null) ? res.optString("message", "Failed to delete group.") : "null response";
|
||||||
|
alert(Alert.AlertType.ERROR, "❌ " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean confirm(String title, String msg) {
|
||||||
|
Alert a = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
a.setTitle(title);
|
||||||
|
return a.showAndWait().filter(btn -> btn == ButtonType.OK).isPresent();
|
||||||
|
}
|
||||||
|
private void alert(Alert.AlertType type, String msg) {
|
||||||
|
new Alert(type, msg, ButtonType.OK).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void updateIcons(boolean dark) {
|
private void updateIcons(boolean dark) {
|
||||||
String suffix = dark ? "_light.png" : "_dark.png";
|
String suffix = dark ? "_light.png" : "_dark.png";
|
||||||
|
|
||||||
@@ -300,7 +337,8 @@ public class GroupInfoController {
|
|||||||
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
|
membersIcon.setImage(loadImage(ICON_PATH + "group" + suffix));
|
||||||
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
|
addMemberButton.setGraphic(makeIcon(ICON_PATH + "add_member" + suffix));
|
||||||
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
|
membersIcon.setImage(loadImage(ICON_PATH + "group_member" + suffix));
|
||||||
|
manageChannelIcon.setImage(loadImage(ICON_PATH + "manage" + suffix));
|
||||||
|
addMemberIcon.setImage(loadImage(ICON_PATH + "add_member" + suffix));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- helpers -------------------------------------------------------------
|
// --- helpers -------------------------------------------------------------
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ public class IntroController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void handleStartMessaging(ActionEvent event) {
|
private void handleStartMessaging(ActionEvent event) {
|
||||||
AppRouter.showLogin(); // همون Scene میمونه، فقط Root عوض میشه
|
AppRouter.showLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML private void goRegister() { AppRouter.showRegister(); }
|
@FXML private void goRegister() { AppRouter.showRegister(); }
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ public class LoginController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// موفق: Session از داخل send پر شده
|
|
||||||
goMain(); // بدون Alert → مستقیم به main.fxml
|
goMain();
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -200,7 +200,7 @@ public class LoginController {
|
|||||||
PauseTransition pause = new PauseTransition(Duration.millis(50));
|
PauseTransition pause = new PauseTransition(Duration.millis(50));
|
||||||
pause.setOnFinished(event -> {
|
pause.setOnFinished(event -> {
|
||||||
errorLabel.setText(message);
|
errorLabel.setText(message);
|
||||||
errorLabel.setStyle("-fx-text-fill: red;"); // 🔴 force red
|
errorLabel.setStyle("-fx-text-fill: red;");
|
||||||
errorLabel.setVisible(true);
|
errorLabel.setVisible(true);
|
||||||
});
|
});
|
||||||
pause.play();
|
pause.play();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import javafx.scene.image.ImageView;
|
|||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
import org.json.JSONObject;
|
||||||
import org.to.telegramfinalproject.Client.Session;
|
import org.to.telegramfinalproject.Client.Session;
|
||||||
import org.to.telegramfinalproject.Models.ChatEntry;
|
import org.to.telegramfinalproject.Models.ChatEntry;
|
||||||
import org.to.telegramfinalproject.Client.ActionHandler;
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
@@ -96,8 +97,12 @@ public class MainController {
|
|||||||
// Keep track of the scene user comes from
|
// Keep track of the scene user comes from
|
||||||
private final Deque<Node> navigationStack = new ArrayDeque<>();
|
private final Deque<Node> navigationStack = new ArrayDeque<>();
|
||||||
private final Map<UUID, ChatItemController> itemControllers = new HashMap<>();
|
private final Map<UUID, ChatItemController> itemControllers = new HashMap<>();
|
||||||
|
private final java.util.Set<UUID> enrichInFlight = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
|
||||||
|
private static final String SAVED_TITLE = "Saved Messages";
|
||||||
|
private static final String SAVED_AVATAR = "/org/to/telegramfinalproject/Avatars/saved_messages.png";
|
||||||
|
|
||||||
//For realtime handling
|
//For realtime handling
|
||||||
public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs,
|
public void onChatUpdated(UUID chatId, String chatType, LocalDateTime lastTs,
|
||||||
boolean isIncoming, String lastPreview) {
|
boolean isIncoming, String lastPreview) {
|
||||||
@@ -351,10 +356,8 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر “Archived Chats” یا هدر دیگری داری، قبلش اضافه کن (اختیاری)
|
// addArchivedHeaderIfYouHaveOne();
|
||||||
// addArchivedHeaderIfYouHaveOne();
|
|
||||||
|
|
||||||
// 1) همیشه Saved اول بیاد (اگر وجود داشت)
|
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
addChatNode(saved);
|
addChatNode(saved);
|
||||||
}
|
}
|
||||||
@@ -408,33 +411,98 @@ public class MainController {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
private void addChatNode(ChatEntry chat) {
|
private void addChatNode(ChatEntry chat) {
|
||||||
try {
|
try {
|
||||||
FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
FXMLLoader fx = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_item.fxml"));
|
||||||
Node item = fx.load();
|
Node item = fx.load();
|
||||||
ChatItemController cc = fx.getController();
|
ChatItemController cc = fx.getController();
|
||||||
|
|
||||||
|
// --- saved detection
|
||||||
|
boolean saved = isSaved(chat);
|
||||||
|
|
||||||
|
String title = safeTitle(chat);
|
||||||
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
|
String preview = chat.getLastMessagePreview() == null ? "" : chat.getLastMessagePreview();
|
||||||
|
String timeText = chat.getLastMessageTime() == null ? "" : formatChatTime(chat.getLastMessageTime());
|
||||||
|
String imageUrl = safeImage(chat.getImageUrl());
|
||||||
|
|
||||||
String timeText = chat.getLastMessageTime() == null
|
// --- force Saved Messages title & avatar
|
||||||
? ""
|
if (saved) {
|
||||||
: formatChatTime(chat.getLastMessageTime());
|
title = SAVED_TITLE;
|
||||||
|
imageUrl = SAVED_AVATAR;
|
||||||
|
}
|
||||||
|
|
||||||
// If chat has a profile picture, pass it; otherwise null
|
cc.setChatData(title, preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType());
|
||||||
String imageUrl = (chat.getImageUrl() != null && !chat.getImageUrl().isEmpty())
|
|
||||||
? chat.getImageUrl()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
cc.setChatData(chat.getName(), preview, timeText, chat.getUnreadCount(), imageUrl, chat.getType());
|
|
||||||
item.setOnMouseClicked(e -> openChat(chat));
|
item.setOnMouseClicked(e -> openChat(chat));
|
||||||
chatListContainer.getChildren().add(item);
|
chatListContainer.getChildren().add(item);
|
||||||
|
|
||||||
itemControllers.put(chat.getId(), cc);
|
itemControllers.put(chat.getId(), cc);
|
||||||
|
|
||||||
|
// --- no enrich for Saved
|
||||||
|
boolean needName = (chat.getName() == null || chat.getName().isBlank());
|
||||||
|
boolean needImage = (chat.getImageUrl() == null || chat.getImageUrl().isBlank());
|
||||||
|
if (!saved && (needName || needImage)) {
|
||||||
|
enrichChatEntryAsync(chat);
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private boolean isSaved(ChatEntry e) {
|
||||||
|
if (e == null) return false;
|
||||||
|
if (e.isSavedMessages()) return true;
|
||||||
|
return "saved".equalsIgnoreCase(e.getType()); // اگر type اختصاصی داری
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void updateSingleChatCell(ChatEntry entry) {
|
||||||
|
ChatItemController cc = itemControllers.get(entry.getId());
|
||||||
|
if (cc != null) {
|
||||||
|
Platform.runLater(() -> cc.setChatData(
|
||||||
|
safeTitle(entry),
|
||||||
|
entry.getLastMessagePreview() == null ? "" : entry.getLastMessagePreview(),
|
||||||
|
entry.getLastMessageTime() == null ? "" : formatChatTime(entry.getLastMessageTime()),
|
||||||
|
entry.getUnreadCount(),
|
||||||
|
safeImage(entry.getImageUrl()),
|
||||||
|
entry.getType()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
refreshChatListUI(); // اگر پیدا نشد، کل لیست را رفرش کن
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enrichChatEntryAsync(ChatEntry entry) {
|
||||||
|
if (entry == null) return;
|
||||||
|
// اگر در حال دریافت هستیم، تکراری نفرست
|
||||||
|
if (!enrichInFlight.add(entry.getId())) return;
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "get_header_info")
|
||||||
|
.put("receiver_id", entry.getId().toString())
|
||||||
|
.put("receiver_type", entry.getType())
|
||||||
|
.put("viewer_id", Session.getUserUUID());
|
||||||
|
|
||||||
|
JSONObject res = ActionHandler.sendWithResponse(req);
|
||||||
|
if (res != null && "success".equalsIgnoreCase(res.optString("status"))) {
|
||||||
|
JSONObject d = res.optJSONObject("data");
|
||||||
|
if (d != null) {
|
||||||
|
String name = d.optString("name", "");
|
||||||
|
String image = d.optString("image_url", "");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (!name.isBlank()) entry.setName(name);
|
||||||
|
if (!image.isBlank()) entry.setImageUrl(image);
|
||||||
|
updateSingleChatCell(entry); // فقط همان آیتم را نوسازی کن
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
} finally {
|
||||||
|
enrichInFlight.remove(entry.getId());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private String mapTypeToLabel(String t) {
|
private String mapTypeToLabel(String t) {
|
||||||
switch (t.toUpperCase()) {
|
switch (t.toUpperCase()) {
|
||||||
@@ -992,7 +1060,6 @@ public class MainController {
|
|||||||
private void openSearchResult(SearchResult r) {
|
private void openSearchResult(SearchResult r) {
|
||||||
switch (r.type) {
|
switch (r.type) {
|
||||||
case USER: {
|
case USER: {
|
||||||
// 1) اگه قبلاً چت پرایوت با این یوزر داری، از همون استفاده کن
|
|
||||||
UUID existingChatId = findExistingPrivateChatId(r.uuid);
|
UUID existingChatId = findExistingPrivateChatId(r.uuid);
|
||||||
|
|
||||||
ChatEntry ce = new ChatEntry();
|
ChatEntry ce = new ChatEntry();
|
||||||
@@ -1006,8 +1073,7 @@ public class MainController {
|
|||||||
ce.setId(existingChatId.toString());
|
ce.setId(existingChatId.toString());
|
||||||
mode = ChatViewMode.NORMAL;
|
mode = ChatViewMode.NORMAL;
|
||||||
} else {
|
} else {
|
||||||
// هنوز چتی وجود ندارد → Preview (بدون ساخت چت)
|
|
||||||
// برای Preview از uuid خودِ طرف مقابل بهعنوان id موقت استفاده میکنیم
|
|
||||||
ce.setId(r.uuid.toString());
|
ce.setId(r.uuid.toString());
|
||||||
mode = isContact(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_ADD_CONTACT;
|
mode = isContact(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_ADD_CONTACT;
|
||||||
}
|
}
|
||||||
@@ -1120,19 +1186,16 @@ public class MainController {
|
|||||||
private boolean isSavedMessages(ChatEntry c) {
|
private boolean isSavedMessages(ChatEntry c) {
|
||||||
if (c == null) return false;
|
if (c == null) return false;
|
||||||
|
|
||||||
// اگر تایپ اختصاصی داری
|
|
||||||
if ("saved".equalsIgnoreCase(c.getType())) return true;
|
if ("saved".equalsIgnoreCase(c.getType())) return true;
|
||||||
|
|
||||||
// اگر با نام مشخص ذخیره میکنی
|
|
||||||
String n = c.getName();
|
String n = c.getName();
|
||||||
if (n != null && n.equalsIgnoreCase("Saved Messages")) return true;
|
if (n != null && n.equalsIgnoreCase("Saved Messages")) return true;
|
||||||
|
|
||||||
// حالت پرایوت با خودِ کاربر
|
|
||||||
String me = (org.to.telegramfinalproject.Client.Session.currentUser != null)
|
String me = (org.to.telegramfinalproject.Client.Session.currentUser != null)
|
||||||
? org.to.telegramfinalproject.Client.Session.currentUser.optString("internal_uuid", "")
|
? org.to.telegramfinalproject.Client.Session.currentUser.optString("internal_uuid", "")
|
||||||
: "";
|
: "";
|
||||||
try {
|
try {
|
||||||
UUID other = c.getOtherUserId(); // اگر این فیلد را داری
|
UUID other = c.getOtherUserId();
|
||||||
if ("private".equalsIgnoreCase(c.getType()) &&
|
if ("private".equalsIgnoreCase(c.getType()) &&
|
||||||
other != null && other.toString().equalsIgnoreCase(me)) {
|
other != null && other.toString().equalsIgnoreCase(me)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1201,13 +1264,11 @@ public class MainController {
|
|||||||
if (Session.chatList == null) Session.chatList = new ArrayList<>();
|
if (Session.chatList == null) Session.chatList = new ArrayList<>();
|
||||||
Session.chatList.add(ce);
|
Session.chatList.add(ce);
|
||||||
}
|
}
|
||||||
// اگر activeChats استفاده میکنی:
|
|
||||||
if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) {
|
if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) {
|
||||||
Session.activeChats.add(ce);
|
Session.activeChats.add(ce);
|
||||||
}
|
}
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
|
|
||||||
// (اختیاری) مرتبسازی بر اساس زمان آخرین پیام
|
|
||||||
Comparator<ChatEntry> byTimeDesc = (a,b) -> {
|
Comparator<ChatEntry> byTimeDesc = (a,b) -> {
|
||||||
LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime();
|
LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime();
|
||||||
if (t1 == null && t2 == null) return 0;
|
if (t1 == null && t2 == null) return 0;
|
||||||
@@ -1223,18 +1284,15 @@ public class MainController {
|
|||||||
|
|
||||||
|
|
||||||
public void addChatAndSelect(org.to.telegramfinalproject.Models.ChatEntry entry) {
|
public void addChatAndSelect(org.to.telegramfinalproject.Models.ChatEntry entry) {
|
||||||
// در Session نگهداری
|
|
||||||
if (org.to.telegramfinalproject.Client.Session.chatList.stream()
|
if (org.to.telegramfinalproject.Client.Session.chatList.stream()
|
||||||
.noneMatch(c -> c.getId().equals(entry.getId()))) {
|
.noneMatch(c -> c.getId().equals(entry.getId()))) {
|
||||||
org.to.telegramfinalproject.Client.Session.chatList.add(0, entry);
|
org.to.telegramfinalproject.Client.Session.chatList.add(0, entry);
|
||||||
org.to.telegramfinalproject.Client.Session.activeChats.add(0, entry);
|
org.to.telegramfinalproject.Client.Session.activeChats.add(0, entry);
|
||||||
}
|
}
|
||||||
// سایدبارت اگر متدی برای رفرش دارد صداش بزن (اسمش را با کلاس خودت هماهنگ کن)
|
|
||||||
try {
|
try {
|
||||||
this.refreshChatListUI(); // اگر نداری، این خط را حذف کن
|
this.refreshChatListUI();
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
|
|
||||||
// نمایش پیج چت
|
|
||||||
if (getChatPageController() != null) {
|
if (getChatPageController() != null) {
|
||||||
getChatPageController().showChat(entry);
|
getChatPageController().showChat(entry);
|
||||||
}
|
}
|
||||||
@@ -1255,13 +1313,11 @@ public class MainController {
|
|||||||
public void ensureArchivedHeaderRow() {
|
public void ensureArchivedHeaderRow() {
|
||||||
boolean hasArchived = Session.archivedChats != null && !Session.archivedChats.isEmpty();
|
boolean hasArchived = Session.archivedChats != null && !Session.archivedChats.isEmpty();
|
||||||
|
|
||||||
// اگر آرشیو نداریم، هدر قبلی را حذف کن و برگرد
|
|
||||||
if (!hasArchived) {
|
if (!hasArchived) {
|
||||||
chatListContainer.getChildren().removeIf(n -> ARCHIVED_ROW_KEY.equals(n.getUserData()));
|
chatListContainer.getChildren().removeIf(n -> ARCHIVED_ROW_KEY.equals(n.getUserData()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر قبلاً هست، دوباره نساز
|
|
||||||
boolean exists = chatListContainer.getChildren().stream()
|
boolean exists = chatListContainer.getChildren().stream()
|
||||||
.anyMatch(n -> ARCHIVED_ROW_KEY.equals(n.getUserData()));
|
.anyMatch(n -> ARCHIVED_ROW_KEY.equals(n.getUserData()));
|
||||||
if (exists) return;
|
if (exists) return;
|
||||||
@@ -1278,7 +1334,6 @@ public class MainController {
|
|||||||
headerRow.getChildren().addAll(title, spacer, openBtn);
|
headerRow.getChildren().addAll(title, spacer, openBtn);
|
||||||
headerRow.setMinHeight(36); headerRow.setPrefHeight(36);
|
headerRow.setMinHeight(36); headerRow.setPrefHeight(36);
|
||||||
|
|
||||||
// در ابتدای همان لیست اصلیِ چتها
|
|
||||||
chatListContainer.getChildren().add(0, headerRow);
|
chatListContainer.getChildren().add(0, headerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1290,7 +1345,6 @@ public class MainController {
|
|||||||
public void refreshArchivedListUI() {
|
public void refreshArchivedListUI() {
|
||||||
chatListContainer.getChildren().clear();
|
chatListContainer.getChildren().clear();
|
||||||
|
|
||||||
// Back row (بماند)
|
|
||||||
HBox backRow = new HBox();
|
HBox backRow = new HBox();
|
||||||
Label back = new Label("← Back to Active");
|
Label back = new Label("← Back to Active");
|
||||||
back.getStyleClass().add("archived-back");
|
back.getStyleClass().add("archived-back");
|
||||||
@@ -1302,10 +1356,9 @@ public class MainController {
|
|||||||
backRow.setStyle("-fx-padding: 6 10 8 10;");
|
backRow.setStyle("-fx-padding: 6 10 8 10;");
|
||||||
chatListContainer.getChildren().add(backRow);
|
chatListContainer.getChildren().add(backRow);
|
||||||
|
|
||||||
// ✅ از همین سلساز FXML استفاده کن تا دقیقا همشکل اکتیو باشد
|
|
||||||
if (Session.archivedChats != null && !Session.archivedChats.isEmpty()) {
|
if (Session.archivedChats != null && !Session.archivedChats.isEmpty()) {
|
||||||
for (ChatEntry e : Session.archivedChats) {
|
for (ChatEntry e : Session.archivedChats) {
|
||||||
addChatNode(e); // ⬅️ همونی که در اکتیو استفاده میکنی
|
addChatNode(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Label empty = new Label("No archived chats");
|
Label empty = new Label("No archived chats");
|
||||||
@@ -1316,29 +1369,40 @@ public class MainController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ساخت یک سل آیتم (برای اکتیو/آرشیو)
|
|
||||||
private HBox buildChatCell(ChatEntry e, boolean archivedView) {
|
private HBox buildChatCell(ChatEntry e, boolean archivedView) {
|
||||||
HBox row = new HBox(8);
|
HBox row = new HBox(8);
|
||||||
ImageView avatar = new ImageView(); avatar.setFitWidth(36); avatar.setFitHeight(36);
|
ImageView avatar = new ImageView(); avatar.setFitWidth(36); avatar.setFitHeight(36);
|
||||||
// ... لود تصویر از e.getImageUrl() اگر داری
|
|
||||||
Label name = new Label(e.getName());
|
Label name = new Label(e.getName());
|
||||||
Region spacer = new Region(); HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
Region spacer = new Region(); HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
||||||
Label last = new Label(safeLastLine(e)); // آخرین پیام/زمان (اختیاری)
|
Label last = new Label(safeLastLine(e));
|
||||||
|
|
||||||
row.getChildren().addAll(avatar, name, spacer, last);
|
row.getChildren().addAll(avatar, name, spacer, last);
|
||||||
row.getStyleClass().add("chat-row");
|
row.getStyleClass().add("chat-row");
|
||||||
|
|
||||||
// کلیک روی آیتم → openChat
|
|
||||||
row.setOnMouseClicked(ev -> openChat(e));
|
row.setOnMouseClicked(ev -> openChat(e));
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String safeLastLine(ChatEntry e) {
|
private String safeLastLine(ChatEntry e) {
|
||||||
String s = e.getLastMessagePreview(); // اگر داری
|
String s = e.getLastMessagePreview();
|
||||||
return s != null ? s : "";
|
return s != null ? s : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String safeTitle(ChatEntry e) {
|
||||||
|
if (e.getName() != null && !e.getName().isBlank()) return e.getName();
|
||||||
|
if (e.getDisplayId() != null && !e.getDisplayId().isBlank()) return e.getDisplayId();
|
||||||
|
if ("saved".equalsIgnoreCase(e.getType()) || e.isSavedMessages()) return "Saved Messages";
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeImage(String img) {
|
||||||
|
return (img != null && !img.isBlank())
|
||||||
|
? img
|
||||||
|
: "/org/to/telegramfinalproject/Avatars/default_user_profile.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
|
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ManageChannelAdminsController {
|
||||||
|
|
||||||
|
@FXML private VBox adminsCard;
|
||||||
|
@FXML private Pane overlayBackground;
|
||||||
|
@FXML private Button closeButton;
|
||||||
|
@FXML private Button closeFooterButton;
|
||||||
|
@FXML private Button addAdminButton;
|
||||||
|
|
||||||
|
@FXML private ScrollPane adminsScroll;
|
||||||
|
@FXML private VBox adminsList;
|
||||||
|
|
||||||
|
private String channelId; // internal_uuid of channel
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
|
||||||
|
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
|
||||||
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(adminsCard.getParent()));
|
||||||
|
|
||||||
|
//addAdminButton.setOnAction(e -> openAddAdminOverlay());
|
||||||
|
|
||||||
|
// Smooth scroll feel
|
||||||
|
adminsScroll.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
|
||||||
|
adminsScroll.setPannable(true);
|
||||||
|
adminsScroll.setFitToWidth(true);
|
||||||
|
adminsScroll.setFitToHeight(false);
|
||||||
|
adminsScroll.getContent().setOnScroll(event -> {
|
||||||
|
double deltaY = event.getDeltaY() * 0.003;
|
||||||
|
adminsScroll.setVvalue(adminsScroll.getVvalue() - deltaY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelData(String channelId, JSONArray admins) {
|
||||||
|
this.channelId = channelId;
|
||||||
|
|
||||||
|
adminsList.getChildren().clear();
|
||||||
|
|
||||||
|
// Convert to list for sorting
|
||||||
|
List<JSONObject> adminList = new ArrayList<>();
|
||||||
|
for (int i = 0; i < admins.length(); i++) {
|
||||||
|
adminList.add(admins.getJSONObject(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: owner first, then admins
|
||||||
|
adminList.sort((a, b) -> {
|
||||||
|
String roleA = a.optString("role", "subscriber");
|
||||||
|
String roleB = b.optString("role", "subscriber");
|
||||||
|
if ("owner".equalsIgnoreCase(roleA) && !"owner".equalsIgnoreCase(roleB)) return -1;
|
||||||
|
if ("owner".equalsIgnoreCase(roleB) && !"owner".equalsIgnoreCase(roleA)) return 1;
|
||||||
|
return 0; // keep relative order for admins
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
for (JSONObject a : adminList) {
|
||||||
|
addAdminRow(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAdminRow(JSONObject a) {
|
||||||
|
HBox row = new HBox(10);
|
||||||
|
row.getStyleClass().add("member-row");
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
ImageView avatar = new ImageView();
|
||||||
|
avatar.setFitWidth(36);
|
||||||
|
avatar.setFitHeight(36);
|
||||||
|
avatar.setPreserveRatio(true);
|
||||||
|
|
||||||
|
String imgUrl = a.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) avatar.setImage(img);
|
||||||
|
} else {
|
||||||
|
avatar.setImage(new Image(getClass().getResourceAsStream(
|
||||||
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name + Status
|
||||||
|
VBox details = new VBox(2);
|
||||||
|
Label name = new Label(a.optString("profile_name", "Unknown"));
|
||||||
|
name.getStyleClass().add("member-name");
|
||||||
|
|
||||||
|
Label status = new Label(
|
||||||
|
a.optBoolean("is_online", false) ? "online"
|
||||||
|
: "last seen recently"
|
||||||
|
);
|
||||||
|
status.getStyleClass().add("member-status");
|
||||||
|
|
||||||
|
details.getChildren().addAll(name, status);
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
row.getChildren().addAll(avatar, details, spacer);
|
||||||
|
|
||||||
|
// Remove admin (cannot remove owner)
|
||||||
|
String role = a.optString("role", "admin");
|
||||||
|
if (!"owner".equalsIgnoreCase(role)) {
|
||||||
|
Button removeBtn = new Button("Remove");
|
||||||
|
removeBtn.getStyleClass().add("link-btn");
|
||||||
|
removeBtn.setOnAction(e -> removeAdmin(a.optString("user_id"), row));
|
||||||
|
row.getChildren().add(removeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
adminsList.getChildren().add(row);
|
||||||
|
|
||||||
|
// User clicks an admin → open set channel admin permissions scene
|
||||||
|
row.setOnMouseClicked(e -> {
|
||||||
|
if (!"owner".equalsIgnoreCase(role)) {
|
||||||
|
try {
|
||||||
|
// request current permissions from server
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "get_channel_admin_permissions")
|
||||||
|
.put("channel_id", channelId)
|
||||||
|
.put("admin_id", a.optString("user_id"));
|
||||||
|
|
||||||
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
JSONObject oldPerms = (resp != null) ? resp.optJSONObject("permissions") : null;
|
||||||
|
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/set_channel_admin_permissions.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
// SetChannelAdminPermissionsController controller = loader.getController();
|
||||||
|
// controller.setTarget(
|
||||||
|
// UUID.fromString(channelId),
|
||||||
|
// UUID.fromString(a.optString("user_id")),
|
||||||
|
// true,
|
||||||
|
// oldPerms
|
||||||
|
// );
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert("Error", "Could not open permissions scene.", Alert.AlertType.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAdmin(String userId, HBox row) {
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "remove_admin_from_channel")
|
||||||
|
.put("channel_id", channelId)
|
||||||
|
.put("target_user_id", userId);
|
||||||
|
|
||||||
|
System.out.println("Sending remove_admin_from_channel request: " + req.toString(2));
|
||||||
|
|
||||||
|
row.setDisable(true);
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||||
|
Platform.runLater(() -> adminsList.getChildren().remove(row));
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
row.setDisable(false);
|
||||||
|
Alert a = new Alert(Alert.AlertType.ERROR,
|
||||||
|
resp != null ? resp.optString("message", "Failed to remove admin.") : "No response from server.",
|
||||||
|
ButtonType.OK);
|
||||||
|
a.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// private void openAddAdminOverlay() {
|
||||||
|
// try {
|
||||||
|
// // === Query the server for full subscribers list ===
|
||||||
|
// JSONObject req = new JSONObject()
|
||||||
|
// .put("action", "view_channel_subscribers")
|
||||||
|
// .put("channel_id", channelId);
|
||||||
|
//
|
||||||
|
// JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
//
|
||||||
|
// if (resp == null || !"success".equalsIgnoreCase(resp.optString("status"))) {
|
||||||
|
// MainController.getInstance().showAlert(
|
||||||
|
// "Error",
|
||||||
|
// "Could not fetch subscribers.",
|
||||||
|
// Alert.AlertType.ERROR
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// JSONObject data = resp.optJSONObject("data");
|
||||||
|
// JSONArray subscribers = (data != null) ? data.optJSONArray("subscribers") : new JSONArray();
|
||||||
|
//
|
||||||
|
// // === Load Add Admins FXML (same FXML reused) ===
|
||||||
|
// FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
// "/org/to/telegramfinalproject/Fxml/add_admins.fxml"));
|
||||||
|
// Node overlay = loader.load();
|
||||||
|
//
|
||||||
|
// AddChannelAdminsController controller = loader.getController();
|
||||||
|
// controller.setChannelData(channelId, subscribers); // send raw subscribers, filtering is done in AddChannelAdminsController
|
||||||
|
//
|
||||||
|
// MainController.getInstance().showOverlay(overlay);
|
||||||
|
//
|
||||||
|
// } catch (IOException e) {
|
||||||
|
// e.printStackTrace();
|
||||||
|
// MainController.getInstance().showAlert(
|
||||||
|
// "Error", "Could not load Add Admins scene.", Alert.AlertType.ERROR
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ManageChannelController {
|
||||||
|
|
||||||
|
@FXML private VBox manageChannelCard;
|
||||||
|
@FXML private Pane overlayBackground;
|
||||||
|
@FXML private Button closeButton, changePicButton, cancelButton, saveButton;
|
||||||
|
@FXML private ImageView channelImage;
|
||||||
|
@FXML private TextField channelNameField;
|
||||||
|
@FXML private TextField channelIdField;
|
||||||
|
@FXML private TextArea channelDescriptionField;
|
||||||
|
@FXML private Label adminCount, subscriberCount;
|
||||||
|
@FXML private Button manageAdminsButton;
|
||||||
|
@FXML private Button manageSubscribersButton;
|
||||||
|
|
||||||
|
private String channelId; // Internal UUID of the channel
|
||||||
|
private String originalName;
|
||||||
|
private String originalDisplayId;
|
||||||
|
private String originalImageUrl;
|
||||||
|
private String originalDescription;
|
||||||
|
private File selectedImageFile;
|
||||||
|
|
||||||
|
private JSONObject data;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
|
||||||
|
cancelButton.setOnAction(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
|
||||||
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(manageChannelCard.getParent()));
|
||||||
|
|
||||||
|
changePicButton.setOnAction(e -> choosePicture());
|
||||||
|
saveButton.setOnAction(e -> saveChanges());
|
||||||
|
|
||||||
|
manageAdminsButton.setOnAction(e -> openManageAdminsScene(data));
|
||||||
|
manageSubscribersButton.setOnAction(e -> openManageSubscribersScene(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelData(JSONObject data) {
|
||||||
|
this.data = data;
|
||||||
|
this.channelId = data.optString("internal_uuid");
|
||||||
|
|
||||||
|
originalName = data.optString("channel_name", "");
|
||||||
|
originalDisplayId = data.optString("channel_id", "");
|
||||||
|
originalImageUrl = data.optString("image_url", "");
|
||||||
|
originalDescription = data.optString("description", "");
|
||||||
|
|
||||||
|
channelNameField.setText(originalName);
|
||||||
|
channelIdField.setText(originalDisplayId);
|
||||||
|
channelDescriptionField.setText(originalDescription);
|
||||||
|
|
||||||
|
if (!originalImageUrl.isBlank()) {
|
||||||
|
channelImage.setImage(new Image(originalImageUrl, true));
|
||||||
|
}
|
||||||
|
adminCount.setText(String.valueOf(countRole(data, "admin") + 1)); // owner + admins
|
||||||
|
subscriberCount.setText(String.valueOf(countRole(data, "subscriber") + Integer.parseInt(adminCount.getText())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countRole(JSONObject channelData, String role) {
|
||||||
|
var arr = channelData.optJSONArray("subscribers");
|
||||||
|
if (arr == null) return 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
if (role.equalsIgnoreCase(arr.getJSONObject(i).optString("role"))) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void choosePicture() {
|
||||||
|
FileChooser fc = new FileChooser();
|
||||||
|
fc.setTitle("Select channel picture");
|
||||||
|
selectedImageFile = fc.showOpenDialog(manageChannelCard.getScene().getWindow());
|
||||||
|
if (selectedImageFile != null) {
|
||||||
|
channelImage.setImage(new Image(selectedImageFile.toURI().toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveChanges() {
|
||||||
|
String newName = channelNameField.getText().trim();
|
||||||
|
String newChannelId = channelIdField.getText().trim();
|
||||||
|
String newDescription = channelDescriptionField.getText().trim();
|
||||||
|
String newImageUrl = (selectedImageFile != null)
|
||||||
|
? selectedImageFile.toURI().toString()
|
||||||
|
: originalImageUrl;
|
||||||
|
|
||||||
|
boolean changed =
|
||||||
|
!Objects.equals(originalName, newName) ||
|
||||||
|
!Objects.equals(originalDisplayId, newChannelId) ||
|
||||||
|
!Objects.equals(originalDescription, newDescription) ||
|
||||||
|
!Objects.equals(originalImageUrl, newImageUrl);
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
MainController.getInstance().closeOverlay(manageChannelCard.getParent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "edit_channel_info")
|
||||||
|
.put("channel_id", channelId) // internal UUID
|
||||||
|
.put("new_channel_id", newChannelId)
|
||||||
|
.put("name", newName)
|
||||||
|
.put("description", newDescription);
|
||||||
|
|
||||||
|
if (newImageUrl != null && !newImageUrl.isBlank()) {
|
||||||
|
req.put("image_url", newImageUrl);
|
||||||
|
} else {
|
||||||
|
req.put("image_url", JSONObject.NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||||
|
MainController.getInstance().closeOverlay(manageChannelCard.getParent());
|
||||||
|
} else {
|
||||||
|
String msg = (resp != null)
|
||||||
|
? resp.optString("message", "Failed to update channel")
|
||||||
|
: "No response from server";
|
||||||
|
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||||
|
a.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openManageAdminsScene(JSONObject data) {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/manage_channel_admins.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
ManageChannelAdminsController controller = loader.getController();
|
||||||
|
// Pass channelId and subscribers list from JSON
|
||||||
|
controller.setChannelData(
|
||||||
|
data.optString("internal_uuid"),
|
||||||
|
data.optJSONArray("subscribers")
|
||||||
|
);
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error", "Could not load Manage Channel Admins scene.", Alert.AlertType.ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openManageSubscribersScene(JSONObject data) {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/manage_subscribers.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
ManageSubscribersController controller = loader.getController();
|
||||||
|
// Pass channelId and subscribers list from the JSON data
|
||||||
|
controller.setChannelData(
|
||||||
|
data.optString("internal_uuid"),
|
||||||
|
data.optJSONArray("subscribers")
|
||||||
|
);
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error", "Could not load Manage Subscribers scene.", Alert.AlertType.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import org.to.telegramfinalproject.Client.ActionHandler;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -116,13 +118,27 @@ public class ManageGroupController {
|
|||||||
groupIdField.setText(originalGroupId);
|
groupIdField.setText(originalGroupId);
|
||||||
|
|
||||||
if (!originalImageUrl.isBlank()) {
|
if (!originalImageUrl.isBlank()) {
|
||||||
groupImage.setImage(new Image(originalImageUrl, true));
|
String uri = resolveLocalUri(originalImageUrl);
|
||||||
|
if (uri != null) {
|
||||||
|
groupImage.setImage(new Image(uri, true));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
adminCount.setText(String.valueOf(countRole(data, "admin") + 1));
|
adminCount.setText(String.valueOf(countRole(data, "admin") + 1));
|
||||||
memberCount.setText(String.valueOf(countRole(data, "member") + Integer.parseInt(adminCount.getText())));
|
memberCount.setText(String.valueOf(countRole(data, "member") + Integer.parseInt(adminCount.getText())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveLocalUri(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) return null;
|
||||||
|
if (raw.startsWith("file:/") || raw.startsWith("http")) return raw;
|
||||||
|
|
||||||
|
Path base = Paths.get(System.getProperty("user.dir"), "uploads");
|
||||||
|
Path resolved = base.resolve(raw.startsWith("/") ? raw.substring(1) : raw)
|
||||||
|
.normalize();
|
||||||
|
return resolved.toUri().toString(); // file:///C:/.../uploads/avatars/2025-09-05/...
|
||||||
|
}
|
||||||
|
|
||||||
private int countRole(JSONObject groupData, String role) {
|
private int countRole(JSONObject groupData, String role) {
|
||||||
var arr = groupData.optJSONArray("members");
|
var arr = groupData.optJSONArray("members");
|
||||||
if (arr == null) return 0;
|
if (arr == null) return 0;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.to.telegramfinalproject.Client.ActionHandler;
|
||||||
|
import org.to.telegramfinalproject.Client.AvatarLocalResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ManageSubscribersController {
|
||||||
|
|
||||||
|
@FXML private VBox subscribersCard;
|
||||||
|
@FXML private Pane overlayBackground;
|
||||||
|
@FXML private Button closeButton;
|
||||||
|
@FXML private Button closeFooterButton;
|
||||||
|
@FXML private Button addSubscribersButton;
|
||||||
|
@FXML private ScrollPane subscribersScroll;
|
||||||
|
@FXML private VBox subscribersList;
|
||||||
|
|
||||||
|
private String channelId; // internal UUID
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void initialize() {
|
||||||
|
closeButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
|
||||||
|
closeFooterButton.setOnAction(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
|
||||||
|
overlayBackground.setOnMouseClicked(e -> MainController.getInstance().closeOverlay(subscribersCard.getParent()));
|
||||||
|
|
||||||
|
addSubscribersButton.setOnAction(e -> openAddSubscribersOverlay(channelId));
|
||||||
|
|
||||||
|
// Smooth scroll
|
||||||
|
subscribersScroll.getStylesheets().add(
|
||||||
|
getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm()
|
||||||
|
);
|
||||||
|
subscribersScroll.setPannable(true);
|
||||||
|
subscribersScroll.setFitToWidth(true);
|
||||||
|
subscribersScroll.setFitToHeight(false);
|
||||||
|
subscribersScroll.getContent().setOnScroll(event -> {
|
||||||
|
double deltaY = event.getDeltaY() * 0.003;
|
||||||
|
subscribersScroll.setVvalue(subscribersScroll.getVvalue() - deltaY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelData(String channelId, JSONArray subscribers) {
|
||||||
|
this.channelId = channelId;
|
||||||
|
subscribersList.getChildren().clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < subscribers.length(); i++) {
|
||||||
|
Object raw = subscribers.get(i);
|
||||||
|
JSONObject sub = (raw instanceof JSONObject)
|
||||||
|
? (JSONObject) raw
|
||||||
|
: new JSONObject((java.util.Map<?, ?>) raw);
|
||||||
|
|
||||||
|
addSubscriberRow(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSubscriberRow(JSONObject sub) {
|
||||||
|
HBox row = new HBox(10);
|
||||||
|
row.getStyleClass().add("member-row");
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
ImageView avatar = new ImageView();
|
||||||
|
avatar.setFitWidth(36);
|
||||||
|
avatar.setFitHeight(36);
|
||||||
|
avatar.setPreserveRatio(true);
|
||||||
|
|
||||||
|
String imgUrl = sub.optString("image_url", "");
|
||||||
|
if (!imgUrl.isBlank()) {
|
||||||
|
Image img = AvatarLocalResolver.load(imgUrl);
|
||||||
|
if (img != null) avatar.setImage(img);
|
||||||
|
} else {
|
||||||
|
avatar.setImage(new Image(getClass().getResourceAsStream(
|
||||||
|
"/org/to/telegramfinalproject/Avatars/default_user_profile.png"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name + Status
|
||||||
|
VBox details = new VBox(2);
|
||||||
|
Label name = new Label(sub.optString("profile_name", "Unknown"));
|
||||||
|
name.getStyleClass().add("member-name");
|
||||||
|
|
||||||
|
Label status = new Label(
|
||||||
|
sub.optBoolean("is_online", false) ? "online" : "last seen recently"
|
||||||
|
);
|
||||||
|
status.getStyleClass().add("member-status");
|
||||||
|
|
||||||
|
details.getChildren().addAll(name, status);
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
row.getChildren().addAll(avatar, details, spacer);
|
||||||
|
|
||||||
|
// Only non-owners can be removed
|
||||||
|
String role = sub.optString("role", "subscriber");
|
||||||
|
if (!"owner".equalsIgnoreCase(role)) {
|
||||||
|
Button removeBtn = new Button("Remove");
|
||||||
|
removeBtn.getStyleClass().add("link-btn");
|
||||||
|
removeBtn.setOnAction(e -> removeSubscriber(sub.optString("user_id"), row));
|
||||||
|
row.getChildren().add(removeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribersList.getChildren().add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeSubscriber(String userId, HBox row) {
|
||||||
|
JSONObject req = new JSONObject()
|
||||||
|
.put("action", "remove_subscriber_from_channel")
|
||||||
|
.put("channel_id", channelId)
|
||||||
|
.put("user_id", userId);
|
||||||
|
|
||||||
|
row.setDisable(true);
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
JSONObject resp = ActionHandler.sendWithResponse(req);
|
||||||
|
|
||||||
|
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
|
||||||
|
Platform.runLater(() -> subscribersList.getChildren().remove(row));
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
row.setDisable(false);
|
||||||
|
Alert a = new Alert(Alert.AlertType.ERROR,
|
||||||
|
resp != null ? resp.optString("message", "Failed to remove subscriber.")
|
||||||
|
: "No response from server.",
|
||||||
|
ButtonType.OK);
|
||||||
|
a.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAddSubscribersOverlay(String channelId) {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource(
|
||||||
|
"/org/to/telegramfinalproject/Fxml/add_subscriber.fxml"));
|
||||||
|
Node overlay = loader.load();
|
||||||
|
|
||||||
|
AddSubscriberController controller = loader.getController();
|
||||||
|
controller.setChannelInfo(UUID.fromString(channelId), "", "", null, "");
|
||||||
|
|
||||||
|
MainController.getInstance().showOverlay(overlay);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
MainController.getInstance().showAlert(
|
||||||
|
"Error", "Could not load Add Subscribers scene.", Alert.AlertType.ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.to.telegramfinalproject.UI;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public final class MediaPathResolver {
|
||||||
|
private final Path uploadsRoot;
|
||||||
|
|
||||||
|
public MediaPathResolver(Path uploadsRoot) {
|
||||||
|
this.uploadsRoot = uploadsRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toFileUri(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) return null;
|
||||||
|
if (raw.startsWith("file:/")) return raw;
|
||||||
|
|
||||||
|
Path p;
|
||||||
|
if (raw.startsWith("/")) {
|
||||||
|
String sub = raw.substring(1).replace("/", File.separator);
|
||||||
|
p = uploadsRoot.resolve(sub).normalize();
|
||||||
|
} else {
|
||||||
|
p = Paths.get(raw).normalize();
|
||||||
|
}
|
||||||
|
return p.toUri().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1410,3 +1410,52 @@
|
|||||||
-fx-border-color: #555;
|
-fx-border-color: #555;
|
||||||
-fx-text-fill: #aaa;
|
-fx-text-fill: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-info-card {
|
||||||
|
-fx-background-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-title {
|
||||||
|
-fx-text-fill: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-subcount {
|
||||||
|
-fx-text-fill: #aaaaaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-buttons .button {
|
||||||
|
-fx-background-color: #3c3c3c;
|
||||||
|
-fx-text-fill: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-button {
|
||||||
|
-fx-text-fill: #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscriber-item,
|
||||||
|
.admin-item {
|
||||||
|
-fx-text-fill: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-description {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme: Channel Manage styles */
|
||||||
|
.description-input {
|
||||||
|
-fx-background-color: #1f2a36; /* darker to match card */
|
||||||
|
-fx-control-inner-background: #1f2a36;
|
||||||
|
-fx-border-color: #444444;
|
||||||
|
-fx-border-radius: 6;
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
-fx-padding: 6;
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-fill: #dddddd; /* normal text */
|
||||||
|
-fx-prompt-text-fill: #888888; /* dimmed placeholder text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-input:focused {
|
||||||
|
-fx-border-color: #4a90e2; /* Telegram blue */
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(74,144,226,0.5), 6, 0.4, 0, 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1387,3 +1387,24 @@
|
|||||||
-fx-border-color: #ccc;
|
-fx-border-color: #ccc;
|
||||||
-fx-text-fill: #222;
|
-fx-text-fill: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-description {
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-text-fill: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme: Channel Manage styles */
|
||||||
|
.description-input {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-border-color: #d0d0d0;
|
||||||
|
-fx-border-radius: 6;
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
-fx-padding: 6;
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-fill: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-input:focused {
|
||||||
|
-fx-border-color: #4a90e2; /* Telegram blue highlight */
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(74,144,226,0.4), 4, 0.3, 0, 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,122 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import java.lang.*?>
|
|
||||||
<?import java.util.*?>
|
|
||||||
<?import javafx.scene.*?>
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
|
||||||
<AnchorPane xmlns="http://javafx.com/javafx"
|
<StackPane xmlns:fx="http://javafx.com/fxml"
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
|
||||||
fx:controller="org.to.telegramfinalproject.UI.ChannelInfoController"
|
fx:controller="org.to.telegramfinalproject.UI.ChannelInfoController"
|
||||||
prefHeight="400.0" prefWidth="600.0">
|
styleClass="overlay-root">
|
||||||
|
|
||||||
</AnchorPane>
|
<!-- Background (click to close) -->
|
||||||
|
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
|
||||||
|
|
||||||
|
<!-- Channel card -->
|
||||||
|
<VBox fx:id="channelCard" styleClass="profile-card" spacing="16"
|
||||||
|
prefWidth="360" maxWidth="360"
|
||||||
|
prefHeight="500" maxHeight="500">
|
||||||
|
|
||||||
|
<!-- ===== Header: Title + More + Close ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Channel Info" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
<!-- More -->
|
||||||
|
<Button fx:id="infoMoreButton" styleClass="icon-button">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fx:id="moreIcon" fitWidth="18" fitHeight="18" preserveRatio="true"/>
|
||||||
|
</graphic>
|
||||||
|
<contextMenu>
|
||||||
|
<ContextMenu fx:id="infoMoreMenu">
|
||||||
|
<items>
|
||||||
|
<!-- Add Members -->
|
||||||
|
<MenuItem fx:id="addMembersBtn" text="Add members">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fx:id="addMemberIcon" fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Manage Channel -->
|
||||||
|
<MenuItem fx:id="manageChannelBtn" text="Manage channel">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fx:id="manageChannelIcon" fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/manage_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<!-- Delete Channel (owner only) -->
|
||||||
|
<MenuItem fx:id="deleteChannelBtn" text="Delete channel" style="-fx-text-fill: red;" visible="false">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/delete_red.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</MenuItem>
|
||||||
|
</items>
|
||||||
|
</ContextMenu>
|
||||||
|
</contextMenu>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Close -->
|
||||||
|
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- ===== Channel picture + name + subscriber count ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="12">
|
||||||
|
<!-- Channel picture -->
|
||||||
|
<ImageView fx:id="channelAvatar" fitWidth="60" fitHeight="60" preserveRatio="true"
|
||||||
|
styleClass="profile-picture"/>
|
||||||
|
|
||||||
|
<!-- Name + subs stacked -->
|
||||||
|
<VBox alignment="CENTER_LEFT" spacing="4">
|
||||||
|
<Label fx:id="channelName" text="Channel name" styleClass="info-title"/>
|
||||||
|
<Label fx:id="subscriberCount" text="0 subscribers" styleClass="info-subtitle"/>
|
||||||
|
<!-- New description label -->
|
||||||
|
<Label fx:id="channelDescription" text="Channel description..." wrapText="true"
|
||||||
|
styleClass="info-description"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- ===== Subscribers list ===== -->
|
||||||
|
<VBox spacing="8" styleClass="info-blocks" VBox.vgrow="ALWAYS">
|
||||||
|
<HBox spacing="8" alignment="CENTER_LEFT">
|
||||||
|
<ImageView fx:id="subscribersIcon" fitWidth="26" fitHeight="26" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/group_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
<Label fx:id="subscribersHeader" text="0 SUBSCRIBERS" styleClass="profile-info-sub"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="addSubscriberButton" styleClass="icon-button">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<ScrollPane fx:id="subscribersScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="subscribersList" spacing="10"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<!-- Add Member -->
|
<!-- Add Member -->
|
||||||
<MenuItem fx:id="addMemberItem" text="Add member">
|
<MenuItem fx:id="addMemberItem" text="Add member">
|
||||||
<graphic>
|
<graphic>
|
||||||
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
<ImageView fx:id="addMemberIcon" fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
<image>
|
<image>
|
||||||
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
<Image url="@/org/to/telegramfinalproject/Icons/add_member_dark.png"/>
|
||||||
</image>
|
</image>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<!-- Manage Group -->
|
<!-- Manage Group -->
|
||||||
<MenuItem fx:id="manageGroupItem" text="Manage group">
|
<MenuItem fx:id="manageGroupItem" text="Manage group">
|
||||||
<graphic>
|
<graphic>
|
||||||
<ImageView fitWidth="16" fitHeight="16" preserveRatio="true">
|
<ImageView fx:id="manageChannelIcon" fitWidth="16" fitHeight="16" preserveRatio="true">
|
||||||
<image>
|
<image>
|
||||||
<Image url="@/org/to/telegramfinalproject/Icons/manage_dark.png"/>
|
<Image url="@/org/to/telegramfinalproject/Icons/manage_dark.png"/>
|
||||||
</image>
|
</image>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<StackPane xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="org.to.telegramfinalproject.UI.ManageChannelController"
|
||||||
|
styleClass="overlay-root">
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<VBox fx:id="manageChannelCard" styleClass="profile-card" spacing="16"
|
||||||
|
prefWidth="360" maxWidth="360"
|
||||||
|
prefHeight="520" maxHeight="520">
|
||||||
|
|
||||||
|
<!-- ===== Header ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Edit channel" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- ===== Channel picture + fields ===== -->
|
||||||
|
<VBox alignment="CENTER" spacing="12">
|
||||||
|
<!-- Channel picture -->
|
||||||
|
<Button fx:id="changePicButton" styleClass="circle-pic-btn">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fx:id="channelImage" fitWidth="80" fitHeight="80" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/org/to/telegramfinalproject/Avatars/default_channel_profile.png"/>
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Channel name -->
|
||||||
|
<TextField fx:id="channelNameField" promptText="Channel name" styleClass="input-field"/>
|
||||||
|
|
||||||
|
<!-- Channel ID -->
|
||||||
|
<TextField fx:id="channelIdField" promptText="Channel ID (unique)" styleClass="input-field"/>
|
||||||
|
|
||||||
|
<!-- Channel description -->
|
||||||
|
<TextArea fx:id="channelDescriptionField"
|
||||||
|
promptText="Description (optional)"
|
||||||
|
wrapText="true"
|
||||||
|
prefRowCount="3"
|
||||||
|
VBox.vgrow="ALWAYS"
|
||||||
|
styleClass="description-input"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- ===== Options ===== -->
|
||||||
|
<VBox spacing="10" styleClass="info-blocks" VBox.vgrow="ALWAYS">
|
||||||
|
<!-- Administrators -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Administrators" styleClass="info-row-label"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label fx:id="adminCount" text="0" styleClass="info-row-value"/>
|
||||||
|
<Button fx:id="manageAdminsButton" text="Manage" styleClass="manage-link-btn"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Subscribers -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Subscribers" styleClass="info-row-label"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label fx:id="subscriberCount" text="0" styleClass="info-row-value"/>
|
||||||
|
<Button fx:id="manageSubscribersButton" text="Manage" styleClass="manage-link-btn"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- ===== Footer ===== -->
|
||||||
|
<HBox spacing="10" alignment="CENTER_RIGHT">
|
||||||
|
<Button fx:id="cancelButton" text="Cancel" styleClass="secondary-btn"/>
|
||||||
|
<Button fx:id="saveButton" text="Save" styleClass="primary-btn"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
|
||||||
|
<StackPane xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="org.to.telegramfinalproject.UI.ManageChannelAdminsController"
|
||||||
|
styleClass="overlay-root">
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<VBox fx:id="adminsCard" styleClass="profile-card" spacing="12"
|
||||||
|
prefWidth="360" maxWidth="360"
|
||||||
|
prefHeight="500" maxHeight="500">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Channel Administrators" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- Scroll list -->
|
||||||
|
<ScrollPane fx:id="adminsScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="adminsList" spacing="10"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<HBox spacing="10" alignment="CENTER_RIGHT">
|
||||||
|
<Button fx:id="addAdminButton" text="Add admin" styleClass="secondary-btn" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="closeFooterButton" text="Close" styleClass="primary-btn"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<StackPane xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="org.to.telegramfinalproject.UI.ManageSubscribersController"
|
||||||
|
styleClass="overlay-root">
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<Pane fx:id="overlayBackground" styleClass="overlay-background"/>
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<VBox fx:id="subscribersCard" styleClass="profile-card" spacing="12"
|
||||||
|
prefWidth="360" maxWidth="360"
|
||||||
|
prefHeight="520" maxHeight="520">
|
||||||
|
|
||||||
|
<!-- ===== Header ===== -->
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||||
|
<Label text="Subscribers" styleClass="profile-title" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button fx:id="closeButton" styleClass="icon-button" text="✕"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- ===== Subscribers List ===== -->
|
||||||
|
<ScrollPane fx:id="subscribersScroll" fitToWidth="true" styleClass="member-scroll" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="subscribersList" spacing="10"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<Separator styleClass="section-separator"/>
|
||||||
|
|
||||||
|
<!-- ===== Footer ===== -->
|
||||||
|
<HBox spacing="12" alignment="CENTER_RIGHT">
|
||||||
|
<Button fx:id="addSubscribersButton" text="Add subscribers" styleClass="secondary-btn"/>
|
||||||
|
<Button fx:id="closeFooterButton" text="Close" styleClass="primary-btn"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |