From de4595febcb3ce64eed3e40575c5c110a282efdb Mon Sep 17 00:00:00 2001 From: Firas Moussa <149446898+fmazmz@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:06:12 +0100 Subject: [PATCH] Revert "Initial implementation of JavaFX Chat App with ntfy integration" --- .env.example | 2 - .gitignore | 1 - README.md | 16 +- pom.xml | 35 --- .../java/com/example/HelloController.java | 224 +----------------- src/main/java/com/example/HelloFX.java | 44 +--- .../com/example/client/ChatNetworkClient.java | 17 -- .../example/client/HttpClientProvider.java | 15 -- .../com/example/client/NtfyHttpClient.java | 131 ---------- .../java/com/example/domain/ChatModel.java | 36 --- .../com/example/domain/NtfyEventResponse.java | 26 -- .../java/com/example/domain/NtfyMessage.java | 72 ------ .../java/com/example/utils/EnvLoader.java | 27 --- src/main/java/module-info.java | 9 - .../resources/com/example/hello-view.fxml | 50 +--- src/main/resources/com/example/styles.css | 86 ------- src/test/java/com/example/ChatModelTest.java | 43 ---- .../java/com/example/TestFxInitializer.java | 21 -- 18 files changed, 23 insertions(+), 832 deletions(-) delete mode 100644 .env.example delete mode 100644 src/main/java/com/example/client/ChatNetworkClient.java delete mode 100644 src/main/java/com/example/client/HttpClientProvider.java delete mode 100644 src/main/java/com/example/client/NtfyHttpClient.java delete mode 100644 src/main/java/com/example/domain/ChatModel.java delete mode 100644 src/main/java/com/example/domain/NtfyEventResponse.java delete mode 100644 src/main/java/com/example/domain/NtfyMessage.java delete mode 100644 src/main/java/com/example/utils/EnvLoader.java delete mode 100644 src/main/resources/com/example/styles.css delete mode 100644 src/test/java/com/example/ChatModelTest.java delete mode 100644 src/test/java/com/example/TestFxInitializer.java diff --git a/.env.example b/.env.example deleted file mode 100644 index 1ba9c1af..00000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -NTFY_BASE_URL=https://ntfy.sh -NTFY_TOPIC= //Add your topic here \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5a54815d..6ac465db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ target/ /.idea/ -.env \ No newline at end of file diff --git a/README.md b/README.md index 6df13fda..5fdc622f 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,8 @@ A JavaFX-based chat client using [ntfy](https://docs.ntfy.sh/) for backend messa - Unit tests for `Model` class - (Advanced) Send files via "Attach local file" option - -## Requirements - -- **Java** - - **Version**: `25` - -- **Maven Compiler Plugin** - - **Version**: `3.11.0` - - **Configuration**: - - **Release**: `25` - -## Usage -1. Set `JAVA_HOME` to JDK 25. -2. Create a **.env** file with the required variables. You can also clone and fill **.env.example** and rename it to `.env`. +## 🚀 Run Instructions +1. Set `JAVA_HOME` to JDK 25 2. Start with: ```bash ./mvnw clean javafx:run diff --git a/pom.xml b/pom.xml index 177d3c50..c40f667e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,21 +17,6 @@ 25 - - org.slf4j - slf4j-api - 2.0.9 - - - org.slf4j - slf4j-simple - 2.0.9 - - - com.fasterxml.jackson.core - jackson-databind - 2.17.2 - org.junit.jupiter junit-jupiter @@ -60,18 +45,6 @@ javafx-fxml ${javafx.version} - - org.testfx - testfx-junit5 - 4.0.17 - test - - - org.openjfx - javafx-swing - ${javafx.version} - test - @@ -90,14 +63,6 @@ true - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 25 - - diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index 7474c4ff..fdd160a0 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,230 +1,22 @@ package com.example; -import com.example.client.ChatNetworkClient; -import com.example.domain.ChatModel; -import com.example.domain.NtfyEventResponse; -import com.example.domain.NtfyMessage; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.*; import javafx.scene.control.Label; -import javafx.scene.control.TextField; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.time.Instant; -import java.time.LocalTime; -import java.time.ZoneId; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +/** + * Controller layer: mediates between the view (FXML) and the model. + */ public class HelloController { - private static final Logger log = LoggerFactory.getLogger(HelloController.class); - private ChatNetworkClient client; - private String baseUrl; - private String topic; - private File selectedFile = null; - - @FXML - private Label messageLabel; - - @FXML - private ListView messagesList; - - @FXML - private TextField messageInput; - - @FXML - private TextField titleInput; - - @FXML - private TextField tagsInput; - - public void setClient(ChatNetworkClient client, String baseUrl, String topic) { - this.client = client; - this.baseUrl = baseUrl; - this.topic = topic; - } - - public void setModel(ChatModel model) { - messagesList.setItems(model.getMessages()); - messagesList.setCellFactory(list -> new MessageCell()); - } - - private static String formatTime(long epochSeconds) { - Instant instant = Instant.ofEpochSecond(epochSeconds); - LocalTime time = LocalTime.ofInstant(instant, ZoneId.systemDefault()); - return time.toString(); - } - - private void showStatus(String text) { - messageLabel.setText(text); - Timeline t = new Timeline(new KeyFrame(javafx.util.Duration.seconds(3), - ev -> messageLabel.setText(""))); - t.setCycleCount(1); - t.play(); - } + private final HelloModel model = new HelloModel(); @FXML - private void onPickAttachment() { - FileChooser chooser = new FileChooser(); - chooser.setTitle("Select attachment"); - File file = chooser.showOpenDialog(messageInput.getScene().getWindow()); - - if (file != null) { - selectedFile = file; - messageLabel.setText("Attachment selected: " + file.getName()); - } - } + private Label messageLabel; @FXML - private void onSend() { - String txt = messageInput.getText(); - - if ((txt == null || txt.isBlank()) && selectedFile == null) { - showStatus("Nothing to send"); - return; - } - - String title = titleInput.getText(); - if (title != null && title.isBlank()) title = null; - - String tagsRaw = tagsInput.getText(); - List tags = null; - - if (tagsRaw != null && !tagsRaw.isBlank()) { - tags = java.util.Arrays.stream(tagsRaw.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .toList(); - } - - NtfyMessage msg = new NtfyMessage.Builder() - .id(UUID.randomUUID().toString()) - .time(System.currentTimeMillis()) - .event("message") - .topic(topic) - .message(txt) - .title(title) - .tags(tags) - .attach(null) - .filename(null) - .build(); - - try { - client.send(baseUrl, msg, selectedFile); - showStatus(selectedFile == null ? "Message sent" : "Attachment sent"); - } catch (InterruptedException | IOException e) { - showStatus("Error sending: " + e.getMessage()); - } - - messageInput.clear(); - titleInput.clear(); - tagsInput.clear(); - selectedFile = null; - } - - - private static final class MessageCell extends ListCell { - @Override - protected void updateItem(NtfyEventResponse msg, boolean empty) { - super.updateItem(msg, empty); - - if (empty || msg == null) { - setText(null); - setGraphic(null); - return; - } - - setText(null); - - VBox container = new VBox(); - container.setSpacing(6); - container.getStyleClass().add("message-bubble"); - - container.setStyle("-fx-alignment: CENTER_LEFT;"); - if (msg.title() != null) { - Label titleLabel = new Label(msg.title()); - titleLabel.getStyleClass().add("message-title"); - container.getChildren().add(titleLabel); - } - - if (msg.attachment() != null) { - NtfyEventResponse.Attachment att = msg.attachment(); - - if (att.type() != null && att.type().startsWith("image")) { - Image image = new Image(att.url(), 300, 0, true, true); - ImageView imageView = new ImageView(image); - container.getChildren().add(imageView); - } else { - Label fileLabel = getFileLabel(att); - container.getChildren().add(fileLabel); - } - } - - if (msg.message() != null && !msg.message().isBlank()) { - Label messageLabel = new Label(msg.message()); - messageLabel.setWrapText(true); - messageLabel.getStyleClass().add("message-text"); - container.getChildren().add(messageLabel); - } - - if (msg.tags() != null && !msg.tags().isEmpty()) { - Label tagsLabel = new Label(String.join(", ", msg.tags())); - tagsLabel.getStyleClass().add("message-tags"); - container.getChildren().add(tagsLabel); - } - - if (msg.time() != null) { - Label timeLabel = new Label(formatTime(msg.time())); - timeLabel.getStyleClass().add("message-time"); - container.getChildren().add(timeLabel); - } - - setGraphic(container); - } - - // Helper method to allow user to open file - private static Label getFileLabel(NtfyEventResponse.Attachment att) { - Label fileLabel = new Label("Open file: " + (att.name() != null ? att.name() : att.url())); - fileLabel.setStyle("-fx-text-fill: #2c75ff; -fx-underline: true;"); - fileLabel.setOnMouseClicked(ev -> { - try { - String url = att.url(); - - // method that works on linux as Desktop is not always supported and crashes application - if (System.getProperty("os.name").toLowerCase().contains("linux")) { - try { - new ProcessBuilder("xdg-open", url).start(); - return; - }catch (IOException e) { - log.error("Error opening file: {}", url, e); - } - } - - if (Desktop.isDesktopSupported()) { - Desktop.getDesktop().browse(new URI(url)); - } - - } catch (IOException | URISyntaxException ex) { - log.error("Failed to open attachment: {}", ex.getMessage()); - log.error(Arrays.toString(ex.getStackTrace())); - } - }); - return fileLabel; + private void initialize() { + if (messageLabel != null) { + messageLabel.setText(model.getGreeting()); } } } diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 6d9a8345..96bdc5ca 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,59 +1,25 @@ package com.example; -import com.example.client.ChatNetworkClient; -import com.example.client.NtfyHttpClient; -import com.example.domain.ChatModel; -import com.example.domain.NtfyMessage; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Objects; -import java.util.Properties; - -import static com.example.utils.EnvLoader.loadEnv; public class HelloFX extends Application { - private static final Logger log = LoggerFactory.getLogger("MAIN"); - static final ChatModel model = new ChatModel(); @Override public void start(Stage stage) throws Exception { - Properties env = loadEnv(); - String baseUrl = env.getProperty("NTFY_BASE_URL", "https://ntfy.sh"); - String topic = env.getProperty("NTFY_TOPIC"); - - if (topic == null || topic.isBlank()) { - throw new IllegalStateException("NTFY_TOPIC is not set"); - } - - FXMLLoader loader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); - Parent root = loader.load(); - - HelloController controller = loader.getController(); - controller.setModel(model); - - ChatNetworkClient client = new NtfyHttpClient(model); - controller.setClient(client, baseUrl, topic); - - client.subscribe(baseUrl, topic); - - Scene scene = new Scene(root); - scene.getStylesheets().add( - Objects.requireNonNull(HelloFX.class.getResource("styles.css")).toExternalForm() - ); - + 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"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { - launch(args); - + launch(); } } \ No newline at end of file diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java deleted file mode 100644 index 815418e4..00000000 --- a/src/main/java/com/example/client/ChatNetworkClient.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.client; - - -import com.example.domain.NtfyMessage; - -import java.io.File; -import java.io.IOException; - -public interface ChatNetworkClient { - Subscription subscribe(String baseUrl, String topic); - void send(String baseUrl, NtfyMessage message, File file) throws IOException, InterruptedException; - interface Subscription extends AutoCloseable { - @Override - void close(); - boolean isOpen(); - } -} diff --git a/src/main/java/com/example/client/HttpClientProvider.java b/src/main/java/com/example/client/HttpClientProvider.java deleted file mode 100644 index a0e86184..00000000 --- a/src/main/java/com/example/client/HttpClientProvider.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.client; - -import java.net.http.HttpClient; - -public final class HttpClientProvider { - - private static final HttpClient INSTANCE = HttpClient.newHttpClient(); - - private HttpClientProvider() { - } - - public static HttpClient get() { - return INSTANCE; - } -} diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java deleted file mode 100644 index e0cb8f5f..00000000 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.example.client; - -import com.example.domain.ChatModel; -import com.example.domain.NtfyEventResponse; -import com.example.domain.NtfyMessage; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; - -public record NtfyHttpClient(ChatModel model) implements ChatNetworkClient { - - private static final ObjectMapper mapper = new ObjectMapper(); - private static final Logger log = LoggerFactory.getLogger("NtfyClient"); - - @Override - public Subscription subscribe(String baseUrl, String topic) { - - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create(baseUrl).resolve(topic + "/json")) - .header("accept", "application/json") - .GET() - .build(); - - CompletableFuture>> future = - HttpClientProvider.get().sendAsync(req, HttpResponse.BodyHandlers.ofLines()); - - AtomicBoolean open = new AtomicBoolean(true); - - future.thenAccept(response -> { - response.body().forEach(line -> { - - log.debug("Raw event received: {}", line); - - if (!open.get()) return; - - try { - NtfyEventResponse msg = mapper.readValue(line, NtfyEventResponse.class); - if (msg.event().equals("message")) { - model.addMessage(msg); - log.info("Message added: {}", msg); - } - } catch (JsonProcessingException e) { - log.error("Error parsing event: {}", line, e); - } - }); - }).exceptionally(ex -> { - log.error("Error while subscribing to topic {}", topic, ex); - open.set(false); - return null; - }); - log.info("Subscribing to topic: {}", topic); - - return new Subscription() { - @Override - public void close() { - open.set(false); - future.cancel(true); - } - - @Override - public boolean isOpen() { - return open.get(); - } - }; - } - - @Override - public void send(String baseUrl, NtfyMessage msg, File attachment) throws IOException, InterruptedException { - - if (attachment != null) { - sendWithAttachment(baseUrl, msg, attachment); - return; - } - - sendJsonOnly(baseUrl, msg); - } - - private void sendJsonOnly(String baseUrl, NtfyMessage msg) throws IOException, InterruptedException { - String json = mapper.writeValueAsString(msg); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(baseUrl)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(json)) - .build(); - - var response = HttpClientProvider.get().send(request, HttpResponse.BodyHandlers.ofString()); - int statusCode = response.statusCode(); - - if (statusCode >= 200 && statusCode < 300) { - log.debug("Sent message payload: {}", json); - log.info("Message sent"); - } - log.error("Failed to send message payload: {}", json); - throw new IOException("Failed to send message payload: " + statusCode); - } - - private void sendWithAttachment(String baseUrl, NtfyMessage msg, File file) - throws IOException, InterruptedException { - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(baseUrl).resolve((msg.topic()))) - .header("Filename", file.getName()) - .PUT(HttpRequest.BodyPublishers.ofFile(file.toPath())) - .build(); - - - var response = HttpClientProvider.get().send(request, HttpResponse.BodyHandlers.ofString()); - - int statusCode = response.statusCode(); - if (200 <= statusCode && statusCode < 300) { - log.debug("Attachment sent: {}", statusCode); - log.info("status: {}", statusCode); - } - log.error("Failed to send attachment: {}", statusCode); - throw new IOException("Failed to send attachment: " + statusCode); - - } - - -} diff --git a/src/main/java/com/example/domain/ChatModel.java b/src/main/java/com/example/domain/ChatModel.java deleted file mode 100644 index 3524a777..00000000 --- a/src/main/java/com/example/domain/ChatModel.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.domain; - -import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - - -public class ChatModel { - private final ObservableList messages = FXCollections.observableArrayList(); - - - public void addMessage(NtfyEventResponse msg) { - runOnFx(() -> messages.add(msg)); - } - - public ObservableList getMessages() { - return messages; - } - - private static void runOnFx(Runnable task) { - try { - if (Platform.isFxApplicationThread()) { - task.run(); - } else if (!Platform.isImplicitExit()) { - Platform.runLater(task); - } else { - // execute test case immediately - task.run(); - } - } catch (IllegalStateException notInitialized) { - task.run(); - } - } - - -} diff --git a/src/main/java/com/example/domain/NtfyEventResponse.java b/src/main/java/com/example/domain/NtfyEventResponse.java deleted file mode 100644 index 603f2a8d..00000000 --- a/src/main/java/com/example/domain/NtfyEventResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.domain; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NtfyEventResponse( - String id, - Long time, - String event, - String topic, - String message, - String title, - List tags, - Attachment attachment -) { - @JsonIgnoreProperties(ignoreUnknown = true) - public record Attachment( - String name, - String type, - Long size, - Long expires, - String url - ) {} -} \ No newline at end of file diff --git a/src/main/java/com/example/domain/NtfyMessage.java b/src/main/java/com/example/domain/NtfyMessage.java deleted file mode 100644 index d838d5fc..00000000 --- a/src/main/java/com/example/domain/NtfyMessage.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.domain; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NtfyMessage( - String id, - Long time, - String event, - String topic, - String message, - String title, - List tags, - String attach, - String filename -) { - public static class Builder { - private String id; - private Long time; - private String event; - private String topic; - private String message; - private String title; - private List tags; - private String attach; - private String filename; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder time(long time) { - this.time = time; - return this; - } - - public Builder event(String event) { - this.event = event; - return this; - } - - public Builder topic(String topic) { - this.topic = topic; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public Builder title(String title) { - this.title = title; - return this; - } - - public Builder tags(List tags) { - this.tags = tags; - return this; - } - - public Builder attach(String attach) { this.attach = attach; return this; } - public Builder filename(String filename) { this.filename = filename; return this; } - - public NtfyMessage build() { - return new NtfyMessage(id, time, event, topic, message, title, tags, attach, filename); - } - } -} diff --git a/src/main/java/com/example/utils/EnvLoader.java b/src/main/java/com/example/utils/EnvLoader.java deleted file mode 100644 index 73ad87be..00000000 --- a/src/main/java/com/example/utils/EnvLoader.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.utils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Properties; - -public class EnvLoader { - private static final Logger log = LoggerFactory.getLogger(EnvLoader.class); - - public static Properties loadEnv() { - Properties props = new Properties(); - - try (FileInputStream fis = new FileInputStream(".env")) { - props.load(fis); - } catch (FileNotFoundException e) { - log.error("Could not load .env file", e); - } catch (IOException e) { - log.error("Failed to load env file: ", e); - } - - return props; - } -} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0c455103..71574a27 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,16 +1,7 @@ module hellofx { requires javafx.controls; requires javafx.fxml; - requires java.net.http; - requires com.fasterxml.jackson.annotation; - requires com.fasterxml.jackson.databind; - requires java.logging; - requires org.slf4j; - requires java.desktop; opens com.example to javafx.fxml; - opens com.example.domain to com.fasterxml.jackson.databind; exports com.example; - exports com.example.domain; - exports com.example.client; } \ No newline at end of file diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml index b080e6d3..20a7dc82 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,43 +1,9 @@ - - - - - - - -