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
20 changes: 18 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
<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>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>4.0.0-beta.15</version>
<scope>test</scope>
</dependency>
Comment on lines +58 to +63
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

🧩 Analysis chain

Beta version of WireMock in use.

You're using WireMock 4.0.0-beta.15, which is a pre-release version. While this is test-scoped, beta versions may have stability issues or breaking changes. Consider whether a stable release would be more appropriate for your testing needs.


🌐 Web query:

What is the latest stable version of WireMock for Maven?

💡 Result:

The latest stable WireMock for Maven is org.wiremock:wiremock version 3.13.1 (released June 11, 2025). [1][2]

Maven dependency:

org.wiremock
wiremock
3.13.1
test

Sources: [1] GitHub releases — WireMock 3.13.1. [2] Maven Central / MVNRepository entry for org.wiremock:wiremock 3.13.1.


Consider upgrading to stable WireMock 3.13.1 for improved stability.

You're using WireMock 4.0.0-beta.15, which is a pre-release version. The latest stable version is WireMock 3.13.1 (released June 11, 2025). While beta versions may be intentional for forward compatibility testing, they can introduce stability issues and breaking changes. Upgrading to the stable release would provide better reliability for your test suite.

🤖 Prompt for AI Agents
In pom.xml around lines 58 to 63, the dependency currently pins WireMock to the
pre-release version 4.0.0-beta.15; change the <version> element to 3.13.1 to use
the stable WireMock release (i.e., replace 4.0.0-beta.15 with 3.13.1), then run
the test suite (mvn -DskipTests=false test) and fix any test API differences if
they surface.

</dependencies>
<build>
<plugins>
Expand All @@ -55,7 +71,7 @@
<configuration>
<mainClass>com.example.HelloFX</mainClass>
<options>
<option>--enable-native-access=javafx.graphics</option>
<option>--enable-native-access=javafx.graphics</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
Expand All @@ -65,4 +81,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
26 changes: 26 additions & 0 deletions src/main/java/com/example/FxUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example;

import javafx.application.Platform;

public class FxUtils {

/**
* Runs a task on the JavaFX thread.
* If already on the thread, runs immediately.
* If not, schedules it to run later.
* If JavaFX is not initialized, runs on the current thread.
*/

public static void runOnFx(Runnable task) {
try {
if (Platform.isFxApplicationThread()) {
task.run();
} else {
Platform.runLater(task);
}
} catch (IllegalStateException notInitialized) {
//headless
task.run();
}
}
}
146 changes: 140 additions & 6 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,156 @@
package com.example;

import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;

import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

/**
* Controller layer: mediates between the view (FXML) and the model.
* Controller for the chat app.
* <p>
* Connects the FXML view to the HelloModel.
* Handles sending messages, changing topics, and updating the UI.
*/

public class HelloController {

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

@FXML
private Button sendButton;

@FXML
private Label messageLabel;

@FXML
private Label topicLabel;

@FXML
private ListView<NtfyMessageDto> messageView;

@FXML
private TextArea messageInput;

@FXML
private TextField topicInput;

@FXML
private Button changeTopicButton;

private final DateTimeFormatter timeFormatter =
DateTimeFormatter.ofPattern("HH:mm:ss")
.withZone(ZoneId.systemDefault());
Comment on lines +47 to +49
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

Remove unused timeFormatter field.

The timeFormatter field is declared and initialized but never used in the controller. The cell factory (lines 85-117) doesn't display timestamps, so this appears to be leftover from a previous implementation.

-    private final DateTimeFormatter timeFormatter =
-            DateTimeFormatter.ofPattern("HH:mm:ss")
-                    .withZone(ZoneId.systemDefault());
-
     @FXML
     private void initialize() {

Also remove the unused imports:

-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
📝 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
private final DateTimeFormatter timeFormatter =
DateTimeFormatter.ofPattern("HH:mm:ss")
.withZone(ZoneId.systemDefault());
@FXML
private void initialize() {
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 47 to 49, the
private field timeFormatter is declared and never used; remove that field
declaration and its initializer, and then remove any now-unused imports (e.g.,
java.time.DateTimeFormatter, java.time.ZoneId or related java.time imports) from
the top of the file so there are no unused import warnings.


@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
messageLabel.setText(model.getGreeting());

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

topicLabel.setText("/" + model.getCurrentTopic());
model.currentTopicProperty().addListener((obs, oldVal, newVal) -> {
topicLabel.setText("/" + newVal);
});

messageView.setItems(model.getMessages());

messageInput.textProperty().bindBidirectional(model.messageToSendProperty());

sendButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> {
String text = messageInput.getText();
return text == null || text.trim().isEmpty();
},
messageInput.textProperty()
));

if (changeTopicButton != null) {
changeTopicButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> {
String text = topicInput.getText();
return text == null || text.trim().isEmpty();
},
topicInput.textProperty()
));
}


