diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index 892b5ca..c62fd63 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -22,20 +22,21 @@ import envoy.client.net.WriteProxy; */ public class Chat implements Serializable { - protected boolean disabled; + protected transient ObservableList messages = FXCollections.observableArrayList(); + + protected final Contact recipient; + + protected boolean disabled; + protected boolean underlyingContactDeleted; /** * Stores the last time an {@link envoy.event.IsTyping} event has been sent. */ protected transient long lastWritingEvent; - protected transient ObservableList messages = FXCollections.observableArrayList(); - protected int unreadAmount; protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(); - protected final Contact recipient; - private static final long serialVersionUID = 2L; /** diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index 2ae3570..da368ba 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -248,6 +248,10 @@ public final class LocalDB implements EventListener { */ @Event(eventType = EnvoyCloseEvent.class, priority = 500) private synchronized void save() { + + // Stop saving if this account has been deleted + if (userFile == null) + return; EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database..."); // Save users @@ -273,6 +277,29 @@ public final class LocalDB implements EventListener { } } + /** + * Deletes any local remnant of this user. + * + * @since Envoy Client v0.3-beta + */ + public void delete() { + try { + + // Save ID generator - can be used for other users in that db + if (hasIDGenerator()) + SerializationUtils.write(idGeneratorFile, idGenerator); + } catch (final IOException e) { + EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", + e); + } + if (lastLoginFile != null) + lastLoginFile.delete(); + userFile.delete(); + users.remove(user.getName()); + userFile = null; + onLogout(); + } + @Event(priority = 500) private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) @@ -404,6 +431,14 @@ public final class LocalDB implements EventListener { getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); } + @Event(priority = 500) + private void onAccountDeletion(AccountDeletion deletion) { + if (user.getID() == deletion.get()) + logger.log(Level.WARNING, + "I have been informed by the server that I have been deleted without even knowing it..."); + getChat(deletion.get()).ifPresent(chat -> chat.setDisabled(true)); + } + /** * @return a {@code Map} of all users stored locally with their user names as keys * @since Envoy Client v0.2-alpha diff --git a/client/src/main/java/envoy/client/event/AccountDeletion.java b/client/src/main/java/envoy/client/event/AccountDeletion.java new file mode 100644 index 0000000..ef73462 --- /dev/null +++ b/client/src/main/java/envoy/client/event/AccountDeletion.java @@ -0,0 +1,22 @@ +package envoy.client.event; + +import envoy.event.Event; + +/** + * Signifies the deletion of an account. + * + * @author Leon Hofmeister + * @since Envoy Common v0.3-beta + */ +public class AccountDeletion extends Event { + + private static final long serialVersionUID = 1L; + + /** + * @param value the ID of the contact that was deleted + * @since Envoy Common v0.3-beta + */ + public AccountDeletion(Long value) { + super(value); + } +} 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 2242d25..8dc48ef 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -6,6 +6,7 @@ import java.io.*; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Map; import java.util.logging.*; import javafx.animation.RotateTransition; @@ -31,12 +32,13 @@ import envoy.data.*; import envoy.data.Attachment.AttachmentType; import envoy.data.Message.MessageStatus; import envoy.event.*; -import envoy.event.contact.UserOperation; +import envoy.event.contact.*; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; import envoy.client.data.*; import envoy.client.data.audio.AudioRecorder; +import envoy.client.data.shortcuts.KeyboardMapping; import envoy.client.event.*; import envoy.client.net.*; import envoy.client.ui.*; @@ -51,7 +53,7 @@ import envoy.client.util.*; * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ -public final class ChatScene implements EventListener, Restorable { +public final class ChatScene implements EventListener, Restorable, KeyboardMapping { @FXML private ListView messageList; @@ -346,6 +348,11 @@ public final class ChatScene implements EventListener, Restorable { eventBus.removeListener(this); } + @Event(eventType = AccountDeletion.class) + private void onAccountDeletion() { + Platform.runLater(chatList::refresh); + } + @Override public void onRestore() { updateRemainingCharsLabel(); @@ -871,4 +878,25 @@ public final class ChatScene implements EventListener, Restorable { : c -> c.getRecipient().getName().toLowerCase() .contains(contactSearch.getText().toLowerCase())); } + + @Override + public Map getKeyboardShortcuts() { + return Map.of( + + // Delete text before the caret with "Control" + U + new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> { + messageTextArea + .setText( + messageTextArea.getText().substring(messageTextArea.getCaretPosition())); + checkPostConditions(false); + + // Delete text after the caret with "Control" + K + }, new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> { + messageTextArea + .setText( + messageTextArea.getText().substring(0, messageTextArea.getCaretPosition())); + checkPostConditions(false); + messageTextArea.positionCaret(messageTextArea.getText().length()); + }); + } } 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 39f414d..f1f1b58 100644 --- a/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java +++ b/client/src/main/java/envoy/client/ui/controller/GroupCreationTab.java @@ -14,11 +14,11 @@ import dev.kske.eventbus.*; import envoy.data.*; import envoy.event.GroupCreation; -import envoy.event.contact.UserOperation; +import envoy.event.contact.*; import envoy.util.Bounds; import envoy.client.data.*; -import envoy.client.event.BackEvent; +import envoy.client.event.*; import envoy.client.ui.control.*; import envoy.client.ui.listcell.ListCellFactory; @@ -252,4 +252,10 @@ public class GroupCreationTab implements EventListener { } }); } + + @Event + private void onAccountDeletion(AccountDeletion deletion) { + final var deletedID = deletion.get(); + Platform.runLater(() -> userList.getItems().removeIf(user -> (user.getID() == deletedID))); + } } diff --git a/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java b/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java index c3790a2..3a35061 100644 --- a/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java +++ b/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java @@ -13,6 +13,7 @@ import javafx.scene.image.*; import javafx.scene.input.InputEvent; import javafx.scene.layout.HBox; import javafx.stage.FileChooser; +import javafx.util.Duration; import dev.kske.eventbus.EventBus; @@ -20,7 +21,7 @@ import envoy.event.*; import envoy.util.*; import envoy.client.ui.control.ProfilePicImageView; -import envoy.client.util.IconUtil; +import envoy.client.util.*; /** * @author Leon Hofmeister @@ -38,6 +39,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane { private final PasswordField newPasswordField = new PasswordField(); private final PasswordField repeatNewPasswordField = new PasswordField(); private final Button saveButton = new Button("Save"); + private final Button deleteAccountButton = new Button("Delete Account (Locally)"); private static final EventBus eventBus = EventBus.getInstance(); private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); @@ -112,16 +114,19 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane { final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField }; - final EventHandler passwordEntered = e -> { - newPassword = - newPasswordField.getText(); - validPassword = newPassword - .equals( - repeatNewPasswordField - .getText()) - && !newPasswordField - .getText().isBlank(); - }; + final EventHandler passwordEntered = + e -> { + newPassword = + newPasswordField + .getText(); + validPassword = + newPassword.equals( + repeatNewPasswordField + .getText()) + && !newPasswordField + .getText() + .isBlank(); + }; newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered); repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered); @@ -140,6 +145,18 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane { .setOnAction(e -> save(currentPasswordField.getText())); saveButton.setAlignment(Pos.BOTTOM_RIGHT); getChildren().add(saveButton); + + // Displaying the delete account button + deleteAccountButton.setAlignment(Pos.BASELINE_CENTER); + deleteAccountButton.setOnAction(e -> UserUtil.deleteAccount()); + deleteAccountButton.setText("Delete Account (locally)"); + deleteAccountButton.setPrefHeight(25); + deleteAccountButton.getStyleClass().clear(); + deleteAccountButton.getStyleClass().add("danger-button"); + final var tooltip = new Tooltip("Remote deletion is currently unsupported."); + tooltip.setShowDelay(Duration.millis(100)); + deleteAccountButton.setTooltip(tooltip); + getChildren().add(deleteAccountButton); } /** diff --git a/client/src/main/java/envoy/client/util/UserUtil.java b/client/src/main/java/envoy/client/util/UserUtil.java index 27078cc..bc7d7fe 100644 --- a/client/src/main/java/envoy/client/util/UserUtil.java +++ b/client/src/main/java/envoy/client/util/UserUtil.java @@ -2,7 +2,7 @@ package envoy.client.util; import java.util.logging.*; -import javafx.scene.control.Alert; +import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import dev.kske.eventbus.EventBus; @@ -121,4 +121,35 @@ public final class UserUtil { }); } } + + /** + * Deletes anything pointing to this user, independent of client or server. Will do nothing if + * the client is currently offline. + * + * @since Envoy Client v0.3-beta + */ + public static void deleteAccount() { + + // Show the first wall of defense, if not disabled by the user + final var outerAlert = new Alert(AlertType.CONFIRMATION); + outerAlert.setContentText( + "Are you sure you want to delete your account entirely? This action can seriously not be undone."); + outerAlert.setTitle("Delete Account?"); + AlertHelper.confirmAction(outerAlert, () -> { + + // Show the final wall of defense in every case + final var lastAlert = new Alert(AlertType.WARNING, + "Do you REALLY want to delete your account? Last Warning. Proceed?", + ButtonType.CANCEL, ButtonType.OK); + lastAlert.setTitle("Delete Account?"); + lastAlert.showAndWait().filter(ButtonType.OK::equals).ifPresent(b2 -> { + + // Delete the account + // TODO: Notify server of account deletion + context.getLocalDB().delete(); + logger.log(Level.INFO, "The user just deleted his account. Goodbye."); + ShutdownHelper.exit(true); + }); + }); + } } diff --git a/client/src/main/resources/css/base.css b/client/src/main/resources/css/base.css index 4c46161..fdfc1b2 100644 --- a/client/src/main/resources/css/base.css +++ b/client/src/main/resources/css/base.css @@ -70,6 +70,17 @@ -fx-text-fill: gray; } +.danger-button { + -fx-background-color: red; + -fx-text-fill: white; + -fx-background-radius: 0.2em; +} + +.danger-button:hover { + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + .received-message { -fx-alignment: center-left; -fx-background-radius: 1.3em; diff --git a/client/src/main/resources/css/light.css b/client/src/main/resources/css/light.css index 7cd3ac1..81ac69b 100644 --- a/client/src/main/resources/css/light.css +++ b/client/src/main/resources/css/light.css @@ -30,6 +30,10 @@ -fx-background-color: black; } +.tooltip { + -fx-text-fill: black; +} + #login-input-field { -fx-border-color: black; } diff --git a/server/src/main/java/envoy/server/data/PersistenceManager.java b/server/src/main/java/envoy/server/data/PersistenceManager.java index c79296b..23f19d9 100755 --- a/server/src/main/java/envoy/server/data/PersistenceManager.java +++ b/server/src/main/java/envoy/server/data/PersistenceManager.java @@ -121,8 +121,9 @@ public final class PersistenceManager { transaction(() -> { // Remove this contact from the contact list of his contacts - for (final var remainingContact : contact.getContacts()) + for (final var remainingContact : contact.contacts) remainingContact.getContacts().remove(contact); + contact.contacts.clear(); }); remove(contact); } diff --git a/server/src/main/java/envoy/server/net/ConnectionManager.java b/server/src/main/java/envoy/server/net/ConnectionManager.java index 4a8ea2c..b2a046b 100755 --- a/server/src/main/java/envoy/server/net/ConnectionManager.java +++ b/server/src/main/java/envoy/server/net/ConnectionManager.java @@ -49,8 +49,10 @@ public final class ConnectionManager implements ISocketIdListener { // Notify contacts of this users offline-going final envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); - user.setLastSeen(Instant.now()); - UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); + if (user != null) { + user.setLastSeen(Instant.now()); + UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); + } // Remove the socket sockets.entrySet().removeIf(e -> e.getValue() == socketID); diff --git a/server/src/main/java/envoy/server/processors/GroupMessageStatusChangeProcessor.java b/server/src/main/java/envoy/server/processors/GroupMessageStatusChangeProcessor.java index fffe41b..b9482f1 100644 --- a/server/src/main/java/envoy/server/processors/GroupMessageStatusChangeProcessor.java +++ b/server/src/main/java/envoy/server/processors/GroupMessageStatusChangeProcessor.java @@ -38,6 +38,8 @@ public final class GroupMessageStatusChangeProcessor } GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID()); + if (gmsg == null) + return; // Any other status than READ is not supposed to be sent to the server if (statusChange.get() != MessageStatus.READ) { diff --git a/server/src/main/java/envoy/server/processors/GroupResizeProcessor.java b/server/src/main/java/envoy/server/processors/GroupResizeProcessor.java index 9c42d3d..92b3f84 100644 --- a/server/src/main/java/envoy/server/processors/GroupResizeProcessor.java +++ b/server/src/main/java/envoy/server/processors/GroupResizeProcessor.java @@ -24,6 +24,10 @@ public final class GroupResizeProcessor implements ObjectProcessor final var group = persistenceManager.getGroupByID(groupResize.getGroupID()); final var sender = persistenceManager.getUserByID(groupResize.get().getID()); + // TODO: Inform the sender that this group has already been deleted + if (group == null) + return; + // Perform the desired operation switch (groupResize.getOperation()) { case ADD: diff --git a/server/src/main/java/envoy/server/processors/MessageStatusChangeProcessor.java b/server/src/main/java/envoy/server/processors/MessageStatusChangeProcessor.java index d954af5..efc407b 100755 --- a/server/src/main/java/envoy/server/processors/MessageStatusChangeProcessor.java +++ b/server/src/main/java/envoy/server/processors/MessageStatusChangeProcessor.java @@ -32,6 +32,8 @@ public final class MessageStatusChangeProcessor implements ObjectProcessor