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
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);
}
}
});
}

@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();
}
}

@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();
}
}

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