messageView.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(NtfyMessageDto msg, boolean empty) {
super.updateItem(msg, empty);

if (empty || msg == null || msg.message() == null || msg.message().isBlank()) {
setText(null);
setGraphic(null);
} else {
// Skapa bubble-label
Label bubble = new Label(msg.message());
bubble.setWrapText(true);
bubble.setMaxWidth(250);
bubble.setPadding(new Insets(10));
bubble.getStyleClass().add("chat-bubble"); // Basstyle

HBox container = new HBox(bubble);
container.setPadding(new Insets(5));

// Använd CSS-klasser för skickat/mottaget
if (model.getUserId().equals(msg.id())) {
bubble.getStyleClass().add("chat-bubble-sent");
container.setAlignment(Pos.CENTER_RIGHT);
} else {
bubble.getStyleClass().add("chat-bubble-received");
container.setAlignment(Pos.CENTER_LEFT);
}

setText(null);
setGraphic(container);
}
}
});
Comment on lines +85 to +117
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

🧩 Analysis chain

Verify sender identification logic.

Line 105 compares model.getUserId() with msg.id() to determine if the message was sent by the current user. However, it's unclear whether msg.id() represents the message ID or the sender/user ID. Looking at test data in HelloModelTest (e.g., line 252: new NtfyMessageDto("Test", 1, "message", "myroom", "Test")), the first parameter appears to be a message identifier rather than a user identifier.

If id() is the message ID (not sender ID), this comparison will never match, and all messages will be styled as "received" instead of properly distinguishing sent vs. received messages.

Verify the NtfyMessageDto structure and confirm which field contains the sender/user information:


🏁 Script executed:

#!/bin/bash
# Check the NtfyMessageDto record definition
rg -A 5 "record NtfyMessageDto"

Length of output: 190



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where NtfyMessageDto is instantiated to understand what the id field contains
rg "new NtfyMessageDto" -B 2 -A 2 -n

Length of output: 1712


🏁 Script executed:

#!/bin/bash
# Check the HelloModel class to see what getUserId() returns and how it's used
rg -A 10 "getUserId\(\)" src/main/java/com/example/HelloModel.java

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Look for any service/repository code that creates NtfyMessageDto
rg "NtfyMessageDto" -B 3 -A 3 -n --type java | head -50

Length of output: 3933


🏁 Script executed:

#!/bin/bash
# Find the real NtfyConnection implementation (not the spy)
rg "class.*NtfyConnection" --type java -A 5

Length of output: 1021


🏁 Script executed:

#!/bin/bash
# Look for where getUserId is set/initialized in the connection
rg "getUserId" --type java -B 3 -A 3 -n | head -60

Length of output: 2147


🏁 Script executed:

#!/bin/bash
# Search for any parsing or mapping of Ntfy messages/responses
rg -i "ntfy\|parse\|message" src/main/java/com/example/ --type java -n | grep -i "parse\|json\|response\|send" | head -30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the full NtfyConnectionImpl implementation, especially the receive method
cat src/main/java/com/example/NtfyConnectionImpl.java

Length of output: 3888


🏁 Script executed:

#!/bin/bash
# Search for where NtfyMessageDto is constructed
rg "new NtfyMessageDto\|NtfyMessageDto(" --type java -B 5 -A 2 -n

Length of output: 120



---

🌐 Web query:

