diff --git a/src/main/java/envoy/client/data/Chat.java b/src/main/java/envoy/client/data/Chat.java index 739d335..b1c2beb 100644 --- a/src/main/java/envoy/client/data/Chat.java +++ b/src/main/java/envoy/client/data/Chat.java @@ -29,6 +29,8 @@ public class Chat implements Serializable { protected final Contact recipient; protected final List messages = new ArrayList<>(); + protected int unreadAmount; + private static final long serialVersionUID = 1L; /** @@ -87,6 +89,7 @@ public class Chat implements Serializable { writeProxy.writeMessageStatusChange(new MessageStatusChange(m)); } } + unreadAmount = 0; } /** @@ -111,6 +114,19 @@ public class Chat implements Serializable { messages.add(0, message); } + /** + * Increments the amount of unread messages. + * + * @since Envoy Client v0.1-beta + */ + public void incrementUnreadAmount() { unreadAmount++; } + + /** + * @return the amount of unread mesages in this chat + * @since Envoy Client v0.1-beta + */ + public int getUnreadAmount() { return unreadAmount; } + /** * @return all messages in the current chat * @since Envoy Client v0.1-beta diff --git a/src/main/java/envoy/client/data/GroupChat.java b/src/main/java/envoy/client/data/GroupChat.java index 3c1e98d..aafd3a0 100644 --- a/src/main/java/envoy/client/data/GroupChat.java +++ b/src/main/java/envoy/client/data/GroupChat.java @@ -50,5 +50,6 @@ public class GroupChat extends Chat { } } } + unreadAmount = 0; } } diff --git a/src/main/java/envoy/client/ui/controller/ChatScene.java b/src/main/java/envoy/client/ui/controller/ChatScene.java index 2f3402f..98dc64a 100644 --- a/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -9,7 +9,6 @@ import java.nio.file.Files; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; import javafx.animation.RotateTransition; import javafx.application.Platform; @@ -65,7 +64,7 @@ public final class ChatScene implements Restorable { private ListView messageList; @FXML - private ListView userList; + private ListView chatList; @FXML private Button postButton; @@ -126,7 +125,7 @@ public final class ChatScene implements Restorable { // Initialize message and user rendering messageList.setCellFactory(MessageListCellFactory::new); - userList.setCellFactory(ContactListCellFactory::new); + chatList.setCellFactory(ContactListCellFactory::new); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); @@ -146,7 +145,15 @@ public final class ChatScene implements Restorable { logger.log(Level.WARNING, "Could not read current chat: ", e1); } Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); }); - } + } else chat.incrementUnreadAmount(); + // Moving chat with most recent unreadMessages to the top + Platform.runLater(() -> { + chatList.getItems().remove(chat); + chatList.getItems().add(0, chat); + if (chat.equals(currentChat)) chatList.getSelectionModel().select(0); + localDB.getChats().remove(chat); + localDB.getChats().add(0, chat); + }); }); }); @@ -166,11 +173,12 @@ public final class ChatScene implements Restorable { // Listen to user status changes eventBus.register(UserStatusChange.class, - e -> userList.getItems() + e -> chatList.getItems() .stream() - .filter(c -> c.getID() == e.getID()) + .filter(c -> c.getRecipient().getID() == e.getID()) .findAny() - .ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(userList::refresh); })); + .map(Chat::getRecipient) + .ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); })); // Listen to contacts changes eventBus.register(ContactOperation.class, e -> { @@ -178,13 +186,14 @@ public final class ChatScene implements Restorable { switch (e.getOperationType()) { case ADD: localDB.getUsers().put(contact.getName(), contact); - localDB.getChats().add(contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact)); - Platform.runLater(() -> userList.getItems().add(contact)); + Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact); + localDB.getChats().add(chat); + Platform.runLater(() -> chatList.getItems().add(chat)); break; case REMOVE: localDB.getUsers().remove(contact.getName()); localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID()); - Platform.runLater(() -> userList.getItems().removeIf(c -> c.getID() == contact.getID())); + Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().getID() == contact.getID())); break; } }); @@ -206,7 +215,7 @@ public final class ChatScene implements Restorable { this.client = client; this.writeProxy = writeProxy; - userList.setItems(FXCollections.observableList(localDB.getChats().stream().map(Chat::getRecipient).collect(Collectors.toList()))); + chatList.setItems(FXCollections.observableList(localDB.getChats())); contactLabel.setText(localDB.getUser().getName()); MessageControl.setUser(localDB.getUser()); if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); @@ -223,8 +232,8 @@ public final class ChatScene implements Restorable { * @since Envoy Client v0.1-beta */ @FXML - private void userListClicked() { - final Contact user = userList.getSelectionModel().getSelectedItem(); + private void chatListClicked() { + final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient(); if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes @@ -233,7 +242,7 @@ public final class ChatScene implements Restorable { currentChat = localDB.getChat(user.getID()).get(); messageList.setItems(FXCollections.observableList(currentChat.getMessages())); - final var scrollIndex = messageList.getItems().size() - 1; + final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1; messageList.scrollTo(scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); deleteContactMenuItem.setText("Delete " + user.getName()); @@ -260,6 +269,7 @@ public final class ChatScene implements Restorable { messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled); voiceButton.setDisable(!recorder.isSupported()); attachmentButton.setDisable(false); + chatList.refresh(); } /** @@ -483,6 +493,14 @@ public final class ChatScene implements Restorable { // Add message to LocalDB and update UI currentChat.insert(message); + // Moving currentChat to the top + Platform.runLater(() -> { + chatList.getItems().remove(currentChat); + chatList.getItems().add(0, currentChat); + chatList.getSelectionModel().select(0); + localDB.getChats().remove(currentChat); + localDB.getChats().add(0, currentChat); + }); messageList.refresh(); scrollToMessageListEnd(); diff --git a/src/main/java/envoy/client/ui/controller/ContactSearchScene.java b/src/main/java/envoy/client/ui/controller/ContactSearchScene.java index 4fe48d2..a86d79d 100644 --- a/src/main/java/envoy/client/ui/controller/ContactSearchScene.java +++ b/src/main/java/envoy/client/ui/controller/ContactSearchScene.java @@ -2,6 +2,7 @@ package envoy.client.ui.controller; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javafx.application.Platform; import javafx.fxml.FXML; @@ -10,12 +11,12 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; import javafx.scene.control.ListView; +import envoy.client.data.Chat; import envoy.client.data.LocalDB; import envoy.client.event.SendEvent; import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; import envoy.client.ui.listcell.ContactListCellFactory; -import envoy.data.Contact; import envoy.event.ElementOperation; import envoy.event.EventBus; import envoy.event.contact.ContactOperation; @@ -37,7 +38,7 @@ public class ContactSearchScene { private ClearableTextField searchBar; @FXML - private ListView contactList; + private ListView chatList; private SceneContext sceneContext; @@ -58,10 +59,13 @@ public class ContactSearchScene { @FXML private void initialize() { - contactList.setCellFactory(ContactListCellFactory::new); - searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); contactList.getItems().clear(); }); + chatList.setCellFactory(ContactListCellFactory::new); + searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); }); eventBus.register(ContactSearchResult.class, - response -> Platform.runLater(() -> { contactList.getItems().clear(); contactList.getItems().addAll(response.get()); })); + response -> Platform.runLater(() -> { + chatList.getItems().clear(); + chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList())); + })); } /** @@ -73,7 +77,7 @@ public class ContactSearchScene { private void sendRequest() { final var text = searchBar.getTextField().getText().strip(); if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text))); - else contactList.getItems().clear(); + else chatList.getItems().clear(); } /** @@ -85,7 +89,7 @@ public class ContactSearchScene { @FXML private void clear() { searchBar.getTextField().setText(null); - contactList.getItems().clear(); + chatList.getItems().clear(); } /** @@ -95,22 +99,22 @@ public class ContactSearchScene { * @since Envoy Client v0.1-beta */ @FXML - private void contactListClicked() { - final var contact = contactList.getSelectionModel().getSelectedItem(); - if (contact != null) { + private void chatListClicked() { + final var chat = chatList.getSelectionModel().getSelectedItem(); + if (chat != null) { final var alert = new Alert(AlertType.CONFIRMATION); alert.setTitle("Add Contact to Contact List"); - alert.setHeaderText("Add the user " + contact.getName() + " to your contact list?"); + alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?"); // Normally, this would be total BS (we are already on the FX Thread), however // it could be proven that the creation of this dialog wrapped in // Platform.runLater is less error-prone than without it Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> { - final var event = new ContactOperation(contact, ElementOperation.ADD); + final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD); // Sends the event to the server eventBus.dispatch(new SendEvent(event)); // Updates the UI eventBus.dispatch(event); - logger.log(Level.INFO, "Added contact " + contact); + logger.log(Level.INFO, "Added contact " + chat.getRecipient()); })); } } diff --git a/src/main/java/envoy/client/ui/controller/GroupCreationScene.java b/src/main/java/envoy/client/ui/controller/GroupCreationScene.java index 76573b8..e7bbf93 100644 --- a/src/main/java/envoy/client/ui/controller/GroupCreationScene.java +++ b/src/main/java/envoy/client/ui/controller/GroupCreationScene.java @@ -7,12 +7,13 @@ import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; +import envoy.client.data.Chat; import envoy.client.data.LocalDB; import envoy.client.event.SendEvent; import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; import envoy.client.ui.listcell.ContactListCellFactory; -import envoy.data.Contact; +import envoy.data.Group; import envoy.event.EventBus; import envoy.event.GroupCreation; import envoy.util.Bounds; @@ -34,7 +35,7 @@ public class GroupCreationScene { private ClearableTextField groupNameField; @FXML - private ListView contactList; + private ListView chatList; private SceneContext sceneContext; @@ -42,8 +43,8 @@ public class GroupCreationScene { @FXML private void initialize() { - contactList.setCellFactory(ContactListCellFactory::new); - contactList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + chatList.setCellFactory(ContactListCellFactory::new); + chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); }); } @@ -55,8 +56,12 @@ public class GroupCreationScene { */ public void initializeData(SceneContext sceneContext, LocalDB localDB) { this.sceneContext = sceneContext; - Platform.runLater(() -> contactList.getItems() - .addAll(localDB.getUsers().values().stream().filter(c -> c.getID() != localDB.getUser().getID()).collect(Collectors.toList()))); + Platform.runLater(() -> chatList.getItems() + .addAll(localDB.getChats() + .stream() + .filter(c -> !(c.getRecipient() instanceof Group)) + .filter(c -> c.getRecipient().getID() != localDB.getUser().getID()) + .collect(Collectors.toList()))); } /** @@ -65,8 +70,8 @@ public class GroupCreationScene { * @since Envoy Client v0.1-beta */ @FXML - private void contactListClicked() { - createButton.setDisable(contactList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank()); + private void chatListClicked() { + createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank()); } /** @@ -93,7 +98,7 @@ public class GroupCreationScene { groupNameField.getTextField().clear(); } else { eventBus.dispatch(new SendEvent(new GroupCreation(name, - contactList.getSelectionModel().getSelectedItems().stream().map(Contact::getID).collect(Collectors.toSet())))); + chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet())))); new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait(); sceneContext.pop(); } diff --git a/src/main/java/envoy/client/ui/listcell/ChatControl.java b/src/main/java/envoy/client/ui/listcell/ChatControl.java new file mode 100644 index 0000000..98294a7 --- /dev/null +++ b/src/main/java/envoy/client/ui/listcell/ChatControl.java @@ -0,0 +1,58 @@ +package envoy.client.ui.listcell; + +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.*; + +import envoy.client.data.Chat; +import envoy.data.Contact; +import envoy.data.Group; +import envoy.data.User; + +/** + * This class formats a single {@link Contact} into a UI component. + *

+ * Project: envoy-client
+ * File: ContactControl.java
+ * Created: 01.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +public class ChatControl extends HBox { + + /** + * @param chat the chat to display + * @since Envoy Client v0.1-beta + */ + public ChatControl(Chat chat) { + // Container with contact name + final var vBox = new VBox(); + final var nameLabel = new Label(chat.getRecipient().getName()); + nameLabel.setWrapText(true); + vBox.getChildren().add(nameLabel); + if (chat.getRecipient() instanceof User) { + // Online status + final var user = (User) chat.getRecipient(); + final var statusLabel = new Label(user.getStatus().toString()); + statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase()); + vBox.getChildren().add(statusLabel); + } else // Member count + vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members")); + + getChildren().add(vBox); + if (chat.getUnreadAmount() != 0) { + Region spacing = new Region(); + setHgrow(spacing, Priority.ALWAYS); + getChildren().add(spacing); + final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount())); + unreadMessagesLabel.setMinSize(15, 15); + var vBox2 = new VBox(); + vBox2.setAlignment(Pos.CENTER_RIGHT); + unreadMessagesLabel.setAlignment(Pos.CENTER); + unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount"); + vBox2.getChildren().add(unreadMessagesLabel); + getChildren().add(vBox2); + } + } +} diff --git a/src/main/java/envoy/client/ui/listcell/ContactControl.java b/src/main/java/envoy/client/ui/listcell/ContactControl.java deleted file mode 100644 index 2a802fc..0000000 --- a/src/main/java/envoy/client/ui/listcell/ContactControl.java +++ /dev/null @@ -1,40 +0,0 @@ -package envoy.client.ui.listcell; - -import javafx.scene.control.Label; -import javafx.scene.layout.VBox; - -import envoy.data.Contact; -import envoy.data.Group; -import envoy.data.User; - -/** - * This class formats a single {@link Contact} into a UI component. - *

- * Project: envoy-client
- * File: ContactControl.java
- * Created: 01.07.2020
- * - * @author Leon Hofmeister - * @since Envoy Client v0.1-beta - */ -public class ContactControl extends VBox { - - /** - * @param contact the contact that should be formatted - * @since Envoy Client v0.1-beta - */ - public ContactControl(Contact contact) { - // Container with contact name - final var nameLabel = new Label(contact.getName()); - nameLabel.setWrapText(true); - getChildren().add(nameLabel); - if (contact instanceof User) { - // Online status - final var user = (User) contact; - final var statusLabel = new Label(user.getStatus().toString()); - statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase()); - getChildren().add(statusLabel); - } else // Member count - getChildren().add(new Label(((Group) contact).getContacts().size() + " members")); - } -} diff --git a/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java b/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java index 8f8d3f0..3d34aa9 100644 --- a/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java +++ b/src/main/java/envoy/client/ui/listcell/ContactListCellFactory.java @@ -3,7 +3,7 @@ package envoy.client.ui.listcell; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import envoy.data.Contact; +import envoy.client.data.Chat; /** * Project: envoy-client
@@ -13,15 +13,15 @@ import envoy.data.Contact; * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ -public class ContactListCellFactory extends ListCell { +public class ContactListCellFactory extends ListCell { - private final ListView listView; + private final ListView listView; /** * @param listView the list view inside which this cell is contained * @since Envoy Client v0.1-beta */ - public ContactListCellFactory(ListView listView) { this.listView = listView; } + public ContactListCellFactory(ListView listView) { this.listView = listView; } /** * Displays the name of a contact. If the contact is a user, their online status @@ -30,13 +30,13 @@ public class ContactListCellFactory extends ListCell { * @since Envoy Client v0.1-beta */ @Override - protected void updateItem(Contact contact, boolean empty) { - super.updateItem(contact, empty); - if (empty || contact == null) { + protected void updateItem(Chat chat, boolean empty) { + super.updateItem(chat, empty); + if (empty || chat.getRecipient() == null) { setText(null); setGraphic(null); } else { - final var control = new ContactControl(contact); + final var control = new ChatControl(chat); prefWidthProperty().bind(listView.widthProperty().subtract(40)); setGraphic(control); } diff --git a/src/main/resources/css/base.css b/src/main/resources/css/base.css index 2830bc6..78fff9e 100644 --- a/src/main/resources/css/base.css +++ b/src/main/resources/css/base.css @@ -58,6 +58,13 @@ -fx-text-alignment: left; } +.unreadMessagesAmount { + -fx-alignment: center; + -fx-background-color: orange; + -fx-background-radius: 4.0em; + -fx-text-alignment: center; +} + #remainingCharsLabel { -fx-text-fill: #00FF00; -fx-background-color: transparent; diff --git a/src/main/resources/fxml/ChatScene.fxml b/src/main/resources/fxml/ChatScene.fxml index 5d1e762..11c4c3e 100644 --- a/src/main/resources/fxml/ChatScene.fxml +++ b/src/main/resources/fxml/ChatScene.fxml @@ -45,7 +45,7 @@ minHeight="-Infinity" prefHeight="40.0" vgrow="NEVER" /> - diff --git a/src/main/resources/fxml/ContactSearchScene.fxml b/src/main/resources/fxml/ContactSearchScene.fxml index b69d15c..a88430c 100644 --- a/src/main/resources/fxml/ContactSearchScene.fxml +++ b/src/main/resources/fxml/ContactSearchScene.fxml @@ -46,8 +46,8 @@ - diff --git a/src/main/resources/fxml/GroupCreationScene.fxml b/src/main/resources/fxml/GroupCreationScene.fxml index 90a8516..0085b03 100644 --- a/src/main/resources/fxml/GroupCreationScene.fxml +++ b/src/main/resources/fxml/GroupCreationScene.fxml @@ -45,8 +45,8 @@ -