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
19 changes: 18 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,41 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>


</dependencies>
<build>
<plugins>

<!-- Compiler plugin: tells Maven to use Java 25 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
</configuration>
</plugin>

<!-- JavaFX 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>--enable-native-access=javafx.graphics</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
</configuration>
</plugin>

</plugins>
</build>
</project>
42 changes: 42 additions & 0 deletions src/main/java/com/example/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example;

import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.application.Platform;

public class ChatController {

@FXML
private ListView<String> messagesList;

@FXML
private TextField inputField;

private final ChatModel model = new ChatModel();
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

Prefer dependency injection over direct instantiation for better testability.

The direct instantiation of ChatModel creates tight coupling and makes the controller difficult to unit test.

Consider using constructor injection:

-private final ChatModel model = new ChatModel();
+private final ChatModel model;
+
+public ChatController() {
+    this(new ChatModel());
+}
+
+// Package-private constructor for testing
+ChatController(ChatModel model) {
+    this.model = model;
+}

This allows you to inject a mock ChatModel during testing.

🤖 Prompt for AI Agents
In src/main/java/com/example/ChatController.java around line 15, the controller
directly instantiates ChatModel which creates tight coupling and hinders unit
testing; change to constructor injection by removing the new ChatModel()
instantiation, add a constructor that accepts a ChatModel parameter and assigns
it to the final field, update any framework annotations if needed (e.g.,
@Autowired or leave as plain constructor for manual wiring), and update calling
code/tests to provide a mock or real ChatModel via the constructor.



@FXML
private void onSend() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
messagesList.getItems().add("Me: " + message);
model.sendMessage(message);
inputField.clear();
}
}
Comment on lines +20 to +27
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

Add error handling for model.sendMessage().

The onSend() method should handle potential exceptions from model.sendMessage() to prevent the UI from becoming unresponsive.

Apply this diff to add error handling:

 @FXML
 private void onSend() {
     String message = inputField.getText().trim();
     if (!message.isEmpty()) {
-        messagesList.getItems().add("Me: " + message);
-        model.sendMessage(message);
-        inputField.clear();
+        try {
+            messagesList.getItems().add("Me: " + message);
+            model.sendMessage(message);
+            inputField.clear();
+        } catch (Exception e) {
+            System.err.println("Failed to send message: " + e.getMessage());
+            // TODO: Show error dialog to user
+        }
     }
 }
📝 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 void onSend() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
messagesList.getItems().add("Me: " + message);
model.sendMessage(message);
inputField.clear();
}
}
private void onSend() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
try {
messagesList.getItems().add("Me: " + message);
model.sendMessage(message);
inputField.clear();
} catch (Exception e) {
System.err.println("Failed to send message: " + e.getMessage());
// TODO: Show error dialog to user
}
}
}
🤖 Prompt for AI Agents
In src/main/java/com/example/ChatController.java around lines 18-25, wrap the
call to model.sendMessage(...) with proper error handling and avoid blocking the
JavaFX thread: dispatch model.sendMessage(...) to a background thread (e.g.,
CompletableFuture.runAsync or a Task), catch any thrown exceptions, log them,
and then use Platform.runLater to update the UI (show an error Alert or mark the
message as failed) so the UI remains responsive; ensure inputField clearing and
messagesList updates occur on the JavaFX thread and adjust their order as needed
(e.g., show the sent message immediately, run sendMessage async, and on
exception notify the user).



@FXML
private void initialize(){
model.subscribe(msg -> {
Platform.runLater(() -> messagesList.getItems().add("Friend: " + msg));
});
}






}
117 changes: 117 additions & 0 deletions src/main/java/com/example/ChatModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.example;

import javafx.application.Platform;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ChatModel {

private final String sendUrl;
private final String subscribeUrl;
private final String clientId;
private final Set<String> sentMessages = Collections.newSetFromMap(new ConcurrentHashMap<>());

public ChatModel() {
String topic = System.getenv().getOrDefault("NTFY_TOPIC", "https://ntfy.sh/newchatroom3");

sendUrl = topic;
subscribeUrl = topic + "/sse";
clientId = UUID.randomUUID().toString();
}

public void sendMessage(String message) {
sentMessages.add(message);

new Thread(() -> {
try { Thread.sleep(5000); } catch (Exception ignored) {}
sentMessages.remove(message);
}).start();

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(sendUrl))
.header("Content-Type", "application/json")
.header("X-Client-ID", clientId)
.header("Title", "Friend: ")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();
Comment on lines +44 to +50
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

Fix the Content-Type/body mismatch to unblock sending.

We POST the raw message string but advertise Content-Type: application/json. The ntfy API rejects that combination with a 400/415 response, so nothing actually gets delivered even though the UI shows the message as sent. Align the header with the body (e.g., use text/plain; charset=UTF-8) or wrap the payload in proper JSON before posting.

Apply this diff to fix the header:

-                .header("Content-Type", "application/json")
+                .header("Content-Type", "text/plain; charset=UTF-8")
📝 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
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(sendUrl))
.header("Content-Type", "application/json")
.header("X-Client-ID", clientId)
.header("Title", "Friend: ")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(sendUrl))
.header("Content-Type", "text/plain; charset=UTF-8")
.header("X-Client-ID", clientId)
.header("Title", "Friend: ")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();
🤖 Prompt for AI Agents
In src/main/java/com/example/ChatModel.java around lines 44 to 50, the request
sets Content-Type: application/json while posting a raw message string; change
the Content-Type header to "text/plain; charset=UTF-8" (or alternatively wrap
the message in a proper JSON object) and ensure the request body is sent with
UTF-8 encoding so the ntfy API accepts the payload; update the header value and,
if applicable, use a UTF-8 string publisher or explicit encoding when building
the POST body.


