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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target/
/.idea/
.env
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
59 changes: 55 additions & 4 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
package com.example;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

import java.io.File;

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

private final HelloModel model = new HelloModel();
private final NtfyConnection connection = new NtfyConnectionImpl();
private final HelloModel model = new HelloModel(connection);

public ListView<Object> messageView;

@FXML
private Label topic;

@FXML
private Label messageLabel;
TextField input;

File attachment = null;

@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
topic.textProperty().bind(connection.topicProperty());
messageView.setItems(model.getFormatedMessages());
model.receiveMessage();
}

/**
* Send a message, file if one has been attached else a string from the input field
* @param actionEvent
*/
public void sendMessage(ActionEvent actionEvent) {
if(attachment != null) {
connection.sendFile(attachment);
attachment = null;
}
else{
model.sendMessage(input.getText().trim());
}
input.clear();
}

/**
* Opens a filechooser and adds selected file as attachement to be sent
* @param actionEvent
*/
public void sendFile(ActionEvent actionEvent){
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Attach a file");

Stage stage = (Stage) topic.getScene().getWindow();


attachment = fileChooser.showOpenDialog(stage);
if(attachment != null) {
input.setText(attachment.getAbsolutePath());
}
}

public void changeTopic(ActionEvent actionEvent) {
model.changeTopic();
Comment on lines +38 to +71
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Surface send failures and avoid sending empty messages/attachments

In sendMessage and sendFile:

  • The boolean results from connection.send(...) and connection.sendFile(...) are ignored, so the UI can’t distinguish between success and failure (e.g. connection down, server error).
  • sendMessage will happily call model.sendMessage(input.getText().trim()) even if the trimmed text is empty; the connection then rejects it, but you still hit the network and the user gets no feedback.
  • For attachments, you clear attachment and the input regardless of whether sendFile actually worked.

Consider:

  • Checking for blank input before delegating to the model, and simply returning (or showing an error) if empty.
  • Using the boolean result to provide minimal user feedback (status label, alert, or log) and not clearing attachment on failure.
    public void sendMessage(ActionEvent actionEvent) {
        if (attachment != null) {
-            connection.sendFile(attachment);
-            attachment = null;
+            boolean ok = connection.sendFile(attachment);
+            if (ok) {
+                attachment = null;
+            } else {
+                // e.g. show error or log
+                return;
+            }
        } else {
-            model.sendMessage(input.getText().trim());
+            String text = input.getText().trim();
+            if (text.isEmpty()) {
+                return;
+            }
+            model.sendMessage(text);
        }
        input.clear();
    }
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 38 to 71,
sendMessage and sendFile ignore the boolean result from connection.send(...) and
connection.sendFile(...) and allow empty messages/clear attachments even on
failure; change sendMessage to first trim and if the text is blank return or set
a status/error without calling model.sendMessage, call connection.send or
model.sendMessage and capture its boolean result, only clear input (and
attachment) when the call returns true, and on false show minimal feedback (set
a status label, log or show an alert) so the UI reflects send failures;
similarly, when sending attachments call connection.sendFile and only null out
attachment and clear the input on true, otherwise preserve attachment and show
feedback.

}
}
1 change: 1 addition & 0 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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);
Expand Down
130 changes: 124 additions & 6 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,133 @@
package com.example;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;

import java.io.IOException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;

