diff --git a/.gitignore b/.gitignore
index 6ac465db..5de38872 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
target/
/.idea/
+.env
+package-lock.json
diff --git a/pom.xml b/pom.xml
index c40f667e..1d04e97f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,22 @@
javafx-fxml
${javafx.version}
+
+ tools.jackson.core
+ jackson-databind
+ 3.0.1
+
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+ test
+
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..091af372 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,86 @@
package com.example;
-
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
-import javafx.scene.control.Label;
+import javafx.geometry.Insets;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
/**
* Controller layer: mediates between the view (FXML) and the model.
*/
public class HelloController {
- private final HelloModel model = new HelloModel();
+ private final HelloModel model = new HelloModel(new NtfyConnectionImpl());
+ public ListView messageView;
@FXML
private Label messageLabel;
+ @FXML
+ private TextField messageField;
+
@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
}
+ messageView.setItems(model.getMessages());
+
+ messageView.setCellFactory(list -> new ListCell<>() {
+ private final Label messageLabel = new Label();
+ private final HBox bubble = new HBox(messageLabel);
+ {
+ bubble.setPadding(new Insets(5, 10, 5, 10));
+ bubble.setMaxWidth(200);
+ messageLabel.setWrapText(true);
+ bubble.getStyleClass().add("chat-bubble");
+ }
+
+ @Override
+ protected void updateItem(NtfyMessageDto item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setGraphic(null);
+ } else {
+ // Format tid + text
+ java.time.LocalTime time = java.time.Instant.ofEpochSecond(item.time())
+ .atZone(java.time.ZoneId.systemDefault())
+ .toLocalTime();
+ String formattedTime = time.format(java.time.format.DateTimeFormatter.ofPattern("HH:mm"));
+
+ messageLabel.setText(formattedTime + "\n" + item.message());
+ setGraphic(bubble);
+ }
+ }
+ });
+
+ model.messageToSendProperty().bind(messageField.textProperty());
+
+ }
+
+ public void sendMessage(ActionEvent actionEvent) {
+ String message = messageField.getText();
+ if(message == null || message.isBlank()){
+ showTemporaryAlert("You must write something before sending!");
+ return;
+ }
+
+ model.sendMessage();
+ messageField.clear();
+ }
+
+ private void showTemporaryAlert(String alertMessage) {
+ Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setHeaderText(null);
+ alert.setContentText(alertMessage);
+ alert.initOwner(messageField.getScene().getWindow());
+
+ alert.show();
+
+ new Thread(() -> {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {}
+ }).start();
}
}
diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java
index 96bdc5ca..b02e6ea2 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -1,5 +1,4 @@
package com.example;
-
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
@@ -13,7 +12,8 @@ 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.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
+ stage.setTitle("Chatt Client");
stage.setScene(scene);
stage.show();
}
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..e5328798 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,55 @@
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;
+
/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {
+
+ private final NtfyConnection connection;
+
+ private final ObservableList messages = FXCollections.observableArrayList();
+ private final StringProperty messageToSend = new SimpleStringProperty();
+
+ public HelloModel(NtfyConnection connection) {
+ this.connection = connection;
+ receiveMessage();
+ }
+
+ public ObservableList getMessages() {
+ return messages;
+ }
+
+ public String getMessageToSend() {
+ return messageToSend.get();
+ }
+
+ public StringProperty messageToSendProperty() {
+ return messageToSend;
+ }
+
+ public void setMessageToSend(String message) {
+ messageToSend.set(message);
+ }
+
/**
* Returns a greeting based on the current Java and JavaFX versions.
*/
public String getGreeting() {
- String javaVersion = System.getProperty("java.version");
- String javafxVersion = System.getProperty("javafx.version");
- return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
+ return "Chat Client by Adam";
+ }
+
+ public void sendMessage() {
+ connection.send(messageToSend.get());
+
+ }
+
+ public void receiveMessage() {
+ connection.receive(m->Platform.runLater(()->messages.add(m)));
}
}
diff --git a/src/main/java/com/example/ManyParameters.java b/src/main/java/com/example/ManyParameters.java
new file mode 100644
index 00000000..0610fba7
--- /dev/null
+++ b/src/main/java/com/example/ManyParameters.java
@@ -0,0 +1,17 @@
+package com.example;
+
+public class ManyParameters {
+
+ public ManyParameters(String computerName, int timeout, String method, int size, byte[] data){
+
+ }
+
+ static void main(){
+ ManyParametersBuilder builder = new ManyParametersBuilder();
+ builder
+ .setComputerName("localhost") // Fluent API
+ .setTimeout(10)
+ .setSize(0)
+ .createManyParameters();
+ }
+}
diff --git a/src/main/java/com/example/ManyParametersBuilder.java b/src/main/java/com/example/ManyParametersBuilder.java
new file mode 100644
index 00000000..fd49920d
--- /dev/null
+++ b/src/main/java/com/example/ManyParametersBuilder.java
@@ -0,0 +1,38 @@
+package com.example;
+
+public class ManyParametersBuilder {
+ private String computerName;
+ private int timeout = 0;
+ private String method;
+ private int size = 0;
+ private byte[] data = null;
+
+ public ManyParametersBuilder setComputerName(String computerName) {
+ this.computerName = computerName;
+ return this;
+ }
+
+ public ManyParametersBuilder setTimeout(int timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ public ManyParametersBuilder setMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ public ManyParametersBuilder setSize(int size) {
+ this.size = size;
+ return this;
+ }
+
+ public ManyParametersBuilder setData(byte[] data) {
+ this.data = data;
+ return this;
+ }
+
+ public ManyParameters createManyParameters() {
+ return new ManyParameters(computerName, timeout, method, size, data);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java
new file mode 100644
index 00000000..c239f3ab
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,9 @@
+package com.example;
+
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+ public boolean send(String message);
+
+ public void receive(Consumer messageHandler);
+}
diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java
new file mode 100644
index 00000000..b89635d5
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,62 @@
+package com.example;
+import io.github.cdimascio.dotenv.Dotenv;
+import tools.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+public class NtfyConnectionImpl implements NtfyConnection {
+
+ private final HttpClient http = HttpClient.newHttpClient();
+ private final String hostName;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ NtfyConnectionImpl(){
+ Dotenv dotenv = Dotenv.load();
+ hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
+ }
+
+ public NtfyConnectionImpl(String hostName){
+ this.hostName = hostName;
+ }
+
+ @Override
+ public boolean send(String message) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+ try {
+ // TODO: handle long blocking send requests to not freeze the JavaFX thread
+ // 1. Use thread send message?
+ // 2. Use async?
+ var response = http.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+ return true;
+ } catch (IOException e) {
+ System.out.println("Error sending message");
+ } catch (InterruptedException e) {
+ System.out.println("Interrupted sending message");
+ }
+ return false;
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/mytopic/json?since=wBuD2KGEaAe0"))
+ .build();
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> response.body()
+ .map(s->
+ mapper.readValue(s, NtfyMessageDto.class))
+ .filter(message -> message.event().equals("message"))
+ .peek(System.out::println)
+ .forEach(messageHandler));
+ }
+}
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..ba65d213
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,6 @@
+package com.example;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record NtfyMessageDto(String id, long time, String event, String topic, String message) {}
diff --git a/src/main/java/com/example/Singelton.java b/src/main/java/com/example/Singelton.java
new file mode 100644
index 00000000..3addf1e0
--- /dev/null
+++ b/src/main/java/com/example/Singelton.java
@@ -0,0 +1,13 @@
+package com.example;
+
+public class Singelton {
+ private final static Singelton instance = new Singelton();
+
+ private Singelton() {
+
+ }
+
+ public static Singelton getInstance() {
+ return instance;
+ }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 71574a27..89e041cb 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,10 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
+ requires io.github.cdimascio.dotenv.java;
+ requires java.net.http;
+ requires tools.jackson.databind;
+ requires javafx.graphics;
opens com.example to javafx.fxml;
exports com.example;
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..5a268db5 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,22 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/example/style.css b/src/main/resources/com/example/style.css
new file mode 100644
index 00000000..31e1af9d
--- /dev/null
+++ b/src/main/resources/com/example/style.css
@@ -0,0 +1,43 @@
+.list-view {
+ -fx-control-inner-background: white;
+ -fx-control-inner-background-alt: white;
+ -fx-background-color: white;
+ -fx-selection-bar: transparent;
+ -fx-selection-bar-non-focused: transparent;
+ -fx-border-color: transparent;
+}
+.list-cell {
+ -fx-background-color: #060312;
+ -fx-padding: 5 10 5 10;
+}
+.chat-bubble {
+ -fx-background-color: linear-gradient(to bottom right, #a922b5, #3d11ed);
+ -fx-background-radius: 15;
+ -fx-padding: 10;
+ -fx-text-fill: white;
+}
+.chat-bubble .label {
+ -fx-text-fill: white;
+}
+
+.button {
+ -fx-background-color: linear-gradient(to bottom right, #6366f1, #4f46e5);
+ -fx-background-radius: 5;
+ -fx-text-fill: white;
+ -fx-font-size: 12px;
+ -fx-font-weight: bold;
+ -fx-padding: 8 24 8 24;
+ -fx-cursor: hand;/
+ -fx-border-color: transparent;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.25), 8, 0, 0, 3);
+ -fx-transition: all 0.3s ease;
+}
+
+.button:hover {
+ -fx-background-color: linear-gradient(to bottom right, #7c3aed, #4338ca);
+}
+
+.button:pressed {
+ -fx-background-color: linear-gradient(to bottom right, #4338ca, #312e81);
+ -fx-translate-y: 2;
+}
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..aaf7a8aa
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,45 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WireMockTest
+class HelloModelTest {
+
+ @Test
+ @DisplayName("Given a model with messageToSend when calling sendMessage then send method on connection should be called ")
+ void sendMessageCallsConnectionWithMessageToSend() {
+
+ // Arrange Given
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+ model.setMessageToSend("Hello World");
+
+ // Act When
+ model.sendMessage();
+
+ // Assert Then
+ assertThat(spy.message).isEqualTo("Hello World");
+ }
+
+ @Test
+ void sendMessageToFakeServer(WireMockRuntimeInfo wmRuntimeInfo) {
+ var con = new NtfyConnectionImpl("http://localhost:" + wmRuntimeInfo.getHttpPort());
+ var model = new HelloModel(con);
+ model.setMessageToSend("Hello World");
+ stubFor(post("/mytopic").willReturn(ok()));
+
+ model.sendMessage();
+
+ // Verify call made to server
+ verify(postRequestedFor(urlEqualTo("/mytopic"))
+ .withRequestBody(matching("Hello World")));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/NtfyConnectionSpy.java b/src/test/java/com/example/NtfyConnectionSpy.java
new file mode 100644
index 00000000..eac8d7f0
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,18 @@
+package com.example;
+
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+
+ String message;
+ @Override
+ public boolean send(String message) {
+ this.message = message;
+ return true;
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+
+ }
+}