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
7 changes: 6 additions & 1 deletion src/main/java/com/example/FxUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*/
@FXML
private void initialize() {
messageLabel.setText(model.getGreeting());
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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();
}
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<NtfyMessageDto> 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.
*
* <p>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);
Expand All @@ -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<Boolean> callback) {
String msg = messageToSend.get();
if (msg == null || msg.isBlank()) {
Expand All @@ -92,6 +160,11 @@ public void sendMessageAsync(Consumer<Boolean> 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;
Expand Down
38 changes: 34 additions & 4 deletions src/main/java/com/example/NtfyConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,48 @@

public interface NtfyConnection {

void send(String message, Consumer<Boolean> callback);

void receive(Consumer<NtfyMessageDto> 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<Boolean> callback);

/**
* Registers a handler that will be invoked for each incoming message.
*
* @param messageHandler consumer invoked with each received {@link NtfyMessageDto}
*/
void receive(Consumer<NtfyMessageDto> 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";
}
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/com/example/NtfyConnectionImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,80 @@ 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"));
this.userId = Objects.requireNonNull(dotenv.get("USER_ID"), "USER_ID");
this.currentTopic = dotenv.get("DEFAULT_TOPIC", "mytopic");
}

/**
* Initialize a connection targeting the specified host, using default credentials and topic.
*
* <p>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<Boolean> callback) {
HttpRequest httpRequest = HttpRequest.newBuilder()
Expand All @@ -67,6 +110,13 @@ public void send(String message, Consumer<Boolean> callback) {
.thenAccept(callback);
}

/**
* Subscribes to the current topic and delivers each successfully parsed message to the provided handler.
*
* <p>Lines received from the topic are parsed as JSON into {@code NtfyMessageDto}; messages that fail parsing are skipped.</p>
*
* @param messageHandler consumer invoked for each parsed {@code NtfyMessageDto} received from the topic
*/
@Override
public void receive(Consumer<NtfyMessageDto> messageHandler) {
HttpRequest httpRequest = HttpRequest.newBuilder()
Expand Down
Loading