/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {

private final NtfyConnection connection;

private final ObservableList<NtfyMessage> messageHistory = FXCollections.observableArrayList();
private final ObservableList<Object> formatedMessages = FXCollections.observableArrayList();


public HelloModel(NtfyConnection connection) {
this.connection = connection;
}


public ObservableList<Object> getFormatedMessages() {
return formatedMessages;
}

public ObservableList<NtfyMessage> getMessageHistory() {
return messageHistory;
}

/**
* Sends a string as message
* @param message String to send
*/
public void sendMessage(String message) {
connection.send(message);
}

/**
* Returns a greeting based on the current Java and JavaFX versions.
* Clears the display before opening a new connection to a topic and displaying it
*/
public String getGreeting() {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
public void receiveMessage() {
formatedMessages.clear();
connection.recieve(m -> Platform.runLater(() -> logMessage(m)));
}


/**
* Adds the message to internal message history and the values to be displayed formated in the display list
* If the message is an attachment it displays it as an image or hyperlink depending on content
* @param message received message from NTFY server
*/
public void logMessage(NtfyMessage message) {
messageHistory.add(message);

if(message.attachment() != null) {
try {
URL url = new URL(message.attachment().get("url"));

if(Pattern.matches("^image\\/\\w+",message.attachment().get("type"))) {//check if the attachment is an image
ImageView image = new ImageView(new Image(url.toExternalForm()));
image.setPreserveRatio(true);
image.setFitHeight ( 250 );
image.setFitWidth ( 250 );

formatedMessages.addFirst(image);
}

else{
Hyperlink hyperlink = new Hyperlink(url.toExternalForm());
formatedMessages.addFirst(hyperlink);
}

} catch (IOException e) {
throw new RuntimeException(e);
}
}


Date timeStamp = new Date(message.time()*1000);
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
String stringMessage = dateFormat.format(timeStamp) + " : " + message.message();
formatedMessages.addFirst(stringMessage);
}
Comment on lines +64 to +95
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

formatedMessages.addFirst(...) will not compile and attachment handling is brittle

In logMessage:

formatedMessages.addFirst(image);
...
formatedMessages.addFirst(hyperlink);
...
formatedMessages.addFirst(stringMessage);

ObservableList does not define addFirst; this will not compile. To prepend items, use index-based adds:

-                    formatedMessages.addFirst(image);
+                    formatedMessages.add(0, image);
...
-                    formatedMessages.addFirst(hyperlink);
+                    formatedMessages.add(0, hyperlink);
...
-        formatedMessages.addFirst(stringMessage);
+        formatedMessages.add(0, stringMessage);

Also, the attachment handling assumes the map always contains non-null "url" and "type" values:

URL url = new URL(message.attachment().get("url"));
Pattern.matches("^image\\/\\w+", message.attachment().get("type"));

If either key is missing or null, you’ll get NullPointerException or MalformedURLException that aren’t caught. To make this more robust:

  • Guard against null or missing keys before constructing the URL or matching the pattern.
  • Optionally fall back to treating such cases as plain text or ignoring the attachment rather than throwing.
Map<String, String> att = message.attachment();
if (att == null) {
    // no attachment
} else {
    String urlStr = att.get("url");
    String type = att.get("type");
    if (urlStr != null && type != null) {
        // existing logic
    }
}
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 64 to 95, replace
non-compiling formatedMessages.addFirst(...) calls with prepending via
index-based insertion (e.g., add(0, item)) and make the attachment handling
robust by null-checking message.attachment() and verifying the "url" and "type"
values before using them; validate/try-create the URL inside a narrower
try/catch that handles MalformedURLException/IOException and on failure fall
back to treating the attachment as plain text or skip it rather than throwing a
RuntimeException, and avoid NPEs by branching when keys are missing.


/**
* Opens as dialog with text input
* If a value is entered and the add button pressed, change the topic per the input
*/
public void changeTopic() {
Dialog dialog = new Dialog();
dialog.setTitle("Add Topic");

ButtonType addTopicButton = new ButtonType("Add Topic", ButtonBar.ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().addAll(addTopicButton, ButtonType.CANCEL);

TextField newTopic = new TextField();
newTopic.setPromptText("Topic");

GridPane gridPane = new GridPane();
gridPane.setHgap(10);
gridPane.setVgap(10);

gridPane.add(newTopic, 0, 0);

dialog.getDialogPane().setContent(gridPane);

Platform.runLater(() -> newTopic.requestFocus());

dialog.setResultConverter(pressedButton -> {
if (pressedButton == addTopicButton) {
if(!newTopic.getText().isBlank()){
connection.setTopic(newTopic.getText().trim());
receiveMessage();
}
}
return null;
});

dialog.show();
}
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/example/NtfyConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example;

import javafx.beans.property.SimpleStringProperty;

import java.io.File;
import java.util.function.Consumer;

public interface NtfyConnection {

SimpleStringProperty topic = new SimpleStringProperty();

public SimpleStringProperty topicProperty();

public String getTopic();

public void setTopic(String topic);

public boolean send(String message);

public void recieve(Consumer<NtfyMessage> messageHandler);

public boolean sendFile(File attachment);
}
Loading
Loading