11package com .example ;
2+ import javafx .collections .FXCollections ;
3+ import javafx .collections .ObservableList ;
24import javafx .event .ActionEvent ;
35import javafx .fxml .FXML ;
46import javafx .geometry .Insets ;
57import javafx .scene .control .*;
8+ import javafx .scene .control .Button ;
9+ import javafx .scene .control .Label ;
10+ import javafx .scene .control .TextField ;
11+ import javafx .scene .image .Image ;
12+ import javafx .scene .image .ImageView ;
613import javafx .scene .layout .HBox ;
14+ import javafx .stage .FileChooser ;
15+
16+ import java .awt .*;
17+ import java .io .File ;
18+ import java .net .URI ;
719
820/**
921 * Controller layer: mediates between the view (FXML) and the model.
@@ -12,61 +24,164 @@ public class HelloController {
1224
1325 private final HelloModel model = new HelloModel (new NtfyConnectionImpl ());
1426 public ListView <NtfyMessageDto > messageView ;
27+ private boolean lastActionWasFile = false ;
1528
1629 @ FXML
1730 private Label messageLabel ;
1831
1932 @ FXML
2033 private TextField messageField ;
2134
35+ @ FXML
36+ private void attachFile () {
37+ FileChooser chooser = new FileChooser ();
38+ chooser .getExtensionFilters ().add (
39+ new FileChooser .ExtensionFilter ("Images" , "*.png" , "*.jpg" , "*.jpeg" , "*.gif" , "*.txt" )
40+ );
41+
42+ File file = chooser .showOpenDialog (messageField .getScene ().getWindow ());
43+ if (file != null ) {
44+ lastActionWasFile = true ;
45+ model .sendFile (file );
46+ }
47+ }
48+
2249 @ FXML
2350 private void initialize () {
51+
2452 if (messageLabel != null ) {
2553 messageLabel .setText (model .getGreeting ());
2654 }
55+
2756 messageView .setItems (model .getMessages ());
2857
58+ // quickfix :D prevent listview cell selection which forces a re-render and
59+ // causes image messages to flicker or disappear when clicking or scrolling.
60+ messageView .setSelectionModel (new NoSelectionModel <>());
61+
62+
2963 messageView .setCellFactory (list -> new ListCell <>() {
30- private final Label messageLabel = new Label ();
31- private final HBox bubble = new HBox (messageLabel );
64+
65+ private final Label textLabel = new Label ();
66+ private final HBox textBubble = new HBox (textLabel );
67+
3268 {
33- bubble .setPadding (new Insets (5 , 10 , 5 , 10 ));
34- bubble .setMaxWidth (200 );
35- messageLabel .setWrapText (true );
36- bubble .getStyleClass ().add ("chat-bubble" );
69+ textBubble .setPadding (new Insets (5 , 10 , 5 , 10 ));
70+ textBubble .setMaxWidth (200 );
71+ textLabel .setWrapText (true );
72+ textBubble .getStyleClass ().add ("chat-bubble" );
3773 }
3874
3975 @ Override
4076 protected void updateItem (NtfyMessageDto item , boolean empty ) {
4177 super .updateItem (item , empty );
78+
4279 if (empty || item == null ) {
80+ setText (null );
4381 setGraphic (null );
44- } else {
45- // Format tid + text
46- java .time .LocalTime time = java .time .Instant .ofEpochSecond (item .time ())
47- .atZone (java .time .ZoneId .systemDefault ())
48- .toLocalTime ();
49- String formattedTime = time .format (java .time .format .DateTimeFormatter .ofPattern ("HH:mm" ));
50-
51- messageLabel .setText (formattedTime + "\n " + item .message ());
52- setGraphic (bubble );
82+ return ;
83+ }
84+
85+ if (item .attachment () != null &&
86+ item .attachment ().type () != null &&
87+ item .attachment ().type ().startsWith ("image" ) &&
88+ item .attachment ().url () != null ) {
89+
90+ try {
91+ Image image = new Image (item .attachment ().url (), 200 , 0 , true , true );
92+ ImageView imageView = new ImageView (image );
93+ imageView .setPreserveRatio (true );
94+
95+ HBox imageBubble = new HBox (imageView );
96+ imageBubble .setPadding (new Insets (5 , 10 , 5 , 10 ));
97+ imageBubble .setMaxWidth (200 );
98+ imageBubble .getStyleClass ().add ("chat-bubble" );
99+
100+ setText (null );
101+ setGraphic (imageBubble );
102+ } catch (Exception e ) {
103+ Label err = new Label ("[Image failed to load]" );
104+ HBox bubble = new HBox (err );
105+ bubble .setPadding (new Insets (5 , 10 , 5 , 10 ));
106+ bubble .getStyleClass ().add ("chat-bubble" );
107+ setGraphic (bubble );
108+ }
109+ return ;
110+ }
111+
112+ if (item .attachment () != null && item .attachment ().url () != null ) {
113+
114+ String fileName = item .attachment ().name ();
115+ String fileUrl = item .attachment ().url ();
116+
117+ Label icon = new Label ("📄" );
118+ icon .setStyle ("-fx-font-size: 20px;" );
119+
120+ Label fileLabel = new Label (fileName );
121+ fileLabel .setStyle ("-fx-text-fill: white; -fx-font-size: 14px;" );
122+
123+ Button openBtn = new Button ("Open file" );
124+ openBtn .setOnAction (e -> {
125+ System .out .println ("Opening: " + fileUrl );
126+ new Thread (() -> {
127+ try {
128+ URI uri = new URI (fileUrl );
129+ Desktop .getDesktop ().browse (uri );
130+ } catch (Exception ex ) {
131+ ex .printStackTrace ();
132+ }
133+ }).start ();
134+ });
135+
136+
137+ HBox fileBox = new HBox (10 , icon , fileLabel , openBtn );
138+ fileBox .setPadding (new Insets (5 , 10 , 5 , 10 ));
139+ fileBox .setMaxWidth (200 );
140+ fileBox .getStyleClass ().add ("chat-bubble" );
141+
142+ setText (null );
143+ setGraphic (fileBox );
144+ return ;
53145 }
146+
147+
148+ java .time .LocalTime time = java .time .Instant .ofEpochSecond (item .time ())
149+ .atZone (java .time .ZoneId .systemDefault ())
150+ .toLocalTime ();
151+ String formattedTime = time .format (java .time .format .DateTimeFormatter .ofPattern ("HH:mm" ));
152+
153+ Label msgLabel = new Label (formattedTime + "\n " + item .message ());
154+ msgLabel .setWrapText (true );
155+ msgLabel .setMaxWidth (250 );
156+
157+ HBox msgBubble = new HBox (msgLabel );
158+ msgBubble .setPadding (new Insets (5 , 10 , 5 , 10 ));
159+ msgBubble .setMaxWidth (300 );
160+ msgBubble .getStyleClass ().add ("chat-bubble" );
161+
162+ setText (null );
163+ setGraphic (msgBubble );
54164 }
165+
55166 });
56167
57168 model .messageToSendProperty ().bind (messageField .textProperty ());
58-
59169 }
60170
171+
61172 public void sendMessage (ActionEvent actionEvent ) {
62173 String message = messageField .getText ();
63- if (message == null || message .isBlank ()){
174+ if (! lastActionWasFile && ( message == null || message .isBlank () )){
64175 showTemporaryAlert ("You must write something before sending!" );
65176 return ;
66177 }
67178
68- model .sendMessage ();
179+ if (message != null && !message .isBlank ()) {
180+ model .sendMessage ();
181+ }
182+
69183 messageField .clear ();
184+ lastActionWasFile = false ;
70185 }
71186
72187 private void showTemporaryAlert (String alertMessage ) {
@@ -83,4 +198,38 @@ private void showTemporaryAlert(String alertMessage) {
83198 } catch (InterruptedException e ) {}
84199 }).start ();
85200 }
201+
202+ private static class NoSelectionModel <T > extends MultipleSelectionModel <T > {
203+
204+ /*
205+ Quickfix to prevent the ListView from selecting cells when the user clicks on them.
206+ Otherwise it triggers full re render of the listcells when which causes message bubbles
207+ or images to flicker or dissapears on click or scroll
208+ */
209+
210+ @ Override
211+ public ObservableList <Integer > getSelectedIndices () {
212+ return FXCollections .emptyObservableList ();
213+ }
214+
215+ @ Override
216+ public ObservableList <T > getSelectedItems () {
217+ return FXCollections .emptyObservableList ();
218+ }
219+
220+ @ Override public void selectIndices (int index , int ... indices ) { }
221+ @ Override public void selectAll () { }
222+ @ Override public void clearAndSelect (int index ) { }
223+ @ Override public void select (int index ) { }
224+ @ Override public void select (T obj ) { }
225+ @ Override public void clearSelection (int index ) { }
226+ @ Override public void clearSelection () { }
227+ @ Override public boolean isSelected (int index ) { return false ; }
228+ @ Override public boolean isEmpty () { return true ; }
229+ @ Override public void selectPrevious () { }
230+ @ Override public void selectNext () { }
231+ @ Override public void selectFirst () { }
232+ @ Override public void selectLast () { }
233+ }
86234}
235+
0 commit comments