From 571a953c40c2dd1549b0d90f1605209ea01c77ca Mon Sep 17 00:00:00 2001 From: delvh Date: Sun, 11 Oct 2020 23:04:25 +0200 Subject: [PATCH] Add partially working blocking and deletion (for both client and server) Additionally had to refactor several classes "a little bit". (Whenever one bug seemed fixed, another one appeared...) --- .../src/main/java/envoy/client/data/Chat.java | 20 +++ .../java/envoy/client/data/GroupChat.java | 2 +- .../main/java/envoy/client/data/LocalDB.java | 59 ++++++++- .../main/java/envoy/client/net/Client.java | 29 ++++- .../main/java/envoy/client/ui/Startup.java | 2 +- .../envoy/client/ui/control/ChatControl.java | 4 +- .../envoy/client/ui/controller/ChatScene.java | 121 ++++++++++++++---- .../ui/controller/ContactSearchTab.java | 6 +- .../ui/controller/GroupCreationTab.java | 6 +- .../main/java/envoy/client/util/UserUtil.java | 62 ++++++++- client/src/main/resources/css/base.css | 19 ++- client/src/main/resources/fxml/ChatScene.fxml | 2 +- .../main/java/envoy/event/GroupCreation.java | 6 +- .../java/envoy/event/GroupCreationResult.java | 18 ++- .../main/java/envoy/event/GroupResize.java | 15 +-- ...ava => ContactsChangedSinceLastLogin.java} | 2 +- ...ntactOperation.java => UserOperation.java} | 17 ++- .../processors/ContactOperationProcessor.java | 12 +- .../processors/GroupCreationProcessor.java | 11 +- .../processors/LoginCredentialProcessor.java | 7 +- 20 files changed, 326 insertions(+), 94 deletions(-) rename common/src/main/java/envoy/event/contact/{ContactDeletionSinceLastLogin.java => ContactsChangedSinceLastLogin.java} (82%) rename common/src/main/java/envoy/event/contact/{ContactOperation.java => UserOperation.java} (56%) diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index 80b2a97..ba40bbe 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -27,6 +27,8 @@ public class Chat implements Serializable { protected int unreadAmount; + private boolean disabled; + /** * Stores the last time an {@link envoy.event.IsTyping} event has been sent. */ @@ -168,4 +170,22 @@ public class Chat implements Serializable { * @since Envoy Client v0.2-beta */ public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); } + + /** + * Determines whether messages can be sent in this chat. Should be true i.e. for + * chats whose recipient deleted this client as a contact. + * + * @return whether this {@code Chat} has been disabled + * @since Envoy Client v0.3-beta + */ + public boolean isDisabled() { return disabled; } + + /** + * Determines whether messages can be sent in this chat. Should be true i.e. for + * chats whose recipient deleted this client as a contact. + * + * @param disabled the disabled to set + * @since Envoy Client v0.3-beta + */ + public void setDisabled(boolean disabled) { this.disabled = disabled; } } diff --git a/client/src/main/java/envoy/client/data/GroupChat.java b/client/src/main/java/envoy/client/data/GroupChat.java index 188709c..049c32e 100644 --- a/client/src/main/java/envoy/client/data/GroupChat.java +++ b/client/src/main/java/envoy/client/data/GroupChat.java @@ -25,7 +25,7 @@ public final class GroupChat extends Chat { * @param recipient the group whose members receive the messages * @since Envoy Client v0.1-beta */ - public GroupChat(User sender, Contact recipient) { + public GroupChat(User sender, Group recipient) { super(recipient); this.sender = sender; } diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index 13db9df..691a878 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -14,6 +14,7 @@ import envoy.client.event.*; import envoy.data.*; import envoy.data.Message.MessageStatus; import envoy.event.*; +import envoy.event.contact.*; import envoy.exception.EnvoyException; import envoy.util.*; @@ -34,11 +35,12 @@ public final class LocalDB implements EventListener { // Data private User user; - private Map users = Collections.synchronizedMap(new HashMap<>()); - private ObservableList chats = FXCollections.observableArrayList(); + private Map users = Collections.synchronizedMap(new HashMap<>()); + private ObservableList chats = FXCollections.observableArrayList(); private IDGenerator idGenerator; - private CacheMap cacheMap = new CacheMap(); + private CacheMap cacheMap = new CacheMap(); private String authToken; + private Set originalContacts = Set.of(); // Auto save timer private Timer autoSaver; @@ -163,7 +165,7 @@ public final class LocalDB implements EventListener { user.getContacts() .stream() .filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty()) - .map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c)) + .map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, (Group) c)) .forEach(chats::add); } @@ -197,7 +199,7 @@ public final class LocalDB implements EventListener { */ @Event(eventType = EnvoyCloseEvent.class, priority = 1000) private synchronized void save() { - EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database..."); + EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database..."); // Save users try { @@ -240,6 +242,31 @@ public final class LocalDB implements EventListener { getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get())); } + @Event(priority = 200) + private void onUserOperation(UserOperation operation) { + final var eventUser = operation.get(); + switch (operation.getOperationType()) { + case ADD: + getUsers().put(eventUser.getName(), eventUser); + Platform.runLater(() -> chats.add(0, new Chat(eventUser))); + break; + case REMOVE: + getChat(eventUser.getID()).ifPresent(chat -> chat.setDisabled(true)); + break; + } + } + + @Event + private void onGroupCreationResult(GroupCreationResult evt) { + final var newGroup = evt.get(); + + // The group creation was not successful + if (newGroup == null) return; + + // The group was successfully created + else Platform.runLater(() -> chats.add(new GroupChat(user, newGroup))); + } + @Event(priority = 150) private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); } @@ -296,6 +323,9 @@ public final class LocalDB implements EventListener { @Event(priority = 500) private void onOwnStatusChange(OwnStatusChange statusChange) { user.setStatus(statusChange.get()); } + @Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500) + private void onContactsChangedSinceLastLogin() { if (user != null) originalContacts = user.getContacts(); } + /** * @return a {@code Map} of all users stored locally with their * user names as keys @@ -323,6 +353,25 @@ public final class LocalDB implements EventListener { */ public Optional getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); } + /** + * Sets the given user as base of this {@code LocalDB}. + * Additionally compares his contacts with the ones returned by the server, in + * case they might have been deleted. + * + * @param user the user to set + * @since Envoy Client v0.2-alpha + */ + public void setUserAndMergeContacts(User user) { + this.user = user; + if (originalContacts.isEmpty()) return; + else { + + // mark all chats of deleted contacts + originalContacts.removeAll(user.getContacts()); + originalContacts.forEach(contact -> getChat(contact.getID()).ifPresent(chat -> chat.setDisabled(true))); + } + } + /** * @return all saved {@link Chat} objects that list the client user as the * sender diff --git a/client/src/main/java/envoy/client/net/Client.java b/client/src/main/java/envoy/client/net/Client.java index 0db43ef..e84388d 100644 --- a/client/src/main/java/envoy/client/net/Client.java +++ b/client/src/main/java/envoy/client/net/Client.java @@ -9,6 +9,7 @@ import envoy.client.data.*; import envoy.client.event.EnvoyCloseEvent; import envoy.data.*; import envoy.event.*; +import envoy.event.contact.ContactsChangedSinceLastLogin; import envoy.util.*; import dev.kske.eventbus.*; @@ -29,6 +30,7 @@ public final class Client implements EventListener, Closeable { private Socket socket; private Receiver receiver; private boolean online; + private boolean isHandShake; // Asynchronously initialized during handshake private volatile User sender; @@ -61,6 +63,7 @@ public final class Client implements EventListener, Closeable { */ public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully"); + isHandShake = true; // Establish TCP connection logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); @@ -92,14 +95,19 @@ public final class Client implements EventListener, Closeable { if (rejected) { socket.close(); receiver.removeAllProcessors(); + isHandShake = false; return; } - if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds"); + if (System.currentTimeMillis() - start > 5000) { + isHandShake = false; + throw new TimeoutException("Did not log in after 5 seconds"); + } Thread.sleep(500); } - online = true; + online = true; + isHandShake = false; logger.log(Level.INFO, "Handshake completed."); } @@ -146,7 +154,7 @@ public final class Client implements EventListener, Closeable { logger.log(Level.FINE, "Sending " + obj); try { SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); - } catch (IOException e) { + } catch (final IOException e) { throw new RuntimeException(e); } } @@ -174,7 +182,20 @@ public final class Client implements EventListener, Closeable { } @Event(eventType = HandshakeRejection.class, priority = 1000) - private void onHandshakeRejection() { rejected = true; } + private void onHandshakeRejection() { + rejected = true; + isHandShake = false; + } + + @Event(eventType = ContactsChangedSinceLastLogin.class, priority = 1000) + private void onContactsChangedSinceLastLogin() { + if (!isHandShake) { + logger.log(Level.WARNING, "Received a request to check contacts while not in the handshake"); + + // TODO: consume event once event consumption is implemented + return; + } + } @Override @Event(eventType = EnvoyCloseEvent.class, priority = 800) diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index a93bf81..cd4ddbc 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -179,7 +179,7 @@ public final class Startup extends Application { // Set client user in local database final var user = client.getSender(); - localDB.setUser(user); + localDB.setUserAndMergeContacts(user); // Initialize chats in local database try { diff --git a/client/src/main/java/envoy/client/ui/control/ChatControl.java b/client/src/main/java/envoy/client/ui/control/ChatControl.java index 353bdaf..6d4b535 100644 --- a/client/src/main/java/envoy/client/ui/control/ChatControl.java +++ b/client/src/main/java/envoy/client/ui/control/ChatControl.java @@ -59,6 +59,8 @@ public final class ChatControl extends HBox { vbox.getChildren().add(unreadMessagesLabel); getChildren().add(vbox); } - getStyleClass().add("list-element"); + + // Set background depending on whether it is disabled or not + getStyleClass().add(chat.isDisabled() ? "disabled-chat" : "list-element"); } } 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 4103a5b..4c98c76 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -37,7 +37,7 @@ import envoy.data.*; import envoy.data.Attachment.AttachmentType; import envoy.data.Message.MessageStatus; import envoy.event.*; -import envoy.event.contact.ContactOperation; +import envoy.event.contact.UserOperation; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; @@ -270,18 +270,22 @@ public final class ChatScene implements EventListener, Restorable { } @Event - private void onContactOperation(ContactOperation operation) { - final var contact = operation.get(); - switch (operation.getOperationType()) { - case ADD: - if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact); - final var chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact); - Platform.runLater(() -> ((ObservableList) chats.getSource()).add(0, chat)); - break; - case REMOVE: - Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact))); - break; - } + private void onUserOperation(UserOperation operation) { + // All ADD dependant logic resides in LocalDB + if (operation.getOperationType().equals(ElementOperation.REMOVE)) + if (currentChat != null && currentChat.getRecipient().equals(operation.get())) Platform.runLater(this::resetState); + } + + @Event + private void onGroupResize(GroupResize resize) { + final var chatFound = localDB.getChat(resize.getGroupID()); + Platform.runLater(() -> { + chatList.refresh(); + + // Update the top-bar status label if all conditions apply + if (currentChat != null && currentChat.getRecipient().equals(chatFound.get().getRecipient())) + chatFound.ifPresent(chat -> topBarStatusLabel.setText(chat.getRecipient().getContacts().size() + " members")); + }); } @Event(eventType = NoAttachments.class) @@ -297,8 +301,8 @@ public final class ChatScene implements EventListener, Restorable { }); } - @Event - private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(!result.get())); } + @Event(priority = 150) + private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(result.get() == null)); } @Event(eventType = ThemeChangeEvent.class) private void onThemeChange() { @@ -311,7 +315,6 @@ public final class ChatScene implements EventListener, Restorable { clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); messageList.setCellFactory(MessageListCell::new); - // TODO: cache image if (currentChat != null) if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); @@ -330,9 +333,11 @@ public final class ChatScene implements EventListener, Restorable { */ @FXML private void chatListClicked() { - if (chatList.getSelectionModel().isEmpty()) return; + if (currentChat != null && chatList.getSelectionModel().isEmpty()) return; + final var chat = chatList.getSelectionModel().getSelectedItem(); + if (chat == null) return; - final var user = chatList.getSelectionModel().getSelectedItem().getRecipient(); + final var user = chat.getRecipient(); if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes @@ -362,17 +367,24 @@ public final class ChatScene implements EventListener, Restorable { remainingChars .setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH)); } - messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled); - voiceButton.setDisable(!recorder.isSupported()); - attachmentButton.setDisable(false); + + // Enable or disable the necessary UI controls + final var chatEditable = currentChat == null || currentChat.isDisabled(); + messageTextArea.setDisable(chatEditable || postingPermanentlyDisabled); + voiceButton.setDisable(!recorder.isSupported() || chatEditable); + attachmentButton.setDisable(chatEditable); chatList.refresh(); + // Design the top bar if (currentChat != null) { topBarContactLabel.setText(currentChat.getRecipient().getName()); + topBarContactLabel.setVisible(true); if (currentChat.getRecipient() instanceof User) { final var status = ((User) currentChat.getRecipient()).getStatus().toString(); topBarStatusLabel.setText(status); + topBarStatusLabel.getStyleClass().clear(); topBarStatusLabel.getStyleClass().add(status.toLowerCase()); + topBarStatusLabel.setVisible(true); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); } else { topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members"); @@ -385,8 +397,11 @@ public final class ChatScene implements EventListener, Restorable { clip.setArcHeight(43); clip.setArcWidth(43); recipientProfilePic.setClip(clip); - + messageList.getStyleClass().clear(); messageSearchButton.setVisible(true); + + // Change the background of the message list if the chat is disabled + if (currentChat.isDisabled()) messageList.getStyleClass().add("disabled-chat"); } } @@ -664,9 +679,9 @@ public final class ChatScene implements EventListener, Restorable { Platform.runLater(() -> { chats.getSource().remove(currentChat); ((ObservableList) chats.getSource()).add(0, currentChat); - chatList.getSelectionModel().select(0); localDB.getChats().remove(currentChat); localDB.getChats().add(0, currentChat); + chatList.getSelectionModel().select(0); }); scrollToMessageListEnd(); @@ -711,7 +726,8 @@ public final class ChatScene implements EventListener, Restorable { * @since Envoy Client v0.1-beta */ private void updateAttachmentView(boolean visible) { - if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); + if (!(attachmentView.getImage() == null || attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE))) + attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setVisible(visible); } @@ -734,7 +750,62 @@ public final class ChatScene implements EventListener, Restorable { // Context menu actions @FXML - private void deleteContact() { try {} catch (final NullPointerException e) {} } + private void blockOrDeleteContact() { + final var selectedChat = chatList.getSelectionModel().getSelectedItem(); + + if (selectedChat == null) return; + // If a contact has already been blocked deletes this chat else only blocks him + if (selectedChat.isDisabled()) UserUtil.deleteContact(selectedChat.getRecipient()); + else UserUtil.blockContact(selectedChat.getRecipient()); + } + + /** + * Redesigns the UI when the {@link Chat} of the given contact has been marked + * as disabled. + * + * @param recipient the contact whose chat got disabled + * @param refreshChatList whether to refresh the chatList + * @since Envoy Client v0.3-beta + */ + public void disableChat(Contact recipient, boolean refreshChatList) { + if (refreshChatList) chatList.refresh(); + if (currentChat != null && currentChat.getRecipient().equals(recipient)) { + messageTextArea.setDisable(true); + voiceButton.setDisable(true); + attachmentButton.setDisable(true); + pendingAttachment = null; + messageList.getStyleClass().clear(); + messageList.getStyleClass().add("disabled-chat"); + } + } + + /** + * Resets every component back to its inital state before a chat was selected. + * + * @since Envoy Client v0.3-beta + */ + public void resetState() { + currentChat = null; + chatList.getSelectionModel().clearSelection(); + messageList.getItems().clear(); + messageTextArea.setDisable(true); + attachmentView.setImage(null); + topBarContactLabel.setVisible(false); + topBarStatusLabel.setVisible(false); + messageSearchButton.setVisible(false); + messageTextArea.clear(); + messageTextArea.setDisable(true); + attachmentButton.setDisable(true); + voiceButton.setDisable(true); + remainingChars.setVisible(false); + pendingAttachment = null; + recipientProfilePic.setImage(null); + + // It is a valid case that cancel can cause a NullPointerException + try { + recorder.cancel(); + } catch (final NullPointerException e) {} + } @FXML private void copyAndPostMessage() { diff --git a/client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java b/client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java index b573345..380c549 100644 --- a/client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java +++ b/client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java @@ -63,7 +63,7 @@ public class ContactSearchTab implements EventListener { } @Event - private void onContactOperation(ContactOperation operation) { + private void onUserOperation(UserOperation operation) { final var contact = operation.get(); if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> { userList.getItems().remove(contact); @@ -96,7 +96,7 @@ public class ContactSearchTab implements EventListener { } /** - * Sends an {@link ContactOperation} for the selected user to the + * Sends an {@link UserOperation} for the selected user to the * server. * * @since Envoy Client v0.1-beta @@ -114,7 +114,7 @@ public class ContactSearchTab implements EventListener { private void addAsContact() { // Sends the event to the server - final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD); + final var event = new UserOperation(currentlySelectedUser, ElementOperation.ADD); client.send(event); // Removes the chosen user and updates the UI diff --git a/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java b/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java index 23ec485..d262214 100644 --- a/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java +++ b/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java @@ -16,7 +16,7 @@ import envoy.client.ui.control.*; import envoy.client.ui.listcell.ListCellFactory; import envoy.data.*; import envoy.event.GroupCreation; -import envoy.event.contact.ContactOperation; +import envoy.event.contact.UserOperation; import envoy.util.Bounds; import dev.kske.eventbus.*; @@ -234,8 +234,8 @@ public class GroupCreationTab implements EventListener { } @Event - private void onContactOperation(ContactOperation operation) { - if (operation.get() instanceof User) Platform.runLater(() -> { + private void onUserOperation(UserOperation operation) { + Platform.runLater(() -> { switch (operation.getOperationType()) { case ADD: userList.getItems().add((User) operation.get()); diff --git a/client/src/main/java/envoy/client/util/UserUtil.java b/client/src/main/java/envoy/client/util/UserUtil.java index 4f1b37b..dc34455 100644 --- a/client/src/main/java/envoy/client/util/UserUtil.java +++ b/client/src/main/java/envoy/client/util/UserUtil.java @@ -5,12 +5,15 @@ import java.util.logging.Level; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import envoy.client.data.Context; +import envoy.client.data.*; import envoy.client.event.*; import envoy.client.helper.*; import envoy.client.ui.SceneContext.SceneInfo; +import envoy.client.ui.controller.ChatScene; +import envoy.data.*; import envoy.data.User.UserStatus; -import envoy.event.UserStatusChange; +import envoy.event.*; +import envoy.event.contact.UserOperation; import envoy.util.EnvoyLog; import dev.kske.eventbus.EventBus; @@ -23,6 +26,8 @@ import dev.kske.eventbus.EventBus; */ public final class UserUtil { + private static final Context context = Context.getInstance(); + private UserUtil() {} /** @@ -40,7 +45,7 @@ public final class UserUtil { EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested"); EventBus.getInstance().dispatch(new EnvoyCloseEvent()); EventBus.getInstance().dispatch(new Logout()); - Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE); + context.getSceneContext().load(SceneInfo.LOGIN_SCENE); }); } @@ -54,11 +59,56 @@ public final class UserUtil { public static void changeStatus(UserStatus newStatus) { // Sending the already active status is a valid action - if (newStatus.equals(Context.getInstance().getLocalDB().getUser().getStatus())) return; + if (newStatus.equals(context.getLocalDB().getUser().getStatus())) return; else { EventBus.getInstance().dispatch(new OwnStatusChange(newStatus)); - if (Context.getInstance().getClient().isOnline()) - Context.getInstance().getClient().send(new UserStatusChange(Context.getInstance().getLocalDB().getUser().getID(), newStatus)); + if (context.getClient().isOnline()) context.getClient().send(new UserStatusChange(context.getLocalDB().getUser().getID(), newStatus)); + } + } + + /** + * Removes the given contact. Should not be confused with the method that is + * called when the server reports that a contact has been deleted while the user + * was offline. + * + * @param block the contact that should be removed + * @since Envoy Client v0.3-beta + * @see LocalDB#setUserAndMergeContacts(envoy.data.User) + */ + public static void blockContact(Contact block) { + if (!context.getClient().isOnline() || block == null) return; + else { + final var alert = new Alert(AlertType.CONFIRMATION); + alert.setContentText("Are you sure you want to block " + block.getName() + "?"); + AlertHelper.confirmAction(alert, () -> { + context.getClient() + .send(block instanceof User ? new UserOperation((User) block, ElementOperation.REMOVE) + : new GroupResize(context.getLocalDB().getUser(), (Group) block, ElementOperation.REMOVE)); + context.getLocalDB().getChat(block.getID()).ifPresent(chat -> chat.setDisabled(true)); + final var controller = context.getSceneContext().getController(); + if (controller instanceof ChatScene) ((ChatScene) controller).disableChat(block, true); + }); + } + } + + /** + * Deletes the given contact with all his messages entirely. + * + * @param delete the contact to delete + * @since Envoy Client v0.3-beta + */ + public static void deleteContact(Contact delete) { + if (!context.getClient().isOnline() || delete == null) return; + else { + final var alert = new Alert(AlertType.CONFIRMATION); + alert.setContentText("Are you sure you want to delete " + delete.getName() + + " entirely? All messages with this contact will be deleted. This action cannot be undone."); + AlertHelper.confirmAction(alert, () -> { + context.getLocalDB().getUsers().remove(delete.getName()); + context.getLocalDB().getChats().removeIf(chat -> chat.getRecipient().equals(delete)); + if (context.getSceneContext().getController() instanceof ChatScene) + ((ChatScene) context.getSceneContext().getController()).resetState(); + }); } } } diff --git a/client/src/main/resources/css/base.css b/client/src/main/resources/css/base.css index 28d276c..bfe4da2 100644 --- a/client/src/main/resources/css/base.css +++ b/client/src/main/resources/css/base.css @@ -139,20 +139,25 @@ .tab-pane { -fx-tab-max-height: 0.0 ; -} +} + .tab-pane .tab-header-area { visibility: hidden ; -fx-padding: -20.0 0.0 0.0 0.0; } +.disabled-chat { + -fx-background-color: #F04000; +} + #quick-select-list .scroll-bar:horizontal{ - -fx-pref-height: 0; - -fx-max-height: 0; - -fx-min-height: 0; + -fx-pref-height: 0.0; + -fx-max-height: 0.0; + -fx-min-height: 0.0; } #quick-select-list .scroll-bar:vertical{ - -fx-pref-width: 0; - -fx-max-width: 0; - -fx-min-width: 0; + -fx-pref-width: 0.0; + -fx-max-width: 0.0; + -fx-min-width: 0.0; } diff --git a/client/src/main/resources/fxml/ChatScene.fxml b/client/src/main/resources/fxml/ChatScene.fxml index 8221f19..8368e4b 100644 --- a/client/src/main/resources/fxml/ChatScene.fxml +++ b/client/src/main/resources/fxml/ChatScene.fxml @@ -130,7 +130,7 @@ diff --git a/common/src/main/java/envoy/event/GroupCreation.java b/common/src/main/java/envoy/event/GroupCreation.java index 02270ee..ffd7851 100644 --- a/common/src/main/java/envoy/event/GroupCreation.java +++ b/common/src/main/java/envoy/event/GroupCreation.java @@ -17,14 +17,14 @@ public final class GroupCreation extends Event { private static final long serialVersionUID = 0L; /** - * @param value the name of this group at creation time + * @param name the name of this group at creation time * @param initialMemberIDs the IDs of all {@link User}s that should be group * members from the beginning on (excluding the creator * of this group) * @since Envoy Common v0.1-beta */ - public GroupCreation(String value, Set initialMemberIDs) { - super(value); + public GroupCreation(String name, Set initialMemberIDs) { + super(name); this.initialMemberIDs = initialMemberIDs != null ? initialMemberIDs : new HashSet<>(); } diff --git a/common/src/main/java/envoy/event/GroupCreationResult.java b/common/src/main/java/envoy/event/GroupCreationResult.java index 3944ec2..14ae2f6 100644 --- a/common/src/main/java/envoy/event/GroupCreationResult.java +++ b/common/src/main/java/envoy/event/GroupCreationResult.java @@ -1,5 +1,7 @@ package envoy.event; +import envoy.data.Group; + /** * Used to communicate with a client that his request to create a group might * have been rejected as it might be disabled on his current server. @@ -7,15 +9,23 @@ package envoy.event; * @author Leon Hofmeister * @since Envoy Common v0.2-beta */ -public class GroupCreationResult extends Event { +public class GroupCreationResult extends Event { private static final long serialVersionUID = 1L; /** - * Creates a new {@code GroupCreationResult}. + * Creates a new {@code GroupCreationResult} that implies the failure of this + * {@link GroupCreationResult}. * - * @param success whether the GroupCreation sent before was successful * @since Envoy Common v0.2-beta */ - public GroupCreationResult(boolean success) { super(success); } + public GroupCreationResult() { super(null); } + + /** + * Creates a new {@code GroupCreationResult}. + * + * @param resultGroup the group the server created + * @since Envoy Common v0.2-beta + */ + public GroupCreationResult(Group resultGroup) { super(resultGroup); } } diff --git a/common/src/main/java/envoy/event/GroupResize.java b/common/src/main/java/envoy/event/GroupResize.java index c8e8c76..4df5f97 100644 --- a/common/src/main/java/envoy/event/GroupResize.java +++ b/common/src/main/java/envoy/event/GroupResize.java @@ -33,13 +33,12 @@ public final class GroupResize extends Event { */ public GroupResize(User user, Group group, ElementOperation operation) { super(user); - this.operation = operation; - if (group.getContacts().contains(user)) { - if (operation.equals(ADD)) - throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group)); - } else if (operation.equals(REMOVE)) - throw new IllegalArgumentException(String.format("Cannot remove %s from %s!", user, group)); - groupID = group.getID(); + this.operation = operation; + final var contained = group.getContacts().contains(user); + if (contained && operation.equals(ADD)) + throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group)); + else if (operation.equals(REMOVE) && !contained) throw new IllegalArgumentException(String.format("Cannot remove %s from %s!", user, group)); + groupID = group.getID(); } /** @@ -72,5 +71,5 @@ public final class GroupResize extends Event { } @Override - public String toString() { return String.format("GroupResize[userid=%d,groupid=%d,operation=%s]", get(), groupID, operation); } + public String toString() { return String.format("GroupResize[user=%s,groupid=%d,operation=%s]", get(), groupID, operation); } } diff --git a/common/src/main/java/envoy/event/contact/ContactDeletionSinceLastLogin.java b/common/src/main/java/envoy/event/contact/ContactsChangedSinceLastLogin.java similarity index 82% rename from common/src/main/java/envoy/event/contact/ContactDeletionSinceLastLogin.java rename to common/src/main/java/envoy/event/contact/ContactsChangedSinceLastLogin.java index cda2483..f8734e8 100644 --- a/common/src/main/java/envoy/event/contact/ContactDeletionSinceLastLogin.java +++ b/common/src/main/java/envoy/event/contact/ContactsChangedSinceLastLogin.java @@ -9,7 +9,7 @@ import envoy.event.Event.Valueless; * @author Leon Hofmeister * @since Envoy Common v0.3-beta */ -public class ContactDeletionSinceLastLogin extends Valueless { +public class ContactsChangedSinceLastLogin extends Valueless { private static final long serialVersionUID = 1L; } diff --git a/common/src/main/java/envoy/event/contact/ContactOperation.java b/common/src/main/java/envoy/event/contact/UserOperation.java similarity index 56% rename from common/src/main/java/envoy/event/contact/ContactOperation.java rename to common/src/main/java/envoy/event/contact/UserOperation.java index a31d147..666183e 100644 --- a/common/src/main/java/envoy/event/contact/ContactOperation.java +++ b/common/src/main/java/envoy/event/contact/UserOperation.java @@ -1,35 +1,38 @@ package envoy.event.contact; -import envoy.data.Contact; +import envoy.data.User; import envoy.event.*; /** * Signifies the modification of a contact list. * * @author Maximilian Käfer - * @since Envoy Common v0.2-alpha + * @since Envoy Common v0.3-beta */ -public final class ContactOperation extends Event { +public final class UserOperation extends Event { private final ElementOperation operationType; private static final long serialVersionUID = 1L; /** - * Initializes a {@link ContactOperation}. + * Initializes a {@link UserOperation}. * * @param contact the user on which the operation is performed * @param operationType the type of operation to perform - * @since Envoy Common v0.2-alpha + * @since Envoy Common v0.3-beta */ - public ContactOperation(Contact contact, ElementOperation operationType) { + public UserOperation(User contact, ElementOperation operationType) { super(contact); this.operationType = operationType; } /** * @return the type of operation to perform - * @since Envoy Common v0.2-alpha + * @since Envoy Common v0.3-beta */ public ElementOperation getOperationType() { return operationType; } + + @Override + public String toString() { return String.format("%s[contact=%s, operation=%s]", UserOperation.class.getSimpleName(), value, operationType); } } diff --git a/server/src/main/java/envoy/server/processors/ContactOperationProcessor.java b/server/src/main/java/envoy/server/processors/ContactOperationProcessor.java index 8049ed6..1fa74fb 100755 --- a/server/src/main/java/envoy/server/processors/ContactOperationProcessor.java +++ b/server/src/main/java/envoy/server/processors/ContactOperationProcessor.java @@ -4,7 +4,7 @@ import java.time.Instant; import java.util.logging.*; import envoy.event.ElementOperation; -import envoy.event.contact.ContactOperation; +import envoy.event.contact.UserOperation; import envoy.server.data.*; import envoy.server.net.*; import envoy.util.EnvoyLog; @@ -13,14 +13,14 @@ import envoy.util.EnvoyLog; * @author Kai S. K. Engelbart * @since Envoy Server Standalone v0.1-alpha */ -public final class ContactOperationProcessor implements ObjectProcessor { +public final class ContactOperationProcessor implements ObjectProcessor { private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final Logger logger = EnvoyLog.getLogger(ContactOperationProcessor.class); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); @Override - public void process(ContactOperation evt, long socketId, ObjectWriteProxy writeProxy) { + public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) { final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId); final long contactID = evt.get().getID(); final var sender = persistenceManager.getUserByID(userID); @@ -31,7 +31,7 @@ public final class ContactOperationProcessor implements ObjectProcessor ((User) c).setLatestContactDeletion(Instant.now())); } } diff --git a/server/src/main/java/envoy/server/processors/GroupCreationProcessor.java b/server/src/main/java/envoy/server/processors/GroupCreationProcessor.java index 8f40af2..3540238 100644 --- a/server/src/main/java/envoy/server/processors/GroupCreationProcessor.java +++ b/server/src/main/java/envoy/server/processors/GroupCreationProcessor.java @@ -5,7 +5,6 @@ import static envoy.server.Startup.config; import java.util.HashSet; import envoy.event.*; -import envoy.event.contact.ContactOperation; import envoy.server.data.*; import envoy.server.net.*; @@ -21,8 +20,10 @@ public final class GroupCreationProcessor implements ObjectProcessor()); @@ -31,11 +32,13 @@ public final class GroupCreationProcessor implements ObjectProcessor c.getContacts().add(group)); group.getContacts().add(persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID))); persistenceManager.addContact(group); + final var resultGroup = group.toCommon(); group.getContacts() .stream() .map(Contact::getID) .filter(connectionManager::isOnline) .map(connectionManager::getSocketID) - .forEach(memberSocketID -> writeProxy.write(memberSocketID, new ContactOperation(group.toCommon(), ElementOperation.ADD))); + .forEach(memberSocketID -> writeProxy.write(memberSocketID, new GroupCreationResult(resultGroup))); + writeProxy.write(socketID, new GroupCreationResult(resultGroup)); } } diff --git a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java index 2526de3..ba484db 100755 --- a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java +++ b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java @@ -13,7 +13,7 @@ import javax.persistence.NoResultException; import envoy.data.LoginCredentials; import envoy.event.*; -import envoy.event.contact.ContactDeletionSinceLastLogin; +import envoy.event.contact.ContactsChangedSinceLastLogin; import envoy.server.data.*; import envoy.server.net.*; import envoy.server.util.*; @@ -207,11 +207,10 @@ public final class LoginCredentialProcessor implements ObjectProcessor