diff --git a/src/main/java/com/example/FxUtils.java b/src/main/java/com/example/FxUtils.java index 770219f0..cc14045a 100644 --- a/src/main/java/com/example/FxUtils.java +++ b/src/main/java/com/example/FxUtils.java @@ -5,7 +5,12 @@ public class FxUtils { /** - * Execute task on FX-thread if possible, otherwise inline. + * Ensures the given task runs on the JavaFX Application Thread when possible. + * + * If already on the JavaFX Application Thread the task is executed immediately; + * if the JavaFX platform is not initialized the task is executed on the current thread. + * + * @param task the work to execute */ static void runOnFx(Runnable task) { try { diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index 046a8f7f..d27a37b8 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -45,6 +45,14 @@ public class HelloController { DateTimeFormatter.ofPattern("HH:mm:ss") .withZone(ZoneId.systemDefault()); + /** + * Initializes the controller by binding UI components to the model, configuring controls, and setting up the message view. + * + *

Sets the initial greeting and topic display, requests focus for the message input, binds the message list + * and the message input to the model, disables send/change-topic buttons when their inputs are empty, configures + * the message list cells to render chat bubbles aligned and styled for sent versus received messages, and adds + * a listener to auto-scroll to the latest message.

+ */ @FXML private void initialize() { messageLabel.setText(model.getGreeting()); @@ -124,6 +132,12 @@ protected void updateItem(NtfyMessageDto msg, boolean empty) { }); } + /** + * Sends the composed message asynchronously and updates the UI according to the send result. + * + * On success, clears the message input and refocuses it. On failure, displays an error alert + * indicating the message could not be sent. + */ @FXML private void sendMessage(ActionEvent actionEvent) { model.sendMessageAsync(success -> { @@ -142,6 +156,11 @@ private void sendMessage(ActionEvent actionEvent) { }); } + /** + * Updates the model's current topic from the topic input field and clears the input. + * + * If the topic input contains non-whitespace text, sets the model's current topic to that value and then clears the topic input field. + */ @FXML private void changeTopic(ActionEvent actionEvent) { String newTopic = topicInput.getText(); diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 6b4a47e7..759d70ad 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -10,6 +10,15 @@ public class HelloFX extends Application { + /** + * Initializes the JavaFX user interface, sets up the scene and stylesheet, and shows the primary stage. + * + * Loads the FXML layout "hello-view.fxml", creates a Scene sized 768×576, sets the window title to + * "RuneChat", applies the "style.css" stylesheet, attaches the scene to the provided stage, and displays it. + * + * @param stage the primary stage provided by the JavaFX runtime to host the application scene + * @throws Exception if the FXML or stylesheet resources cannot be loaded or another initialization error occurs + */ @Override public void start(Stage stage) throws Exception { FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); @@ -26,6 +35,11 @@ public void start(Stage stage) throws Exception { } + /** + * Launches the JavaFX application. + * + * @param args command-line arguments; unused by this application + */ public static void main(String[] args) { launch(); } diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java index 1be38206..e7eeb1b6 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -17,36 +17,80 @@ public class HelloModel { private final StringProperty messageToSend = new SimpleStringProperty(); private final StringProperty currentTopic = new SimpleStringProperty(); + /** + * Create a HelloModel bound to the given NtfyConnection. + * + * Initializes the model's current topic from the connection and begins receiving messages for that topic. + */ public HelloModel(NtfyConnection connection) { this.connection = connection; this.currentTopic.set(connection.getCurrentTopic()); receiveMessage(); } + /** + * Provides the observable list of received messages for UI binding. + * + * @return the live ObservableList of NtfyMessageDto messages. + */ public ObservableList getMessages() { return messages; } + /** + * Gets the current message text intended for sending. + * + * @return the current message text, or {@code null} if no text is set + */ public String getMessageToSend() { return messageToSend.get(); } + /** + * Exposes the property that holds the text currently entered for sending so the UI can bind to it. + * + * @return the `StringProperty` representing the message currently entered for sending + */ public StringProperty messageToSendProperty() { return messageToSend; } + /** + * Update the text currently entered for sending. + * + * @param message the new message text; may be {@code null} to clear the input + */ public void setMessageToSend(String message) { messageToSend.set(message); } + /** + * Retrieve the currently selected topic. + * + * @return the current topic, or {@code null} if no topic is set + */ public String getCurrentTopic() { return currentTopic.get(); } + /** + * Exposes the JavaFX property for the currently selected topic so callers can observe or bind to it. + * + * @return the StringProperty holding the current topic value + */ public StringProperty currentTopicProperty() { return currentTopic; } + /** + * Update the model's current topic and refresh messages when a non-empty topic is provided. + * + *

If the provided topic is non-null and not blank, this updates the connection's current + * topic, updates the model's currentTopic property, clears the message list, and restarts + * receiving messages for the new topic. If the topic is null or blank, no action is taken. + * + * @param topic the new topic to set; ignored if null or blank + */ public void setCurrentTopic(String topic) { if (topic != null && !topic.isBlank()) { connection.setCurrentTopic(topic); @@ -56,19 +100,43 @@ public void setCurrentTopic(String topic) { } } + /** + * Provides the current user's identifier. + * + * @return the current user's identifier + */ public String getUserId() { return connection.getUserId(); } + /** + * Retrieve the application's greeting text. + * + * @return the greeting text "RuneChat" + */ public String getGreeting() { return "RuneChat"; } + /** + * Determine whether the current draft message is eligible to be sent. + * + * @return `true` if the draft message is non-null and contains at least one non-whitespace character, `false` otherwise. + */ public boolean canSendMessage() { String msg = messageToSend.get(); return msg != null && !msg.isBlank(); } + /** + * Sends the current message through the connection and reports the outcome via the provided callback. + * + * If the message is null or blank the callback is invoked with `false` and no send is attempted. On successful send, + * the input is cleared only if it has not changed since sending. The callback is invoked with `true` on success and + * `false` on failure. + * + * @param callback consumer invoked with `true` when the message was sent successfully, `false` otherwise + */ public void sendMessageAsync(Consumer callback) { String msg = messageToSend.get(); if (msg == null || msg.isBlank()) { @@ -92,6 +160,11 @@ public void sendMessageAsync(Consumer callback) { }); } + /** + * Subscribes to the connection's incoming messages and appends each received message with a non-null, non-blank text to the observable messages list on the JavaFX Application Thread. + * + * Messages that are null or whose message text is null or blank are ignored. + */ public void receiveMessage() { connection.receive(m -> { if (m == null || m.message() == null || m.message().isBlank()) return; diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java index 0ed45a88..62b91b30 100644 --- a/src/main/java/com/example/NtfyConnection.java +++ b/src/main/java/com/example/NtfyConnection.java @@ -6,18 +6,48 @@ public interface NtfyConnection { - void send(String message, Consumer callback); - - void receive(Consumer messageHandler); - + /** + * Sends a message over the connection and notifies the caller of the outcome. + * + * @param message the payload to send + * @param callback consumer invoked with `true` if the message was sent successfully, `false` otherwise + */ +void send(String message, Consumer callback); + + /** + * Registers a handler that will be invoked for each incoming message. + * + * @param messageHandler consumer invoked with each received {@link NtfyMessageDto} + */ +void receive(Consumer messageHandler); + + /** + * Retrieves the current topic used by this connection. + * + * @return the current topic string; by default returns "mytopic". + */ default String getCurrentTopic() { return "mytopic"; } + /** + * Sets the connection's current topic. + * + * The default implementation does nothing. + * + * @param topic the topic to set; ignored by the default implementation + */ default void setCurrentTopic(String topic) { } + /** + * Get the identifier of the current user. + * + * Default implementation returns "unknown". + * + * @return the user identifier; "unknown" when not specified + */ default String getUserId() { return "unknown"; } diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java index 03ed1bc6..c74a05dd 100644 --- a/src/main/java/com/example/NtfyConnectionImpl.java +++ b/src/main/java/com/example/NtfyConnectionImpl.java @@ -18,6 +18,14 @@ public class NtfyConnectionImpl implements NtfyConnection { private String currentTopic; private final ObjectMapper mapper = new ObjectMapper(); + /** + * Creates a connection configured from environment variables. + * + * Reads `HOST_NAME` and `USER_ID` from the environment and sets the connection's topic + * to the value of `DEFAULT_TOPIC` if present, or `"mytopic"` otherwise. + * + * @throws NullPointerException if `HOST_NAME` or `USER_ID` is not set in the environment + */ public NtfyConnectionImpl() { Dotenv dotenv = Dotenv.load(); this.hostName = Objects.requireNonNull(dotenv.get("HOST_NAME")); @@ -25,30 +33,65 @@ public NtfyConnectionImpl() { this.currentTopic = dotenv.get("DEFAULT_TOPIC", "mytopic"); } + /** + * Initialize a connection targeting the specified host, using default credentials and topic. + * + *

The instance will use userId "testuser" and topic "mytopic" unless changed. + * + * @param hostName the base URL of the ntfy host to connect to + */ public NtfyConnectionImpl(String hostName) { this.hostName = hostName; this.userId = "testuser"; this.currentTopic = "mytopic"; } + /** + * Constructs a NtfyConnectionImpl configured with the specified host, user, and topic. + * + * @param hostName the base URL of the ntfy host to connect to + * @param userId the user identifier to include with requests + * @param topic the initial topic name to use for sending and receiving messages + */ public NtfyConnectionImpl(String hostName, String userId, String topic) { this.hostName = hostName; this.userId = userId; this.currentTopic = topic; } + /** + * User identifier used in outgoing requests. + * + * @return the configured user identifier + */ public String getUserId() { return userId; } + /** + * Gets the current topic used for sending and receiving messages. + * + * @return the name of the current topic + */ public String getCurrentTopic() { return currentTopic; } + /** + * Updates the topic used for subsequent send and receive operations. + * + * @param topic the new topic name to set + */ public void setCurrentTopic(String topic) { this.currentTopic = topic; } + /** + * Sends the given message to the current topic on the configured host and delivers whether the send succeeded to the callback. + * + * @param message the message body to post to the current topic + * @param callback consumer invoked with `true` if the HTTP response status code is in the 2xx range, `false` otherwise + */ @Override public void send(String message, Consumer callback) { HttpRequest httpRequest = HttpRequest.newBuilder() @@ -67,6 +110,13 @@ public void send(String message, Consumer callback) { .thenAccept(callback); } + /** + * Subscribes to the current topic and delivers each successfully parsed message to the provided handler. + * + *

Lines received from the topic are parsed as JSON into {@code NtfyMessageDto}; messages that fail parsing are skipped.

+ * + * @param messageHandler consumer invoked for each parsed {@code NtfyMessageDto} received from the topic + */ @Override public void receive(Consumer messageHandler) { HttpRequest httpRequest = HttpRequest.newBuilder() diff --git a/src/test/java/com/example/NtfyConnectionSpy.java b/src/test/java/com/example/NtfyConnectionSpy.java index a12edf7c..8fb8999a 100644 --- a/src/test/java/com/example/NtfyConnectionSpy.java +++ b/src/test/java/com/example/NtfyConnectionSpy.java @@ -7,6 +7,14 @@ public class NtfyConnectionSpy implements NtfyConnection{ String message; Consumer handler; + /** + * Records the provided message and asynchronously signals success to the callback. + * + *

Stores the message in the spy's state and invokes the callback with {@code true} on a separate thread. + * + * @param message the message to record + * @param callback callback invoked with {@code true} to indicate simulated send success + */ @Override public void send(String message, Consumer callback) { this.message = message; @@ -14,11 +22,23 @@ public void send(String message, Consumer callback) { } + /** + * Registers a handler to receive incoming Ntfy messages. + * + * The provided consumer is stored and will be invoked by simulateIncoming when a message is simulated. + * + * @param messageHandler the consumer to call with incoming {@code NtfyMessageDto} instances; may be {@code null} to unregister the handler + */ @Override public void receive(Consumer messageHandler) { this.handler = messageHandler; } + /** + * Simulates arrival of a message by delivering the given message to the registered handler if present. + * + * @param msg the incoming message to deliver to the registered message handler; ignored if no handler is registered + */ public void simulateIncoming(NtfyMessageDto msg) { if (handler != null) handler.accept(msg); }