client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> System.out.println("Sent, status=" + response.statusCode()))
.exceptionally(e -> { e.printStackTrace(); return null; });
}


public void subscribe(Consumer<String> onMessageReceived) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(subscribeUrl))
.header("Accept", "text/event-stream")
.build();

client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
.thenAccept(response -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) {
String line;
boolean firstMessageSkipped = false;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data:")) {
if (!firstMessageSkipped) {
firstMessageSkipped = true;
continue;
}

String raw = line.substring(5).trim();
String msg = parseMessage(raw);

if (msg != null && !sentMessages.contains(msg)) {
Platform.runLater(() -> onMessageReceived.accept(msg));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
})
.exceptionally(e -> { e.printStackTrace(); return null; });
}

private String parseMessage(String data) {
try {
Matcher eventMatcher = Pattern.compile("\"event\"\\s*:\\s*\"(.*?)\"").matcher(data);
if (eventMatcher.find()) {
String event = eventMatcher.group(1);

if (!"message".equals(event)) {
return null;
}
}

Matcher msgMatcher = Pattern.compile("\"message\"\\s*:\\s*\"(.*?)\"").matcher(data);
if (msgMatcher.find()) {
return msgMatcher.group(1).replace("\\\"", "\"");
Comment on lines +77 to +105
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

Don't drop legitimate messages that match our recent text.

sentMessages.contains(msg) only compares the text, so any remote user who sends the same text within the five-second eviction window is silently filtered out. Because we already stamp each send with X-Client-ID, the SSE payload contains "client" for every event—compare that to clientId instead. That lets us remove the whole sentMessages bookkeeping and still suppress only our own echoes.

Apply this diff to switch to client-based filtering:

-                                String raw = line.substring(5).trim();
-                                String msg = parseMessage(raw);
-
-                                if (msg != null && !sentMessages.contains(msg)) {
-                                    Platform.runLater(() -> onMessageReceived.accept(msg));
-                                }
+                                String raw = line.substring(5).trim();
+                                MessagePayload payload = parseMessage(raw);
+
+                                if (payload != null && !clientId.equals(payload.client())) {
+                                    Platform.runLater(() -> onMessageReceived.accept(payload.message()));
+                                }

And adjust parseMessage to surface the client id:

-    private String parseMessage(String data) {
+    private MessagePayload parseMessage(String data) {
         try {
             Matcher eventMatcher = Pattern.compile("\"event\"\\s*:\\s*\"(.*?)\"").matcher(data);
             if (eventMatcher.find()) {
                 String event = eventMatcher.group(1);

                 if (!"message".equals(event)) {
                     return null;
                 }
             }

             Matcher msgMatcher = Pattern.compile("\"message\"\\s*:\\s*\"(.*?)\"").matcher(data);
             if (msgMatcher.find()) {
-                return msgMatcher.group(1).replace("\\\"", "\"");
+                String message = msgMatcher.group(1).replace("\\\"", "\"");
+                Matcher clientMatcher = Pattern.compile("\"client\"\\s*:\\s*\"(.*?)\"").matcher(data);
+                String client = clientMatcher.find() ? clientMatcher.group(1) : null;
+                return new MessagePayload(message, client);
             }
         } catch (Exception ignored) {}

         return null;
     }
+
+    private record MessagePayload(String message, String client) {}

Once this is in place you can drop the sentMessages set and its cleanup thread entirely—they are no longer needed.

}
} catch (Exception ignored) {}

return null;
}



}



4 changes: 2 additions & 2 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ public class HelloFX extends Application {

@Override
public void start(Stage stage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("chat-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Hello MVC");
stage.setTitle("Chat App");
stage.setScene(scene);
stage.show();
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
requires java.net.http;


opens com.example to javafx.fxml;
exports com.example;
Expand Down
24 changes: 24 additions & 0 deletions src/main/resources/com/example/chat-view.fxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>

<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.ChatController">

<center>
<ListView fx:id="messagesList"/>
</center>

<bottom>
<HBox spacing="8" style="-fx-padding: 10;">
<TextField fx:id="inputField" HBox.hgrow="ALWAYS"/>
<Button text="Send" onAction="#onSend"/>
</HBox>
</bottom>

</BorderPane>
Loading