Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .env.example

This file was deleted.

1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
target/
/.idea/
.env
16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,8 @@ A JavaFX-based chat client using [ntfy](https://docs.ntfy.sh/) for backend messa
- Unit tests for `Model` class
- (Advanced) Send files via "Attach local file" option


## Requirements

- **Java**
- **Version**: `25`

- **Maven Compiler Plugin**
- **Version**: `3.11.0`
- **Configuration**:
- **Release**: `25`

## Usage
1. Set `JAVA_HOME` to JDK 25.
2. Create a **.env** file with the required variables. You can also clone and fill **.env.example** and rename it to `.env`.
## 🚀 Run Instructions
1. Set `JAVA_HOME` to JDK 25
2. Start with:
```bash
./mvnw clean javafx:run
35 changes: 0 additions & 35 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,6 @@
<javafx.version>25</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down Expand Up @@ -60,18 +45,6 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-junit5</artifactId>
<version>4.0.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand All @@ -90,14 +63,6 @@
<noManPages>true</noManPages>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
224 changes: 8 additions & 216 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,230 +1,22 @@
package com.example;

import com.example.client.ChatNetworkClient;
import com.example.domain.ChatModel;
import com.example.domain.NtfyEventResponse;
import com.example.domain.NtfyMessage;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
* Controller layer: mediates between the view (FXML) and the model.
*/
public class HelloController {
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
private ChatNetworkClient client;
private String baseUrl;
private String topic;
private File selectedFile = null;

@FXML
private Label messageLabel;

@FXML
private ListView<NtfyEventResponse> messagesList;

@FXML
private TextField messageInput;

@FXML
private TextField titleInput;

@FXML
private TextField tagsInput;

public void setClient(ChatNetworkClient client, String baseUrl, String topic) {
this.client = client;
this.baseUrl = baseUrl;
this.topic = topic;
}

public void setModel(ChatModel model) {
messagesList.setItems(model.getMessages());
messagesList.setCellFactory(list -> new MessageCell());
}

private static String formatTime(long epochSeconds) {
Instant instant = Instant.ofEpochSecond(epochSeconds);
LocalTime time = LocalTime.ofInstant(instant, ZoneId.systemDefault());
return time.toString();
}

private void showStatus(String text) {
messageLabel.setText(text);
Timeline t = new Timeline(new KeyFrame(javafx.util.Duration.seconds(3),
ev -> messageLabel.setText("")));
t.setCycleCount(1);
t.play();
}

private final HelloModel model = new HelloModel();

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

if (file != null) {
selectedFile = file;
messageLabel.setText("Attachment selected: " + file.getName());
}
}
private Label messageLabel;

@FXML
private void onSend() {
String txt = messageInput.getText();

if ((txt == null || txt.isBlank()) && selectedFile == null) {
showStatus("Nothing to send");
return;
}

String title = titleInput.getText();
if (title != null && title.isBlank()) title = null;

String tagsRaw = tagsInput.getText();
List<String> tags = null;

if (tagsRaw != null && !tagsRaw.isBlank()) {
tags = java.util.Arrays.stream(tagsRaw.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}

NtfyMessage msg = new NtfyMessage.Builder()
.id(UUID.randomUUID().toString())
.time(System.currentTimeMillis())
.event("message")
.topic(topic)
.message(txt)
.title(title)
.tags(tags)
.attach(null)
.filename(null)
.build();

try {
client.send(baseUrl, msg, selectedFile);
showStatus(selectedFile == null ? "Message sent" : "Attachment sent");
} catch (InterruptedException | IOException e) {
showStatus("Error sending: " + e.getMessage());
}

messageInput.clear();
titleInput.clear();
tagsInput.clear();
selectedFile = null;
}


private static final class MessageCell extends ListCell<NtfyEventResponse> {
@Override
protected void updateItem(NtfyEventResponse msg, boolean empty) {
super.updateItem(msg, empty);

if (empty || msg == null) {
setText(null);
setGraphic(null);
return;
}

setText(null);

VBox container = new VBox();
container.setSpacing(6);
container.getStyleClass().add("message-bubble");

container.setStyle("-fx-alignment: CENTER_LEFT;");
if (msg.title() != null) {
Label titleLabel = new Label(msg.title());
titleLabel.getStyleClass().add("message-title");
container.getChildren().add(titleLabel);
}

if (msg.attachment() != null) {
NtfyEventResponse.Attachment att = msg.attachment();

if (att.type() != null && att.type().startsWith("image")) {
Image image = new Image(att.url(), 300, 0, true, true);
ImageView imageView = new ImageView(image);
container.getChildren().add(imageView);
} else {
Label fileLabel = getFileLabel(att);
container.getChildren().add(fileLabel);
}
}

if (msg.message() != null && !msg.message().isBlank()) {
Label messageLabel = new Label(msg.message());
messageLabel.setWrapText(true);
messageLabel.getStyleClass().add("message-text");
container.getChildren().add(messageLabel);
}

if (msg.tags() != null && !msg.tags().isEmpty()) {
Label tagsLabel = new Label(String.join(", ", msg.tags()));
tagsLabel.getStyleClass().add("message-tags");
container.getChildren().add(tagsLabel);
}

if (msg.time() != null) {
Label timeLabel = new Label(formatTime(msg.time()));
timeLabel.getStyleClass().add("message-time");
container.getChildren().add(timeLabel);
}

setGraphic(container);
}

// Helper method to allow user to open file
private static Label getFileLabel(NtfyEventResponse.Attachment att) {
Label fileLabel = new Label("Open file: " + (att.name() != null ? att.name() : att.url()));
fileLabel.setStyle("-fx-text-fill: #2c75ff; -fx-underline: true;");
fileLabel.setOnMouseClicked(ev -> {
try {
String url = att.url();

// method that works on linux as Desktop is not always supported and crashes application
if (System.getProperty("os.name").toLowerCase().contains("linux")) {
try {
new ProcessBuilder("xdg-open", url).start();
return;
}catch (IOException e) {
log.error("Error opening file: {}", url, e);
}
}

if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().browse(new URI(url));
}

} catch (IOException | URISyntaxException ex) {
log.error("Failed to open attachment: {}", ex.getMessage());
log.error(Arrays.toString(ex.getStackTrace()));
}
});
return fileLabel;
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
}
}
}
44 changes: 5 additions & 39 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
@@ -1,59 +1,25 @@
package com.example;

import com.example.client.ChatNetworkClient;
import com.example.client.NtfyHttpClient;
import com.example.domain.ChatModel;
import com.example.domain.NtfyMessage;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;
import java.util.Properties;

import static com.example.utils.EnvLoader.loadEnv;

public class HelloFX extends Application {
private static final Logger log = LoggerFactory.getLogger("MAIN");
static final ChatModel model = new ChatModel();

@Override
public void start(Stage stage) throws Exception {
Properties env = loadEnv();
String baseUrl = env.getProperty("NTFY_BASE_URL", "https://ntfy.sh");
String topic = env.getProperty("NTFY_TOPIC");

if (topic == null || topic.isBlank()) {
throw new IllegalStateException("NTFY_TOPIC is not set");
}

FXMLLoader loader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = loader.load();

HelloController controller = loader.getController();
controller.setModel(model);

ChatNetworkClient client = new NtfyHttpClient(model);
controller.setClient(client, baseUrl, topic);

client.subscribe(baseUrl, topic);

Scene scene = new Scene(root);
scene.getStylesheets().add(
Objects.requireNonNull(HelloFX.class.getResource("styles.css")).toExternalForm()
);

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");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
launch(args);

launch();
}

}
Loading