30 Commits
Author SHA1 Message Date
Sadra3st 34cb8ba84c Merge pull request 'Develop' (#1) from develop into main
Reviewed-on: #1
2026-06-26 14:29:38 +00:00
mohammadreza 275a89a59a Delete images/a 2026-06-24 13:42:25 +00:00
mohammadreza 5e246a2968 Upload files to "images" 2026-06-24 13:42:15 +00:00
mohammadreza 267c01d463 Add images/a 2026-06-24 13:41:03 +00:00
mohammadreza 379efffb4e Delete images 2026-06-24 13:40:48 +00:00
mohammadreza ba426c6ca3 Delete login_success.png 2026-06-24 13:39:35 +00:00
mohammadreza 3bd9d46aa4 Delete login_screen.png 2026-06-24 13:39:28 +00:00
mohammadreza 06862f2376 Delete chat_interface.png 2026-06-24 13:39:16 +00:00
mohammadreza 176b52a2cb Upload files to "/" 2026-06-24 13:39:01 +00:00
mohammadreza bf7d8ee05a Add images 2026-06-24 13:38:00 +00:00
mohammadreza f75fd98a2f Delete images.png 2026-06-24 10:19:20 +00:00
mohammadreza 8461a9986a Update images.png 2026-06-24 10:19:07 +00:00
mohammadreza 17d597dd4c Add images 2026-06-24 10:18:16 +00:00
mohammadreza 523871e957 Delete images 2026-06-24 10:17:54 +00:00
mohammadreza 7c206ee192 Delete chat_interface.png 2026-06-24 10:17:43 +00:00
mohammadreza 22fbe41051 Delete login_screen.png 2026-06-24 10:17:36 +00:00
mohammadreza 5e7aa9cae0 Delete login_success.png 2026-06-24 10:17:28 +00:00
mohammadreza 0356d6f574 Upload files to "/" 2026-06-24 10:17:10 +00:00
mohammadreza 8f819673b8 Add images 2026-06-24 10:16:55 +00:00
mohammadreza db9e1ceb05 Update README.md 2026-06-24 10:12:50 +00:00
mohammadreza e5e16c0611 feat: initialize JavaFX login window 2026-06-24 13:27:00 +03:30
mohammadreza abb0819cc0 feat: add JavaFX application launcher 2026-06-24 13:26:28 +03:30
mohammadreza eb88aaeed0 feat: add LoginController.java logic for handling UI events 2026-06-24 13:22:40 +03:30
mohammadreza 60fe662629 feat: add login.fxml for ui 2026-06-24 13:20:44 +03:30
mohammadreza 95d46dd02a refactor: improve user list formatting in UserManager.java 2026-06-24 13:17:40 +03:30
mohammadreza 0c3b43cbd7 feat: implement message handling and add utility methods to ClientSession 2026-06-24 13:11:58 +03:30
mohammadreza 47b2c3f85f feat: complete TODO tasks in ChatServer.java 2026-06-24 13:07:14 +03:30
mohammadreza bda8e144ff refactor: move files to Net package 2026-06-24 13:03:44 +03:30
mohammadreza 8454be1cec feat: add javafx chat ui and styling 2026-06-24 12:56:42 +03:30
mohammadreza 079145b4e2 build: add javafx dependencies to project 2026-06-24 12:54:17 +03:30
19 changed files with 1126 additions and 204 deletions
+27 -123
View File
@@ -1,132 +1,36 @@
## Network File-Sharing chat System (Java) # UniChat - Network File-Sharing chat System (Java)
### Overview A modern, real-time chat application built using **JavaFX** and **Java Socket Programming**. UniChat allows users to connect to a central server, see who is online, and engage in both public and private conversations through a clean, dark-themed interface.
This project is a multi-client network chat system with file sharing, implemented using **Java sockets**, **MUltithreading** and **Object Serialization**.
Clients connect to a central server and can:
* send public chat message ## 🚀 Key Features
* send private messages
* request the list of online users
* transfer files to other users
communication between client and server is implemented using Java Object Streams(`ObjectInputStream` / `ObjectOutputStream`) so that structured objects can be transferred directly over the network. * **Authentication System:** A dedicated login screen where users can authenticate with a username.
* **Real-time Public Chat:** A global communication channel to broadcast messages to all connected users.
* **Private Messaging:** Targeted, one-on-one communication capabilities between users.
* **Live User List:** A sidebar that displays all currently connected users in real-time.
* **Modern UI:** Built with JavaFX, featuring a responsive, dark-themed interface for a comfortable user experience.
## 📸 Project Interface
| Login Screen | Authentication Success | Main Chat Interface |
| :---: | :---: | :---: |
| ![Login](images/login_screen.png) | ![Success](images/login_success.png) | ![Chat](images/chat_interface.png) |
## 🛠 Tech Stack
* **Language:** Java 17+
* **GUI Framework:** JavaFX
* **Networking:** Java Sockets (TCP)
* **Concurrency:** Multi-threaded Client-Server architecture
## 💻 How to Run
1. **Start the Server:** Ensure the `Server` application is running to listen for incoming connections.
2. **Launch the Client:** Run the `Launcher` class to open the UI.
3. **Connect:** Enter your desired username on the login screen and click "Connect".
--- ---
### Architecture
1. **ChatServer** opens a `ServerSocket` and waits for connections.
2. Each connecting client gets its own **ClientSession** thread.
3. A shared **UserManager** tracks who's online and routes messages.
4. Everything is exchanged as serialized objects (`ChatMessage` / `FileMessage`).
```
Client → Socket → Server → ClientSession (thread) → UserManager → Broadcast/Route → Clients
```
---
### Message Protocol
Two serializable classes carry everything over the wire:
* **ChatMessage** text-based: logins, public/private messages, user-list requests, server replies.
* **FileMessage** file transfers, carrying the file as a `byte[]`.
`MessageType` is an enum (`LOGIN`, `LOGIN_SUCCESS`, `LOGIN_FAILED`, `PUBLIC_MESSAGE`, `PRIVATE_MESSAGE`, `USER_LIST`, …) so both sides agree on what each message means.
---
### Package Structure
```
com.university.chat
├── common → shared message classes (ChatMessage, FileMessage, MessageType)
├── server → server-side logic
└── client → client application
```
#### `common`
Already complete — these define the protocol and don't need any changes.
#### `server`
* **ChatServer** entry point. Opens the server socket and creates a `ClientSession` per client.
* **ClientSession** one thread per client. Handles login, reads incoming messages, and dispatches them (broadcast, private message, user list, file transfer).
* **UserManager** thread-safe registry of online users (`ConcurrentHashMap<String, ClientSession>`). Already complete.
* **FileManager** manages per-user folders on disk (`server_data/<username>/sent` and `received`). Already complete.
#### `client`
* **chatClient** entry point. Connects to the server, logs in, starts a listener thread, and runs the input loop for user commands.
* **ServerListener** background thread that continuously reads and displays messages/files coming from the server.
* **TransferProgress** small helper that prints a progress bar during file uploads. Already complete.
---
### Client Commands
```
/msg <username> <message> → private message
/users → list online users
/sendfile <username> <filepath> → send a file
<anything else> → public message
```
---
### File Transfer Flow
1. Client reads the file and converts it to `byte[]`.
2. It's wrapped in a `FileMessage` and sent to the server.
3. The server saves a copy (in `sent/` for the sender, `received/` for the receiver) and forwards it to the recipient.
---
### Threading Model
* **Server**: one `ClientSession` thread per connected client.
* **Client**: main thread handles user input, a `ServerListener` thread handles incoming messages — so you can chat and receive **at the same time**.
---
## Your Task
Several core pieces are left as **TODOs** for you to implement. Follow the comments in each file alongside the structure mentioned above to complete them.
| File | What to implement |
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Server/ChatServer.java` | Pick a port, create a shared `UserManager`, open the server socket, accept clients in a loop, and create a `ClientSession` thread per client. |
| `Server/ClientSession.java` | Set up object streams in the constructor; handle login (success/failure); receive messages in a loop and dispatch `ChatMessage`/`FileMessage`; broadcast, private message, and user-list logic; remove user on disconnect; forward files. |
| `Client/chatClient.java` | Connect to the server, set up streams, log in, start `ServerListener`, and implement the command loop (`/msg`, `/users`, `/sendfile`, plain messages). |
| `Client/ServerListener.java` | Continuously read objects from the server and print chat messages / file-received notifications. |
**Tips:**
* Always create the `ObjectOutputStream` *before* the `ObjectInputStream` on both ends — this avoids a stream-handshake deadlock.
* Don't forget `out.flush()` after `writeObject(...)`.
* Test with two or more client instances to verify broadcasting, private messages, and file transfer all work correctly.
#### Running multiple clients in IntelliJ
1. Click the **⋮** icon next to the run button → **Edit Configurations**
2. Open **Modify options** (Alt+M)
3. Enable **Allow multiple instances** (Alt+U)
---
## Optional Bonus: JavaFX UI
If you want to go further replace the console client with a simple **JavaFX** GUI (chat window, online-user list, file-send button, etc.).
1. **Add the JavaFX dependencies** to your `pom.xml` — for Maven:
```xml
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21</version>
</dependency>
```
You'll also need the `javafx-maven-plugin` to run the app from Maven.
2. **Create a JavaFX entry point**, e.g. `Client/ChatClientApp.java`, extending `javafx.application.Application` and implementing `start(Stage stage)`. This becomes your new launch class instead of (or alongside) `chatClient`.
3. Reuse your existing socket/streams logic — just move the "send" actions to button handlers and update the UI from `ServerListener` using `Platform.runLater(...)` (**since UI updates must happen on the JavaFX Application Thread**). 3. Reuse your existing socket/streams logic — just move the "send" actions to button handlers and update the UI from `ServerListener` using `Platform.runLater(...)` (**since UI updates must happen on the JavaFX Application Thread**).
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+26
View File
@@ -13,5 +13,31 @@
<maven.compiler.target>25</maven.compiler.target> <maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- آدرس کلاس اصلی کلاینت را اینجا بنویس -->
<mainClass>com.university.chat.Client.UI.Launcher</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>
@@ -0,0 +1,46 @@
package com.university.chat.Client.Net;
import java.io.ObjectInputStream;
import java.util.function.Consumer;
public class ServerListener implements Runnable {
private final ObjectInputStream in;
private Consumer<Object> onMessageReceived;
private final Consumer<Exception> onConnectionError;
private volatile boolean running = true;
public ServerListener(ObjectInputStream in,
Consumer<Object> onMessageReceived,
Consumer<Exception> onConnectionError) {
this.in = in;
this.onMessageReceived = onMessageReceived;
this.onConnectionError = onConnectionError;
}
@Override
public void run() {
try {
while (running) {
Object object = in.readObject();
if (onMessageReceived != null) {
onMessageReceived.accept(object);
}
}
} catch (Exception e) {
if (running && onConnectionError != null) {
onConnectionError.accept(e);
}
}
}
public void stop() {
running = false;
}
public void setOnMessageReceived(Consumer<Object> onMessageReceived) {
this.onMessageReceived = onMessageReceived;
}
}
@@ -0,0 +1,158 @@
package com.university.chat.Client.Net;
import com.university.chat.Client.UI.ChatController;
import com.university.chat.Common.ChatMessage;
import com.university.chat.Common.FileMessage;
import com.university.chat.Common.MessageType;
import javafx.scene.control.ListView;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.util.function.Consumer;
public class chatClient {
private Socket socket;
private ObjectOutputStream out;
private ObjectInputStream in;
private Thread listenerThread;
private ServerListener serverListener;
private String username;
public void connect(String host, int port) throws IOException {
socket = new Socket(host, port);
out = new ObjectOutputStream(socket.getOutputStream());
out.flush();
in = new ObjectInputStream(socket.getInputStream());
}
public void startListening(Consumer<Object> onMessageReceived,
Consumer<Exception> onConnectionError) {
serverListener = new ServerListener(in, onMessageReceived, onConnectionError);
listenerThread = new Thread(serverListener);
listenerThread.setDaemon(true);
listenerThread.start();
}
public void login(String username) throws IOException {
this.username = username;
ChatMessage loginMessage = new ChatMessage(
MessageType.LOGIN,
username,
null,
null
);
sendObject(loginMessage);
}
public void sendPublicMessage(String content) throws IOException {
ChatMessage message = new ChatMessage(
MessageType.PUBLIC_MESSAGE,
username,
null,
content
);
sendObject(message);
}
public void sendPrivateMessage(String receiver, String content) throws IOException {
ChatMessage message = new ChatMessage(
MessageType.PRIVATE_MESSAGE,
username,
receiver,
content
);
sendObject(message);
}
public void requestUserList() throws IOException {
ChatMessage message = new ChatMessage(
MessageType.USER_LIST,
username,
null,
null
);
sendObject(message);
}
public void sendFile(String receiver, File file) throws IOException {
if (file == null || !file.exists() || !file.isFile()) {
throw new FileNotFoundException("Invalid file.");
}
byte[] fileData = Files.readAllBytes(file.toPath());
FileMessage fileMessage = new FileMessage(
username,
receiver,
file.getName(),
fileData
);
sendObject(fileMessage);
}
private synchronized void sendObject(Object object) throws IOException {
if (out == null) {
throw new IOException("Client is not connected.");
}
out.writeObject(object);
out.flush();
}
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
public String getUsername() {
return username;
}
public void close() {
try {
if (serverListener != null) {
serverListener.stop();
}
if (listenerThread != null) {
listenerThread.interrupt();
}
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
System.err.println("Error while closing client: " + e.getMessage());
}
}
public ServerListener getServerListener() {
return serverListener;
}
}
@@ -1,20 +0,0 @@
package com.university.chat.Client;
public class ServerListener implements Runnable{
// TODO: store the ObjectInputStream from the user socket
// (this should be the same input stream the
// chatClient created when connecting)
@Override
public void run() {
try {
// TODO: In an infinite loop read objects from the server
// - if it's a ChatMessage -> print "<sender>: <content>"
// - if it's a FileMessage -> print that a file was received
// (filename + sender), it's already
// saved to disk by the server.
} catch (Exception e){
System.out.println("Disconnected from server");
}
}
}
@@ -0,0 +1,28 @@
package com.university.chat.Client.UI;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.io.IOException;
public class ChatApp extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("login.fxml"));
Scene scene = new Scene(fxmlLoader.load());
scene.getStylesheets().add(
getClass().getResource("/chat_style.css").toExternalForm()
);
stage.setTitle("Login");
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(scene);
stage.show();
}
}
@@ -0,0 +1,337 @@
package com.university.chat.Client.UI;
import com.university.chat.Client.Net.chatClient;
import com.university.chat.Client.TransferProgress;
import com.university.chat.Common.ChatMessage;
import com.university.chat.Common.FileMessage;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
public class ChatController {
@FXML
private ListView<String> usersList;
@FXML
private TextArea publicChatArea;
@FXML
private AnchorPane chatAnchor;
@FXML
private TextField messageField;
@FXML
private BorderPane titleBar;
private double xOffset = 0;
private double yOffset = 0;
private chatClient client;
private String selectedUser = null;
private final Map<String, BorderPane> privateChatViews = new HashMap<>();
private final Map<String, TextArea> privateTextAreas = new HashMap<>();
private final Map<String, TextField> privateInputFields = new HashMap<>();
@FXML
private VBox fileTransferBox;
@FXML
private Label fileTransferLabel;
@FXML
private ProgressBar fileTransferProgress;
@FXML
private void initialize() {
titleBar.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
titleBar.setOnMouseDragged(event -> {
Stage stage = (Stage) titleBar.getScene().getWindow();
stage.setX(event.getScreenX() - xOffset);
stage.setY(event.getScreenY() - yOffset);
});
usersList.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null || newVal.isBlank()) {
selectedUser = null;
chatAnchor.getChildren().clear();
return;
}
String clickedUser = newVal.trim();
if (client != null && clickedUser.equals(client.getUsername())) {
selectedUser = null;
chatAnchor.getChildren().clear();
return;
}
selectedUser = clickedUser;
openPrivateChat(clickedUser);
});
}
public void setClient(chatClient client) {
this.client = client;
if (client.getServerListener() != null) {
client.getServerListener().setOnMessageReceived(this::handleIncomingMessage);
}
try {
client.requestUserList();
} catch (IOException e) {
e.printStackTrace();
}
}
public void handleIncomingMessage(Object obj) {
if (obj instanceof ChatMessage msg) {
switch (msg.getType()) {
case USER_LIST -> {
String content = msg.getContent();
if (content != null) {
List<String> users = Arrays.stream(content.split(","))
.map(String::trim)
.filter(u -> !u.isEmpty())
.toList();
updateUsersList(users);
}
}
case PUBLIC_MESSAGE -> Platform.runLater(() ->
publicChatArea.appendText(msg.getSender() + ": " + msg.getContent() + "\n"));
case PRIVATE_MESSAGE -> Platform.runLater(() ->
appendPrivateMessage(msg.getSender(),
msg.getSender() + ": " + msg.getContent()));
}
}
else if (obj instanceof FileMessage msg) {
Platform.runLater(() -> {
FileChooser chooser = new FileChooser();
chooser.setInitialFileName(msg.getFilename());
File file = chooser.showSaveDialog(usersList.getScene().getWindow());
if(file != null){
try {
Files.write(file.toPath(), msg.getData());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
public void updateUsersList(List<String> users) {
Platform.runLater(() ->
usersList.getItems().setAll(users));
}
@FXML
public void sendPublicMessage() {
String text = messageField.getText();
if (text == null || text.trim().isEmpty()) return;
text = text.trim();
try {
client.sendPublicMessage(text);
publicChatArea.appendText("Me: " + text + "\n");
messageField.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendPrivateMessage(String receiver) {
TextField inputField = privateInputFields.get(receiver);
TextArea chatArea = privateTextAreas.get(receiver);
if (inputField == null || chatArea == null) return;
String text = inputField.getText();
if (text == null || text.trim().isEmpty()) return;
text = text.trim();
try {
client.sendPrivateMessage(receiver, text);
chatArea.appendText("Me: " + text + "\n");
inputField.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
private void sendFile() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select file to send");
Stage stage = (Stage) usersList.getScene().getWindow();
File file = fileChooser.showOpenDialog(stage);
if (file == null) return;
try {
updateFileTransfer(file.length());
client.sendFile(selectedUser,file);
publicChatArea.appendText("Me sent file: " + file.getName() + "\n");
}
catch (Exception e){
e.printStackTrace();
}
}
private void openPrivateChat(String user) {
BorderPane chatView = privateChatViews.get(user);
if (chatView == null) {
chatView = createPrivateChatView(user);
privateChatViews.put(user, chatView);
}
chatAnchor.getChildren().setAll(chatView);
AnchorPane.setTopAnchor(chatView, 0.0);
AnchorPane.setBottomAnchor(chatView, 0.0);
AnchorPane.setLeftAnchor(chatView, 0.0);
AnchorPane.setRightAnchor(chatView, 0.0);
}
private BorderPane createPrivateChatView(String user) {
TextArea chatArea = new TextArea();
chatArea.setEditable(false);
chatArea.setWrapText(true);
TextField inputField = new TextField();
inputField.setPromptText("Message to " + user);
Button sendButton = new Button("Send");
sendButton.setOnAction(e -> sendPrivateMessage(user));
inputField.setOnAction(e -> sendPrivateMessage(user));
HBox bottom = new HBox(8, inputField, sendButton);
bottom.setPadding(new Insets(8));
inputField.setPrefWidth(300);
BorderPane root = new BorderPane();
root.setCenter(chatArea);
root.setBottom(bottom);
privateTextAreas.put(user, chatArea);
privateInputFields.put(user, inputField);
return root;
}
private void appendPrivateMessage(String user, String line) {
BorderPane view = privateChatViews.get(user);
if (view == null) {
view = createPrivateChatView(user);
privateChatViews.put(user, view);
}
TextArea area = privateTextAreas.get(user);
if (area != null) {
area.appendText(line + "\n");
}
}
public void showFileTransfer(String fileName) {
Platform.runLater(() -> {
fileTransferBox.setVisible(true);
fileTransferBox.setManaged(true);
fileTransferProgress.setProgress(0);
fileTransferLabel.setText("Sending " + fileName + "...");
});
}
public void updateFileTransfer(long length) {
Platform.runLater(() -> {
fileTransferProgress.setProgress(length);
fileTransferLabel.setText(String.format(
"Sending... %d",
(int) (length * 100)
));
});
}
public void finishFileTransfer(String fileName) {
Platform.runLater(() -> {
fileTransferProgress.setProgress(1.0);
fileTransferLabel.setText(fileName + " sent successfully");
});
}
@FXML
private void closeApp() {
System.exit(0);
}
@FXML
private void minimize() {
Stage stage = (Stage) usersList.getScene().getWindow();
stage.setIconified(true);
}
@FXML
private void maximize() {
Stage stage = (Stage) usersList.getScene().getWindow();
stage.setMaximized(!stage.isMaximized());
}
}
@@ -0,0 +1,9 @@
package com.university.chat.Client.UI;
import javafx.application.Application;
public class Launcher{
public static void main(String[] args) {
Application.launch(ChatApp.class);
}
}
@@ -0,0 +1,180 @@
package com.university.chat.Client.UI;
import com.university.chat.Client.Net.chatClient;
import com.university.chat.Common.ChatMessage;
import com.university.chat.Common.MessageType;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class LoginController {
@FXML
public HBox titleBar;
private double xOffset = 0;
private double yOffset = 0;
@FXML
private TextField usernameField;
@FXML
private Button nameButton;
private final chatClient client = new chatClient();
private ChatController chatController;
@FXML
public void initialize() {
titleBar.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
titleBar.setOnMouseDragged(event -> {
Stage stage = (Stage) titleBar.getScene().getWindow();
stage.setX(event.getScreenX() - xOffset);
stage.setY(event.getScreenY() - yOffset);
});
}
@FXML
private void onConnectClicked() {
String username = usernameField.getText().trim();
if (username.isEmpty()) {
showError("Username cannot be empty.");
return;
}
nameButton.setDisable(true);
new Thread(() -> {
try {
client.connect("localhost", 9000);
client.startListening(
this::handleServerMessage,
this::handleConnectionError
);
client.login(username);
} catch (Exception e) {
Platform.runLater(() -> {
showError("Failed to connect to server.\nMake sure the server is running.\n\n" + e.getMessage());
nameButton.setDisable(false);
});
}
}).start();
}
private void handleServerMessage(Object obj) {
if (obj instanceof ChatMessage msg) {
if (msg.getType() == MessageType.LOGIN_SUCCESS) {
Platform.runLater(() -> {
showInfo("Login successful!");
openChatWindow();
});
}
else if (msg.getType() == MessageType.LOGIN_FAILED) {
Platform.runLater(() -> {
showError("Login failed: username taken");
nameButton.setDisable(false);
});
}
else if (msg.getType() == MessageType.USER_LIST) {
Platform.runLater(() -> {
if (chatController != null) {
String content = msg.getContent();
java.util.List<String> users = java.util.Arrays.asList(content.split(","));
chatController.updateUsersList(users);
}
});
}
else {
Platform.runLater(() -> {
if (chatController != null) {
chatController.sendPublicMessage();
}
System.out.println("Server: " + msg.getContent());
});
}
}
}
private void handleConnectionError(Exception e) {
Platform.runLater(() -> showError("Connection lost: " + e.getMessage()));
}
private void showError(String text) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(null);
alert.setContentText(text);
alert.showAndWait();
}
private void showInfo(String text) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText(text);
alert.showAndWait();
}
private void openChatWindow() {
try {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("chat.fxml")
);
Parent root = loader.load();
this.chatController = loader.getController();
this.chatController.setClient(client);
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setScene(new Scene(root, 900, 600));
stage.getScene().getStylesheets().add(
getClass().getResource("/chat_style.css").toExternalForm()
);
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
private void closeApp() {
System.exit(0);
}
@FXML
private void minimize() {
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setIconified(true);
}
@FXML
private void maximize() {
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setMaximized(!stage.isMaximized());
}
}
@@ -1,29 +0,0 @@
package com.university.chat.Client;
public class chatClient {
public static void main() {
// TODO: Connecting to the server
// 1. Create a socket and connect to the server
// 2. Create an ObjectOutputStream (out) and ObjectInputStream (in)
// from the socket's streams — output FIRST, then input.
// 2. Get the username, and send a LOGIN ChatMessage with that username
// 3. Start a new Thread running a ServerListener(in) so incoming
// messages are handled concurrently.
while (true){
try {
// TODO: Program loop — read a line from the console and act on it:
// - "/msg <user> <text>" -> build & send a PRIVATE_MESSAGE
// - "/users" -> build & send a USER_LIST request
// - "/sendfile <user> <path>" -> read the file into a byte[]
// (you can use TransferProgress
// to show progress)
// and send it as a FileMessage
// - anything else -> send a PUBLIC_MESSAGE
// Remember to flush() the output stream after writeObject().
} catch (Exception e){
System.out.println("command failed: " + e.getMessage());
}
}
}
}
@@ -1,15 +1,37 @@
package com.university.chat.Server; package com.university.chat.Server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ChatServer { public class ChatServer {
// TODO: declare a single shared UserManager instance (static final)
// This MUST be shared by all ClientSession threads so that private static final UserManager userManager = new UserManager();
// broadcasting and private messaging work correctly. private static ServerSocket serverSocket;
public static void main(String[] args) { public static void main(String[] args) {
// TODO: Create a ServerSocket
// TODO: In an infinite loop: try {
// accept an incoming client connection serverSocket = new ServerSocket(9000);
// make a new thread running ClientSession for each user. System.out.printf("server started. port: %s\n" ,9000);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
while (true) {
Socket s = serverSocket.accept();
System.out.println("new client accepted.");
ClientSession clientSession = new ClientSession(s, userManager);
Thread thread = new Thread(clientSession);
thread.start();
}
}
catch (IOException e) {
System.err.println("Error running thread.");
}
} }
} }
@@ -2,62 +2,145 @@ package com.university.chat.Server;
import com.university.chat.Common.ChatMessage; import com.university.chat.Common.ChatMessage;
import com.university.chat.Common.FileMessage; import com.university.chat.Common.FileMessage;
import com.university.chat.Common.MessageType;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket; import java.net.Socket;
import java.nio.file.Files; import java.nio.file.Files;
public class ClientSession implements Runnable { public class ClientSession implements Runnable {
private String username; private String username;
private Socket socket;
private final UserManager userManager;
private ObjectOutputStream out;
private ObjectInputStream in;
public ClientSession(Socket socket, UserManager userManager) { public ClientSession(Socket socket, UserManager userManager) {
// TODO : Create an ObjectOutputStream from socket.getOutputStream()
// and an ObjectInputStream from socket.getInputStream(). this.userManager = userManager;
this.socket = socket;
try {
this.out = new ObjectOutputStream(socket.getOutputStream());
out.flush();
this.in = new ObjectInputStream(socket.getInputStream());
}
catch (IOException e) {
System.out.println("Error initializing stream: " + e.getMessage());
}
} }
@Override @Override
public void run() { public void run() {
try { try {
System.out.println("welcome");
// TODO: Welcome the user (login step) Object object = in.readObject();
// 1. Read the first object sent by the client.
// 2. Check it's a ChatMessage with type LOGIN.
// 3. Extract the username.
// 4. Try to register the user via userManager.addUser(...).
// 5. If the username is taken, send back LOGIN_FAILED and close the socket.
// 6. Otherwise, create the user's folders with FileManager.createUserFolders(...)
// and send back LOGIN_SUCCESS.
// TODO: Main message loop if (object instanceof ChatMessage login && login.getType() == MessageType.LOGIN) {
// In a loop, call in.readObject(), you can separate messages by their type:
// - if it's a ChatMessage -> call handleChatMessage(msg)
// - if it's a FileMessage -> call handleFileMessage(fileMsg)
// Keep looping until the connection is closed (an exception will be thrown).
} catch (Exception e) { username = login.getSender();
if (userManager.addUser(username, this)) {
System.out.printf("Successfully registered: %s\n", username);
FileManager.createUserFolders(username);
out.writeObject(
new ChatMessage(MessageType.LOGIN_SUCCESS,
"[server]",
username,
null
));
out.flush();
broadcastUserList();
}
else {
out.writeObject(
new ChatMessage(MessageType.LOGIN_FAILED,
"[server]",
username,
null
));
out.flush();
socket.close();
return;
}
} else {
socket.close();
return;
}
while (true) {
Object msg = in.readObject();
if (msg instanceof ChatMessage chatMsg) {
handleChatMessage(chatMsg);
}
else if (msg instanceof FileMessage fileMsg) {
handleFileMessage(fileMsg);
}
}
} catch (IOException | ClassNotFoundException e) {
System.out.println("Disconnected: " + username); System.out.println("Disconnected: " + username);
} finally { }
// TODO: Remove the user from UserManager so they no longer finally {
// receive broadcasts or appear in users list if (username != null) {
userManager.removeUser(username);
broadcastUserList();
}
} }
} }
private void handleChatMessage(ChatMessage msg) throws IOException { private void handleChatMessage(ChatMessage msg) throws IOException {
switch (msg.getType()) { switch (msg.getType()) {
case PUBLIC_MESSAGE -> { case PUBLIC_MESSAGE -> {
// TODO: Broadcast this message to every connected client. Iterable<ClientSession> users = userManager.getAllSessions();
for (ClientSession user : users) {
if (user != this) {
user.sendObject(msg);
}
}
} }
case PRIVATE_MESSAGE -> { case PRIVATE_MESSAGE -> {
// TODO: Forward this message to the receiver user.
ClientSession receiver = userManager.getUser(msg.getReceiver());
if (receiver != null) {
receiver.sendObject(msg);
}
else {
sendObject(new ChatMessage(
MessageType.PRIVATE_MESSAGE,
"[server]",
username,
"User not found: " + msg.getReceiver()
));
}
} }
case USER_LIST -> { case USER_LIST -> {
// TODO: Reply to the requester with the list of online users.
sendObject(new ChatMessage(MessageType.USER_LIST,
"[server]",
username,
userManager.listUsers())
);
} }
} }
} }
private synchronized void sendObject(Object obj) throws IOException {
out.writeObject(obj);
out.flush();
}
private void handleFileMessage(FileMessage fileMsg) throws IOException { private void handleFileMessage(FileMessage fileMsg) throws IOException {
// Storing the file // Storing the file
var sentPath = FileManager.getSentPath(fileMsg.getSender(), fileMsg.getFilename()); var sentPath = FileManager.getSentPath(fileMsg.getSender(), fileMsg.getFilename());
@@ -66,6 +149,26 @@ public class ClientSession implements Runnable {
Files.write(sentPath, fileMsg.getData()); Files.write(sentPath, fileMsg.getData());
Files.write(recvPath, fileMsg.getData()); Files.write(recvPath, fileMsg.getData());
// TODO: Forward the received file-message to the destination user. ClientSession receiver = userManager.getUser(fileMsg.getReceiver());
receiver.sendObject(fileMsg);
} }
private void broadcastUserList() {
try {
String activeUsers = userManager.listUsers();
ChatMessage updateMsg = new ChatMessage(
MessageType.USER_LIST,
"[server]",
null,
activeUsers
);
for (ClientSession session : userManager.getAllSessions()) {
session.sendObject(updateMsg);
}
} catch (IOException e) {
System.out.println("Error broadcasting user list: " + e.getMessage());
}
}
} }
@@ -19,7 +19,7 @@ public class UserManager {
} }
public String listUsers() { public String listUsers() {
return users.keySet().toString(); return String.join(",", users.keySet());
} }
public Iterable<ClientSession> getAllSessions() { public Iterable<ClientSession> getAllSessions() {
+77
View File
@@ -0,0 +1,77 @@
.root {
-fx-font-family: "Segoe UI", "Helvetica", sans-serif;
-fx-background-color: #1e1e2f;
}
.title-bar {
-fx-background-color: #262638;
-fx-padding: 5 10 5 10;
-fx-alignment: CENTER_LEFT;
}
.title-text {
-fx-text-fill: #a0a0c0;
-fx-font-size: 13px;
-fx-font-weight: bold;
}
.title-button {
-fx-background-color: transparent;
-fx-text-fill: white;
-fx-font-size: 14px;
-fx-cursor: hand;
}
.title-button:hover {
-fx-background-color: #3b3b55;
}
.title-button-close:hover {
-fx-background-color: #e81123;
}
.label {
-fx-text-fill: #d1d1e0;
}
.text-field {
-fx-background-color: #2a2a3d;
-fx-text-fill: white;
-fx-background-radius: 6;
-fx-border-color: #3c3c55;
-fx-padding: 8;
}
.button {
-fx-background-color: #4da3ff;
-fx-text-fill: white;
-fx-background-radius: 6;
-fx-cursor: hand;
-fx-padding: 6 15 6 15;
}
.button:hover {
-fx-background-color: #61afff;
}
.list-view, .text-area {
-fx-background-color: #262638;
-fx-control-inner-background: #262638;
-fx-text-fill: white;
}
.attach-button{
-fx-font-size:16px;
-fx-background-color: transparent;
}
.attach-button:hover{
-fx-background-color:#3b3b55;
}
.content-wrapper {
-fx-padding: 20;
}
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.university.chat.Client.UI.ChatController">
<top>
<BorderPane fx:id="titleBar" styleClass="title-bar">
<center><Label text="UniChat" styleClass="title-text"/></center>
<right>
<HBox>
<Button text="—" styleClass="title-button" onAction="#minimize"/>
<Button text="□" styleClass="title-button" onAction="#maximize"/>
<Button text="✕" styleClass="title-button, title-button-close" onAction="#closeApp"/>
</HBox>
</right>
</BorderPane>
</top>
<center>
<HBox styleClass="content-wrapper" spacing="20">
<VBox spacing="10" prefWidth="200">
<Label text="Online Users"/>
<ListView fx:id="usersList" VBox.vgrow="ALWAYS"/>
</VBox>
<VBox spacing="10" HBox.hgrow="ALWAYS">
<Label text="Private Chat"/>
<AnchorPane fx:id="chatAnchor" style="-fx-background-color: #262638; -fx-background-radius: 8;" VBox.vgrow="ALWAYS"/>
</VBox>
<VBox spacing="10" prefWidth="250">
<Label text="Public Chat"/>
<TextArea fx:id="publicChatArea" editable="false" VBox.vgrow="ALWAYS"/>
<VBox fx:id="fileTransferBox" spacing="4" visible="false" managed="false">
<Label fx:id="fileTransferLabel" text="Sending file..."/>
<ProgressBar fx:id="fileTransferProgress" progress="0.0" prefWidth="260"/>
</VBox>
<HBox spacing="6">
<Button text="📎" onAction="#sendFile"/>
<TextField fx:id="messageField" HBox.hgrow="ALWAYS" promptText="Message..."/>
<Button text="Send" onAction="#sendPublicMessage"/>
</HBox>
</VBox>
</HBox>
</center>
</BorderPane>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.university.chat.Client.UI.LoginController">
<top>
<BorderPane styleClass="title-bar">
<center><Label text="Login" styleClass="title-text"/></center>
<right>
<HBox fx:id="titleBar">
<Button text="—" styleClass="title-button" onAction="#minimize"/>
<Button text="▢" styleClass="title-button" onAction="#maximize"/>
<Button text="✕" styleClass="title-button, title-button-close" onAction="#closeApp"/>
</HBox>
</right>
</BorderPane>
</top>
<center>
<VBox alignment="CENTER" maxWidth="300" spacing="15">
<Label text="Welcome to UniChat" style="-fx-font-size: 20px; -fx-font-weight: bold;"/>
<TextField fx:id="usernameField" promptText="Enter your username"/>
<Button fx:id="nameButton" text="Connect" maxWidth="Infinity" onAction="#onConnectClicked"/>
</VBox>
</center>
</BorderPane>