Skip to content
Open
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>
</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());

@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace Swedish comments with English.

Code comments should be in English to ensure accessibility for international collaborators and maintainers.

Apply this diff:

-                    // Skapa bubble-label
+                    // Create bubble label
                     Label bubble = new Label(msg.message());
-                    // Använd CSS-klasser för skickat/mottaget
+                    // Use CSS classes for sent/received messages
                     if (model.getUserId().equals(msg.id())) {
-        // Scrolla ner till senaste meddelandet
+        // Scroll down to the latest message
         model.getMessages().addListener((javafx.collections.ListChangeListener<NtfyMessageDto>) change -> {

Also applies to: 104-104, 120-120

🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 94, 104 and 120,
replace the Swedish comments with English equivalents: change the comment on
line 94 from "Skapa bubble-label" to "Create bubble label", and translate the
comments on lines 104 and 120 from Swedish to clear English (preserve intent and
punctuation), updating only the comment text so code behavior is unchanged.

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


// 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
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Changing topic appears to create multiple long‑lived receive streams

HelloModel calls receiveMessage() in the constructor and again inside setCurrentTopic(). Each receiveMessage() delegates to NtfyConnection.receive(...), and in NtfyConnectionImpl that starts a new HttpClient.sendAsync(...BodyHandlers.ofLines()) stream that is never cancelled.

As a result, every topic change can leave the previous receive stream running indefinitely:

  • Extra HTTP connections/resources remain open.
  • You may end up processing messages from multiple topics concurrently (depending on server behavior).
  • There’s no way to shut down or replace the active stream from the model.

It would be safer to ensure only one active receive stream per model, e.g. by:

  • Extending NtfyConnection.receive to return a handle/future that can be cancelled when switching topic, or
  • Adding an explicit “stopReceive”/close method on the connection and calling it before starting a new receive.

Also applies to: 50-56, 95-100

🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 20-24 (and also review
occurrences at 50-56 and 95-100), the class starts a new receive stream each
time the topic is set (constructor + setCurrentTopic) which leaves previous
HttpClient.sendAsync line-based streams running; change the logic so only one
active receive stream exists: have NtfyConnection.receive return a cancellable
handle (e.g., CompletableFuture or Closeable subscription) or add a
stopReceive/close method on NtfyConnection, store the current handle in
HelloModel, and when switching topics cancel/close the existing handle before
calling receive() for the new topic; also remove the redundant receive() call
from the constructor (or ensure it uses the same single-start path) and ensure
you cancel the active handle when HelloModel is closed/destroyed.


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

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