```web
Ntfy protocol JSON message format sender ID user identification

💡 Result:

Short answer: ntfy's JSON message format does not include a built‑in "sender" or "user" field. Messages (when published as JSON or received via /json or SSE) include id, time, topic, message, title, tags, priority, actions, click/attachment, etc., but not a publisher/sender property — identification of who published is handled by authentication (Basic auth or access tokens) at the HTTP layer, not as a JSON field in the message. See ntfy publish/JSON and subscribe docs and the auth/config docs for details. [1][2][3][4]

References

  • ntfy publish (fields, headers, actions examples). [1]
  • Publish-as-JSON (full list of supported JSON publish fields). [2]
  • Subscribe API — JSON message format returned by /json and SSE (id, time, topic, message, title, tags, priority, attachment...). [3]
  • Configuration / authentication (users, tokens, ACLs — how publishers are identified/authenticated). [4]

Fix sender identification logic — msg.id() returns message ID, not sender ID.

The comparison at line 105 (model.getUserId().equals(msg.id())) will never match. According to the Ntfy protocol, the JSON message format does not include sender/publisher information. The id field contains the message ID generated by the Ntfy server, not the user ID of who sent the message. Therefore, all messages will always be styled as "received" regardless of sender.

The current implementation cannot distinguish sent from received messages without additional logic. Consider either:

  • Storing locally-sent messages separately and tracking them before receiving server responses
  • Appending a sender identifier to the message payload before publishing (if the Ntfy server permits custom fields)
  • Implementing a different mechanism to correlate outgoing and incoming messages
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 85 to 117, the
cell factory currently compares model.getUserId() to msg.id(), but msg.id() is
the server-generated message ID (not the sender), so the sent/received styling
never matches; fix by correlating incoming messages to locally-sent messages or
an explicit sender field: modify the message send path to attach a
client-generated identifier (e.g., a UUID or local delivery token) or sender tag
to the payload and persist that token in a local Set/Map when sending, then
change the cell factory to check that token or lookup in the local sent-message
store (instead of msg.id()) to decide sent vs received; alternatively, if you
cannot change payload, track locally-sent messages by their content+timestamp
and match incoming messages against that store, removing entries once matched.



// Scrolla ner till senaste meddelandet
model.getMessages().addListener((javafx.collections.ListChangeListener<NtfyMessageDto>) change -> {
Platform.runLater(() -> {
if (!messageView.getItems().isEmpty()) {
messageView.scrollTo(messageView.getItems().size() - 1);
}
});
});
}

@FXML
private void sendMessage(ActionEvent actionEvent) {
model.sendMessageAsync(success -> {
if (success) {
Platform.runLater(() -> messageInput.clear());
Platform.runLater(() -> messageInput.requestFocus());
} else {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Send Failed");
alert.setHeaderText("Failed to send message");
alert.setContentText("Could not send your message. Please try again.");
alert.showAndWait();
});
}
});
}

@FXML
private void changeTopic(ActionEvent actionEvent) {
String newTopic = topicInput.getText();
if (newTopic != null && !newTopic.isBlank()) {
model.setCurrentTopic(newTopic);
topicInput.clear();
}
}
}
}
18 changes: 16 additions & 2 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.util.Objects;

/**
* Main JavaFX application class for RuneChat.
* <p>
* Loads the FXML view, applies the stylesheet, and starts the application window.
*/

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

Scene scene = new Scene(root, 768, 576);
stage.setTitle("RuneChat");

scene.getStylesheets().add(Objects.requireNonNull(HelloFX.class.getResource("style.css")).toExternalForm());

stage.setScene(scene);
stage.show();


}

public static void main(String[] args) {
Expand Down
110 changes: 102 additions & 8 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,109 @@
package com.example;

import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import java.util.function.Consumer;

import static com.example.FxUtils.runOnFx;

/**
* Model layer: encapsulates application data and business logic.
* Model layer for the chatapp RuneChat.
* <p>
* Manages messages, the current topic, and sending/receiving messages via NtfyConnection.
*/

public class HelloModel {
/**
* Returns a greeting based on the current Java and JavaFX versions.
*/

private final NtfyConnection connection;
private final ObservableList<NtfyMessageDto> messages = FXCollections.observableArrayList();
private final StringProperty messageToSend = new SimpleStringProperty();
private final StringProperty currentTopic = new SimpleStringProperty();

public HelloModel(NtfyConnection connection) {
this.connection = connection;
this.currentTopic.set(connection.getCurrentTopic());
receiveMessage();
}
Comment on lines +26 to +30
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

Avoid starting async operations in the constructor.

Line 23 calls receiveMessage(), which starts an asynchronous operation, before the object is fully constructed. This can lead to race conditions where callbacks run while the object is still being initialized.

Consider refactoring to separate construction from initialization:

     public HelloModel(NtfyConnection connection) {
         this.connection = connection;
         this.currentTopic.set(connection.getCurrentTopic());
-        receiveMessage();
     }
+
+    public void start() {
+        receiveMessage();
+    }

Then call model.start() after construction completes in the controller.

📝 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
public HelloModel(NtfyConnection connection) {
this.connection = connection;
this.currentTopic.set(connection.getCurrentTopic());
receiveMessage();
}
public HelloModel(NtfyConnection connection) {
this.connection = connection;
this.currentTopic.set(connection.getCurrentTopic());
}
public void start() {
receiveMessage();
}
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 20 to 24, remove the
call to receiveMessage() from the constructor to avoid starting async work
during object construction; instead add a public start() (or init()) method that
calls receiveMessage(), keep the constructor limited to assigning fields
(connection and currentTopic), and update all callers (controllers/creators of
HelloModel) to invoke model.start() immediately after construction completes.


public ObservableList<NtfyMessageDto> getMessages() {
return messages;
}

public String getMessageToSend() {
return messageToSend.get();
}

public StringProperty messageToSendProperty() {
return messageToSend;
}

public void setMessageToSend(String message) {
messageToSend.set(message);
}

public String getCurrentTopic() {
return currentTopic.get();
}

public StringProperty currentTopicProperty() {
return currentTopic;
}

public void setCurrentTopic(String topic) {
if (topic != null && !topic.isBlank()) {
connection.setCurrentTopic(topic);
this.currentTopic.set(topic);
messages.clear();
receiveMessage();
}
}
Comment on lines +56 to +63
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

Potential duplicate message listeners when changing topics.

Line 55 calls receiveMessage() every time the topic changes, but there's no mechanism to stop the previous listener. This could result in multiple concurrent receive operations, leading to duplicate messages or resource leaks.

Consider implementing a mechanism to:

  1. Cancel/stop the previous receive operation before starting a new one
  2. Track the active receive operation (e.g., store the CompletableFuture)
  3. Ensure only one receive operation is active at a time
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 50 to 57, calling
receiveMessage() on every topic change can spawn multiple concurrent listeners;
store the active receive operation in a field (e.g., a CompletableFuture<?>
activeReceive), before starting a new receive check if activeReceive != null and
not done, then cancel it (activeReceive.cancel(true)) or signal it to stop and
wait/handle completion, set activeReceive to the new CompletableFuture returned
by receiveMessage(), and ensure receiveMessage() honors
interruption/cancellation and clears activeReceive on normal completion or error
so only one receive operation is active at a time.


public String getUserId() {
return connection.getUserId();
}

public String getGreeting() {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
return "RuneChat";
}

public boolean canSendMessage() {
String msg = messageToSend.get();
return msg != null && !msg.isBlank();
}

public void sendMessageAsync(Consumer<Boolean> callback) {
String msg = messageToSend.get();
if (msg == null || msg.isBlank()) {
System.out.println("Nothing to send!");
callback.accept(false);
return;
}

connection.send(msg, success -> {
if (success) {
runOnFx(() -> {
if (msg.equals(messageToSend.get())) {
messageToSend.set("");
}
});
callback.accept(true);
} else {
System.out.println("Failed to send message!");
callback.accept(false);
}
});
}
}

public void receiveMessage() {
connection.receive(m -> {
if (m == null || m.message() == null || m.message().isBlank()) return;
runOnFx(() -> messages.add(m));
});
}


}
Loading
Loading