Skip to content

Commit 7ee18ed

Browse files
authored
Merge pull request #34 from ithsjava25/dev
Implementation of JavaFX Chat App with ntfy integration: ## 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`. 2. Start with: ```bash ./mvnw clean javafx:run
2 parents daa1037 + c5d06b7 commit 7ee18ed

File tree

18 files changed

+832
-23
lines changed

18 files changed

+832
-23
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NTFY_BASE_URL=https://ntfy.sh
2+
NTFY_TOPIC= //Add your topic here

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
target/
22
/.idea/
3+
.env

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,20 @@ A JavaFX-based chat client using [ntfy](https://docs.ntfy.sh/) for backend messa
1111
- Unit tests for `Model` class
1212
- (Advanced) Send files via "Attach local file" option
1313

14-
## 🚀 Run Instructions
15-
1. Set `JAVA_HOME` to JDK 25
14+
15+
## Requirements
16+
17+
- **Java**
18+
- **Version**: `25`
19+
20+
- **Maven Compiler Plugin**
21+
- **Version**: `3.11.0`
22+
- **Configuration**:
23+
- **Release**: `25`
24+
25+
## Usage
26+
1. Set `JAVA_HOME` to JDK 25.
27+
2. Create a **.env** file with the required variables. You can also clone and fill **.env.example** and rename it to `.env`.
1628
2. Start with:
1729
```bash
1830
./mvnw clean javafx:run

pom.xml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
<javafx.version>25</javafx.version>
1818
</properties>
1919
<dependencies>
20+
<dependency>
21+
<groupId>org.slf4j</groupId>
22+
<artifactId>slf4j-api</artifactId>
23+
<version>2.0.9</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.slf4j</groupId>
27+
<artifactId>slf4j-simple</artifactId>
28+
<version>2.0.9</version>
29+
</dependency>
30+
<dependency>
31+
<groupId>com.fasterxml.jackson.core</groupId>
32+
<artifactId>jackson-databind</artifactId>
33+
<version>2.17.2</version>
34+
</dependency>
2035
<dependency>
2136
<groupId>org.junit.jupiter</groupId>
2237
<artifactId>junit-jupiter</artifactId>
@@ -45,6 +60,18 @@
4560
<artifactId>javafx-fxml</artifactId>
4661
<version>${javafx.version}</version>
4762
</dependency>
63+
<dependency>
64+
<groupId>org.testfx</groupId>
65+
<artifactId>testfx-junit5</artifactId>
66+
<version>4.0.17</version>
67+
<scope>test</scope>
68+
</dependency>
69+
<dependency>
70+
<groupId>org.openjfx</groupId>
71+
<artifactId>javafx-swing</artifactId>
72+
<version>${javafx.version}</version>
73+
<scope>test</scope>
74+
</dependency>
4875
</dependencies>
4976
<build>
5077
<plugins>
@@ -63,6 +90,14 @@
6390
<noManPages>true</noManPages>
6491
</configuration>
6592
</plugin>
93+
<plugin>
94+
<groupId>org.apache.maven.plugins</groupId>
95+
<artifactId>maven-compiler-plugin</artifactId>
96+
<version>3.11.0</version>
97+
<configuration>
98+
<release>25</release>
99+
</configuration>
100+
</plugin>
66101
</plugins>
67102
</build>
68103
</project>
Lines changed: 216 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,230 @@
11
package com.example;
22

3+
import com.example.client.ChatNetworkClient;
4+
import com.example.domain.ChatModel;
5+
import com.example.domain.NtfyEventResponse;
6+
import com.example.domain.NtfyMessage;
7+
import javafx.animation.KeyFrame;
8+
import javafx.animation.Timeline;
9+
import javafx.event.ActionEvent;
310
import javafx.fxml.FXML;
11+
import javafx.scene.control.*;
412
import javafx.scene.control.Label;
13+
import javafx.scene.control.TextField;
14+
import javafx.scene.image.Image;
15+
import javafx.scene.image.ImageView;
16+
import javafx.scene.layout.VBox;
17+
import javafx.stage.FileChooser;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
520

6-
/**
7-
* Controller layer: mediates between the view (FXML) and the model.
8-
*/
9-
public class HelloController {
21+
import java.awt.*;
22+
import java.io.File;
23+
import java.io.IOException;
24+
import java.net.URI;
25+
import java.net.URISyntaxException;
26+
import java.time.Instant;
27+
import java.time.LocalTime;
28+
import java.time.ZoneId;
29+
import java.util.Arrays;
30+
import java.util.List;
31+
import java.util.UUID;
1032

11-
private final HelloModel model = new HelloModel();
33+
public class HelloController {
34+
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
35+
private ChatNetworkClient client;
36+
private String baseUrl;
37+
private String topic;
38+
private File selectedFile = null;
1239

1340
@FXML
1441
private Label messageLabel;
1542

1643
@FXML
17-
private void initialize() {
18-
if (messageLabel != null) {
19-
messageLabel.setText(model.getGreeting());
44+
private ListView<NtfyEventResponse> messagesList;
45+
46+
@FXML
47+
private TextField messageInput;
48+
49+
@FXML
50+
private TextField titleInput;
51+
52+
@FXML
53+
private TextField tagsInput;
54+
55+
public void setClient(ChatNetworkClient client, String baseUrl, String topic) {
56+
this.client = client;
57+
this.baseUrl = baseUrl;
58+
this.topic = topic;
59+
}
60+
61+
public void setModel(ChatModel model) {
62+
messagesList.setItems(model.getMessages());
63+
messagesList.setCellFactory(list -> new MessageCell());
64+
}
65+
66+
private static String formatTime(long epochSeconds) {
67+
Instant instant = Instant.ofEpochSecond(epochSeconds);
68+
LocalTime time = LocalTime.ofInstant(instant, ZoneId.systemDefault());
69+
return time.toString();
70+
}
71+
72+
private void showStatus(String text) {
73+
messageLabel.setText(text);
74+
Timeline t = new Timeline(new KeyFrame(javafx.util.Duration.seconds(3),
75+
ev -> messageLabel.setText("")));
76+
t.setCycleCount(1);
77+
t.play();
78+
}
79+
80+
81+
@FXML
82+
private void onPickAttachment() {
83+
FileChooser chooser = new FileChooser();
84+
chooser.setTitle("Select attachment");
85+
File file = chooser.showOpenDialog(messageInput.getScene().getWindow());
86+
87+
if (file != null) {
88+
selectedFile = file;
89+
messageLabel.setText("Attachment selected: " + file.getName());
90+
}
91+
}
92+
93+
@FXML
94+
private void onSend() {
95+
String txt = messageInput.getText();
96+
97+
if ((txt == null || txt.isBlank()) && selectedFile == null) {
98+
showStatus("Nothing to send");
99+
return;
100+
}
101+
102+
String title = titleInput.getText();
103+
if (title != null && title.isBlank()) title = null;
104+
105+
String tagsRaw = tagsInput.getText();
106+
List<String> tags = null;
107+
108+
if (tagsRaw != null && !tagsRaw.isBlank()) {
109+
tags = java.util.Arrays.stream(tagsRaw.split(","))
110+
.map(String::trim)
111+
.filter(s -> !s.isEmpty())
112+
.toList();
113+
}
114+
115+
NtfyMessage msg = new NtfyMessage.Builder()
116+
.id(UUID.randomUUID().toString())
117+
.time(System.currentTimeMillis())
118+
.event("message")
119+
.topic(topic)
120+
.message(txt)
121+
.title(title)
122+
.tags(tags)
123+
.attach(null)
124+
.filename(null)
125+
.build();
126+
127+
try {
128+
client.send(baseUrl, msg, selectedFile);
129+
showStatus(selectedFile == null ? "Message sent" : "Attachment sent");
130+
} catch (InterruptedException | IOException e) {
131+
showStatus("Error sending: " + e.getMessage());
132+
}
133+
134+
messageInput.clear();
135+
titleInput.clear();
136+
tagsInput.clear();
137+
selectedFile = null;
138+
}
139+
140+
141+
private static final class MessageCell extends ListCell<NtfyEventResponse> {
142+
@Override
143+
protected void updateItem(NtfyEventResponse msg, boolean empty) {
144+
super.updateItem(msg, empty);
145+
146+
if (empty || msg == null) {
147+
setText(null);
148+
setGraphic(null);
149+
return;
150+
}
151+
152+
setText(null);
153+
154+
VBox container = new VBox();
155+
container.setSpacing(6);
156+
container.getStyleClass().add("message-bubble");
157+
158+
container.setStyle("-fx-alignment: CENTER_LEFT;");
159+
if (msg.title() != null) {
160+
Label titleLabel = new Label(msg.title());
161+
titleLabel.getStyleClass().add("message-title");
162+
container.getChildren().add(titleLabel);
163+
}
164+
165+
if (msg.attachment() != null) {
166+
NtfyEventResponse.Attachment att = msg.attachment();
167+
168+
if (att.type() != null && att.type().startsWith("image")) {
169+
Image image = new Image(att.url(), 300, 0, true, true);
170+
ImageView imageView = new ImageView(image);
171+
container.getChildren().add(imageView);
172+
} else {
173+
Label fileLabel = getFileLabel(att);
174+
container.getChildren().add(fileLabel);
175+
}
176+
}
177+
178+
if (msg.message() != null && !msg.message().isBlank()) {
179+
Label messageLabel = new Label(msg.message());
180+
messageLabel.setWrapText(true);
181+
messageLabel.getStyleClass().add("message-text");
182+
container.getChildren().add(messageLabel);
183+
}
184+
185+
if (msg.tags() != null && !msg.tags().isEmpty()) {
186+
Label tagsLabel = new Label(String.join(", ", msg.tags()));
187+
tagsLabel.getStyleClass().add("message-tags");
188+
container.getChildren().add(tagsLabel);
189+
}
190+
191+
if (msg.time() != null) {
192+
Label timeLabel = new Label(formatTime(msg.time()));
193+
timeLabel.getStyleClass().add("message-time");
194+
container.getChildren().add(timeLabel);
195+
}
196+
197+
setGraphic(container);
198+
}
199+
200+
// Helper method to allow user to open file
201+
private static Label getFileLabel(NtfyEventResponse.Attachment att) {
202+
Label fileLabel = new Label("Open file: " + (att.name() != null ? att.name() : att.url()));
203+
fileLabel.setStyle("-fx-text-fill: #2c75ff; -fx-underline: true;");
204+
fileLabel.setOnMouseClicked(ev -> {
205+
try {
206+
String url = att.url();
207+
208+
// method that works on linux as Desktop is not always supported and crashes application
209+
if (System.getProperty("os.name").toLowerCase().contains("linux")) {
210+
try {
211+
new ProcessBuilder("xdg-open", url).start();
212+
return;
213+
}catch (IOException e) {
214+
log.error("Error opening file: {}", url, e);
215+
}
216+
}
217+
218+
if (Desktop.isDesktopSupported()) {
219+
Desktop.getDesktop().browse(new URI(url));
220+
}
221+
222+
} catch (IOException | URISyntaxException ex) {
223+
log.error("Failed to open attachment: {}", ex.getMessage());
224+
log.error(Arrays.toString(ex.getStackTrace()));
225+
}
226+
});
227+
return fileLabel;
20228
}
21229
}
22230
}
Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,59 @@
11
package com.example;
22

3+
import com.example.client.ChatNetworkClient;
4+
import com.example.client.NtfyHttpClient;
5+
import com.example.domain.ChatModel;
6+
import com.example.domain.NtfyMessage;
37
import javafx.application.Application;
48
import javafx.fxml.FXMLLoader;
59
import javafx.scene.Parent;
610
import javafx.scene.Scene;
711
import javafx.stage.Stage;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
import java.util.Objects;
16+
import java.util.Properties;
17+
18+
import static com.example.utils.EnvLoader.loadEnv;
819

920
public class HelloFX extends Application {
21+
private static final Logger log = LoggerFactory.getLogger("MAIN");
22+
static final ChatModel model = new ChatModel();
1023

1124
@Override
1225
public void start(Stage stage) throws Exception {
13-
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
14-
Parent root = fxmlLoader.load();
15-
Scene scene = new Scene(root, 640, 480);
16-
stage.setTitle("Hello MVC");
26+
Properties env = loadEnv();
27+
String baseUrl = env.getProperty("NTFY_BASE_URL", "https://ntfy.sh");
28+
String topic = env.getProperty("NTFY_TOPIC");
29+
30+
if (topic == null || topic.isBlank()) {
31+
throw new IllegalStateException("NTFY_TOPIC is not set");
32+
}
33+
34+
FXMLLoader loader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
35+
Parent root = loader.load();
36+
37+
HelloController controller = loader.getController();
38+
controller.setModel(model);
39+
40+
ChatNetworkClient client = new NtfyHttpClient(model);
41+
controller.setClient(client, baseUrl, topic);
42+
43+
client.subscribe(baseUrl, topic);
44+
45+
Scene scene = new Scene(root);
46+
scene.getStylesheets().add(
47+
Objects.requireNonNull(HelloFX.class.getResource("styles.css")).toExternalForm()
48+
);
49+
1750
stage.setScene(scene);
1851
stage.show();
1952
}
2053

2154
public static void main(String[] args) {
22-
launch();
55+
launch(args);
56+
2357
}
2458

2559
}

0 commit comments

Comments
 (0)