Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
target/
/.idea/
.env
.env.local
*.env
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ A JavaFX-based chat client using [ntfy](https://docs.ntfy.sh/) for backend messa
2. Start with:
```bash
./mvnw clean javafx:run

## Author
Created for Lab 3 - Network Programming - Younes Ahmad
11 changes: 11 additions & 0 deletions ntfy-server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ntfy server configuration
base-url: "http://localhost:8080"

# Enable attachments
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "50M"
attachment-expiry-duration: "3h"

# Logging
log-level: "info"
Binary file added null
Binary file not shown.
105 changes: 79 additions & 26 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,68 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>javafx</artifactId>
<artifactId>javafx-chat-app</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>25</maven.compiler.release>
<javafx.version>21.0.3</javafx.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>6.0.0</junit.jupiter.version>
<assertj.core.version>3.27.6</assertj.core.version>
<mockito.version>5.20.0</mockito.version>
<javafx.version>25</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.core.version}</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.15.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.15.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.example.HelloFX</mainClass>
<options>
<option>--enable-native-access=javafx.graphics</option>
<option>--add-exports</option>
<option>javafx.graphics/com.sun.glass.utils=ALL-UNNAMED</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
</project>
3 changes: 0 additions & 3 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
import javafx.fxml.FXML;
import javafx.scene.control.Label;

/**
* Controller layer: mediates between the view (FXML) and the model.
*/
public class HelloController {

private final HelloModel model = new HelloModel();
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package com.example;

import com.example.util.EnvLoader;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloFX extends Application {

@Override
public void start(Stage stage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Hello MVC");
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("ChatView.fxml"));
Scene scene = new Scene(fxmlLoader.load());

stage.setTitle("Java25 Chat App");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
EnvLoader.load();
launch();
}

}
7 changes: 1 addition & 6 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package com.example;

/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {
/**
* Returns a greeting based on the current Java and JavaFX versions.
*/

public String getGreeting() {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
Expand Down
142 changes: 142 additions & 0 deletions src/main/java/com/example/controller/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.example.controller;

import com.example.model.ChatModel;
import com.example.model.NtfyMessage;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.stage.FileChooser;

import java.io.File;

public class ChatController {

@FXML
private ListView<String> messageListView;

@FXML
private TextField messageInput;

@FXML
private Label statusLabel;

private ChatModel model;

@FXML
public void initialize() {
this.model = new ChatModel();

updateStatusOnline();

messageListView.setCellFactory(listView -> new ListCell<String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
setStyle("");
} else {
setText(item);
setAlignment(Pos.CENTER_LEFT);
setStyle("-fx-background-color: rgba(255, 250, 240, 0.6); " +
"-fx-text-fill: #3d3d3d; " +
"-fx-padding: 14 18 14 18; " +
"-fx-border-color: rgba(214, 69, 69, 0.15); " +
"-fx-border-width: 0 0 0 3; " +
"-fx-font-size: 13px; " +
"-fx-background-radius: 0; " +
"-fx-border-radius: 0; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.05), 3, 0, 0, 2);");
}
}
});

model.getMessages().addListener((javafx.collections.ListChangeListener.Change<? extends NtfyMessage> change) -> {
while (change.next()) {
if (change.wasAdded()) {
for (NtfyMessage msg : change.getAddedSubList()) {
String formatted = "🌸 " + msg.message();
messageListView.getItems().add(formatted);
}
messageListView.scrollTo(messageListView.getItems().size() - 1);
}
}
});
}
Comment on lines +30 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resource leak: ChatModel subscription is never closed.

The ChatModel.connect() method creates a subscription, but there's no cleanup when the controller is destroyed. If the view is closed and recreated, this could lead to resource leaks.

Consider adding cleanup:

public void shutdown() {
    // Add a close/disconnect method to ChatModel and call it here
    if (model != null) {
        model.disconnect(); // You'll need to implement this
    }
}

Then ensure this is called when the stage closes (add a listener in start() method of HelloFX).

🤖 Prompt for AI Agents
In src/main/java/com/example/controller/ChatController.java around lines 30 to
70, the controller initializes a ChatModel subscription but never closes it;
implement a cleanup path by adding a public disconnect()/close() method on
ChatModel that unsubscribes/closes the underlying subscription and releases
resources, add a shutdown() (or @PreDestroy) method on ChatController that calls
model.disconnect() if model != null, and ensure this shutdown is invoked when
the view/stage closes (e.g., register a stage close listener in your application
start method to call controller.shutdown() or rely on lifecycle cleanup).


@FXML
private void handleSendButtonAction() {
String text = messageInput.getText();
if (text != null && !text.trim().isEmpty()) {
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
model.sendMessage(text.trim());
return null;
}
};

task.setOnSucceeded(e -> {
messageInput.clear();
updateStatusOnline();
});

task.setOnFailed(e -> {
System.err.println("Send failed: " + task.getException().getMessage());
updateStatusOffline();
});

new Thread(task).start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use ExecutorService instead of manual thread creation.

Creating threads manually with new Thread(task).start() is not ideal for resource management. Consider using a cached or fixed thread pool via ExecutorService to properly manage thread lifecycle.

Example using JavaFX utilities:

-            new Thread(task).start();
+            javafx.concurrent.Service<Void> service = new javafx.concurrent.Service<>() {
+                @Override
+                protected javafx.concurrent.Task<Void> createTask() {
+                    return task;
+                }
+            };
+            service.start();

Or more simply, consider Platform.runLater for the background work with proper thread pool management at the model level.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
new Thread(task).start();
javafx.concurrent.Service<Void> service = new javafx.concurrent.Service<>() {
@Override
protected javafx.concurrent.Task<Void> createTask() {
return task;
}
};
service.start();
🤖 Prompt for AI Agents
In src/main/java/com/example/controller/ChatController.java around line 94,
replace the manual thread creation ("new Thread(task).start()") with submission
to a shared ExecutorService (e.g., a cached or fixed thread pool) to manage
lifecycle and resource usage; create a private final ExecutorService field in
the controller or a shared model/service, call executor.submit(task) instead of
new Thread(...).start(), ensure long-lived Executors are shutdown appropriately
(e.g., in controller/application stop or @PreDestroy), and if task updates UI
elements, wrap UI updates with Platform.runLater to marshal back to the JavaFX
thread.

}
}

@FXML
private void handleAttachFileAction() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select File");
File file = fileChooser.showOpenDialog(messageInput.getScene().getWindow());

if (file != null) {
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
model.sendFile(file);
return null;
}
};

task.setOnSucceeded(e -> {
System.out.println("✅ File info sent: " + file.getName());
});

task.setOnFailed(e -> {
System.err.println("❌ File send failed: " + task.getException().getMessage());
});

new Thread(task).start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Same thread management issue as line 94.

Apply the same ExecutorService approach here for consistency.

🤖 Prompt for AI Agents
In src/main/java/com/example/controller/ChatController.java around line 121,
replace the direct new Thread(task).start() usage with the same ExecutorService
approach used at line 94: submit the Runnable/Callable to a shared
ExecutorService (reuse the existing executor field or factory method), handle
returned Future if needed, and avoid creating raw threads; ensure proper error
handling around task submission and do not call shutdown here (manage lifecycle
where the executor is created).

}
}

private void updateStatusOnline() {
if (statusLabel != null) {
statusLabel.setText("● online");
statusLabel.setStyle("-fx-text-fill: #c93939; " +
"-fx-font-size: 10px; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 2, 0, 0, 1);");
}
}

private void updateStatusOffline() {
if (statusLabel != null) {
statusLabel.setText("● offline");
statusLabel.setStyle("-fx-text-fill: #6b5d54; " +
"-fx-font-size: 10px; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 2, 0, 0, 1);");
}
}
}
Loading
Loading