diff --git a/client/src/main/java/envoy/client/ui/ListViewRefresh.java b/client/src/main/java/envoy/client/ui/ListViewRefresh.java new file mode 100644 index 0000000..962b18c --- /dev/null +++ b/client/src/main/java/envoy/client/ui/ListViewRefresh.java @@ -0,0 +1,36 @@ +package envoy.client.ui; + +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; + +/** + * This is a utility class that provides access to a refreshing mechanism for + * elements that were added without notifying the underlying {@link ListView}. + *

+ * Project: envoy-client
+ * File: ListViewRefresh.java
+ * Created: 16.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.1-beta + */ +public final class ListViewRefresh { + + private ListViewRefresh() {} + + /** + * Deeply refreshes a {@code listview}, meaning it recomputes every single of + * its {@link ListCell}s. + *

+ * While it does work, it is not the most efficient algorithm possible. + * + * @param toRefresh the listView to refresh + * @param the type of its {@code listcells} + * @since Envoy Client v0.1-beta + */ + public static void deepRefresh(ListView toRefresh) { + final var items = toRefresh.getItems(); + toRefresh.setItems(null); + toRefresh.setItems(items); + } +} diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java index 920d93f..83e64fe 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -31,9 +31,7 @@ import envoy.client.data.audio.AudioRecorder; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; -import envoy.client.ui.IconUtil; -import envoy.client.ui.Restorable; -import envoy.client.ui.SceneContext; +import envoy.client.ui.*; import envoy.client.ui.listcell.ChatControl; import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.MessageControl; @@ -144,7 +142,7 @@ public final class ChatScene implements Restorable { } catch (final IOException e1) { logger.log(Level.WARNING, "Could not read current chat: ", e1); } - Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); }); + Platform.runLater(() -> { ListViewRefresh.deepRefresh(messageList); scrollToMessageListEnd(); }); } else chat.incrementUnreadAmount(); // Moving chat with most recent unreadMessages to the top Platform.runLater(() -> { @@ -158,8 +156,9 @@ public final class ChatScene implements Restorable { // Listen to message status changes eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> { message.setStatus(e.get()); - // Update UI if in current chat - if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); + // Update UI if in current chat and the current user was the sender of the + // message + if (currentChat != null && message.getSenderID() == client.getSender().getID()) Platform.runLater(messageList::refresh); })); eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> { @@ -176,7 +175,7 @@ public final class ChatScene implements Restorable { .filter(c -> c.getRecipient().getID() == e.getID()) .findAny() .map(Chat::getRecipient) - .ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); })); + .ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(() -> ListViewRefresh.deepRefresh(chatList)); })); // Listen to contacts changes eventBus.register(ContactOperation.class, e -> { @@ -184,7 +183,7 @@ public final class ChatScene implements Restorable { switch (e.getOperationType()) { case ADD: localDB.getUsers().put(contact.getName(), contact); - Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact); + final Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact); Platform.runLater(() -> chatList.getItems().add(chat)); break; case REMOVE: @@ -356,6 +355,7 @@ public final class ChatScene implements Restorable { try { final var fileBytes = Files.readAllBytes(file.toPath()); pendingAttachment = new Attachment(fileBytes, type); + checkPostConditions(false); // Setting the preview image as image of the attachmentView if (type == AttachmentType.PICTURE) attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true)); @@ -414,9 +414,9 @@ public final class ChatScene implements Restorable { || !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()); } - private void checkPostConditions(boolean sendKeyPressed) { + private void checkPostConditions(boolean postMessage) { if (!postingPermanentlyDisabled) { - if (!postButton.isDisabled() && sendKeyPressed) postMessage(); + if (!postButton.isDisabled() && postMessage) postMessage(); postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null); } else { final var noMoreMessaging = "Go online to send messages"; @@ -499,7 +499,7 @@ public final class ChatScene implements Restorable { localDB.getChats().remove(currentChat); localDB.getChats().add(0, currentChat); }); - messageList.refresh(); + ListViewRefresh.deepRefresh(messageList); scrollToMessageListEnd(); // Request a new ID generator if all IDs were used diff --git a/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java b/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java index 5b5c448..e21c646 100644 --- a/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java @@ -118,7 +118,8 @@ public class ContactSearchScene { final var event = new ContactOperation(user, ElementOperation.ADD); // Sends the event to the server eventBus.dispatch(new SendEvent(event)); - // Updates the UI + // Removes the chosen user and updates the UI + userList.getItems().remove(user); eventBus.dispatch(event); logger.log(Level.INFO, "Added user " + user); })); diff --git a/client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java b/client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java index e6ca29a..71c7dd4 100644 --- a/client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java +++ b/client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java @@ -16,6 +16,8 @@ import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; import envoy.client.ui.listcell.ContactControl; import envoy.client.ui.listcell.ListCellFactory; +import envoy.data.Contact; +import envoy.data.Group; import envoy.data.User; import envoy.event.EventBus; import envoy.event.GroupCreation; @@ -50,6 +52,8 @@ public class GroupCreationScene { private SceneContext sceneContext; + private LocalDB localDB; + private static final EventBus eventBus = EventBus.getInstance(); @FXML @@ -66,7 +70,8 @@ public class GroupCreationScene { * @since Envoy Client v0.1-beta */ public void initializeData(SceneContext sceneContext, LocalDB localDB) { - this.sceneContext = sceneContext; + this.sceneContext = sceneContext; + this.localDB = localDB; Platform.runLater(() -> userList.getItems() .addAll(localDB.getChats() .stream() @@ -109,14 +114,47 @@ public class GroupCreationScene { if (!Bounds.isValidContactName(name)) { new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait(); groupNameField.getTextField().clear(); + } else if (groupNameAlreadyPresent(name)) { + final var alert = new Alert(AlertType.WARNING, "You already have a group with that name.", ButtonType.OK, ButtonType.CANCEL); + alert.setTitle("Create Group?"); + alert.setHeaderText("Proceed?"); + alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> createGroup(name)); } else { - eventBus.dispatch(new SendEvent( - new GroupCreation(name, userList.getSelectionModel().getSelectedItems().stream().map(User::getID).collect(Collectors.toSet())))); new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait(); - sceneContext.pop(); + createGroup(name); } } + /** + * Creates a new group with the given name and all selected members.
+ * Additionally pops the scene automatically. + * + * @param name the chosen group name + * @since Envoy Client v0.1-beta + */ + private void createGroup(String name) { + eventBus.dispatch(new SendEvent( + new GroupCreation(name, userList.getSelectionModel().getSelectedItems().stream().map(User::getID).collect(Collectors.toSet())))); + sceneContext.pop(); + } + + /** + * Returns true if the proposed group name is already present in the users + * {@code LocalDB}. + * + * @param newName the chosen group name + * @return true if this name is already present + * @since Envoy Client v0.1-beta + */ + public boolean groupNameAlreadyPresent(String newName) { + return localDB.getChats() + .stream() + .map(Chat::getRecipient) + .filter(Group.class::isInstance) + .map(Contact::getName) + .anyMatch(newName::equals); + } + @FXML private void backButtonClicked() { sceneContext.pop(); } }