Merge pull request 'Add Local Account Deletion' (#108) from f/account-deletion into develop

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/108
Reviewed-by: kske <kai@kske.dev>
This commit is contained in:
Kai S. K. Engelbart 2020-10-31 16:57:09 +01:00
commit cd7793a589
Signed by: Käfer & Engelbart Git
GPG Key ID: 70F2F9206EDC1FCE
15 changed files with 200 additions and 28 deletions

View File

@ -22,20 +22,21 @@ import envoy.client.net.WriteProxy;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected boolean disabled; protected transient ObservableList<Message> 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. * Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/ */
protected transient long lastWritingEvent; protected transient long lastWritingEvent;
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount; protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(); protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
protected final Contact recipient;
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**

View File

@ -248,6 +248,10 @@ public final class LocalDB implements EventListener {
*/ */
@Event(eventType = EnvoyCloseEvent.class, priority = 500) @Event(eventType = EnvoyCloseEvent.class, priority = 500)
private synchronized void save() { 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..."); EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
// Save users // 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) @Event(priority = 500)
private void onMessage(Message msg) { private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT) if (msg.getStatus() == MessageStatus.SENT)
@ -404,6 +431,14 @@ public final class LocalDB implements EventListener {
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); 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<String, User>} of all users stored locally with their user names as keys * @return a {@code Map<String, User>} of all users stored locally with their user names as keys
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha

View File

@ -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<Long> {
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);
}
}

View File

@ -6,6 +6,7 @@ import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.*; import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
@ -31,12 +32,13 @@ import envoy.data.*;
import envoy.data.Attachment.AttachmentType; import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.net.*; import envoy.client.net.*;
import envoy.client.ui.*; import envoy.client.ui.*;
@ -51,7 +53,7 @@ import envoy.client.util.*;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ChatScene implements EventListener, Restorable { public final class ChatScene implements EventListener, Restorable, KeyboardMapping {
@FXML @FXML
private ListView<Message> messageList; private ListView<Message> messageList;
@ -346,6 +348,11 @@ public final class ChatScene implements EventListener, Restorable {
eventBus.removeListener(this); eventBus.removeListener(this);
} }
@Event(eventType = AccountDeletion.class)
private void onAccountDeletion() {
Platform.runLater(chatList::refresh);
}
@Override @Override
public void onRestore() { public void onRestore() {
updateRemainingCharsLabel(); updateRemainingCharsLabel();
@ -871,4 +878,25 @@ public final class ChatScene implements EventListener, Restorable {
: c -> c.getRecipient().getName().toLowerCase() : c -> c.getRecipient().getName().toLowerCase()
.contains(contactSearch.getText().toLowerCase())); .contains(contactSearch.getText().toLowerCase()));
} }
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.<KeyCombination, Runnable>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());
});
}
} }

View File

@ -14,11 +14,11 @@ import dev.kske.eventbus.*;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupCreation; import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.util.Bounds; import envoy.util.Bounds;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.BackEvent; import envoy.client.event.*;
import envoy.client.ui.control.*; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory; 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)));
}
} }

View File

@ -13,6 +13,7 @@ import javafx.scene.image.*;
import javafx.scene.input.InputEvent; import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -20,7 +21,7 @@ import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import envoy.client.ui.control.ProfilePicImageView; import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil; import envoy.client.util.*;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -38,6 +39,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
private final PasswordField newPasswordField = new PasswordField(); private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField(); private final PasswordField repeatNewPasswordField = new PasswordField();
private final Button saveButton = new Button("Save"); 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 EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
@ -112,16 +114,19 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
final PasswordField[] passwordFields = final PasswordField[] passwordFields =
{ currentPasswordField, newPasswordField, repeatNewPasswordField }; { currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> { final EventHandler<? super InputEvent> passwordEntered =
newPassword = e -> {
newPasswordField.getText(); newPassword =
validPassword = newPassword newPasswordField
.equals( .getText();
repeatNewPasswordField validPassword =
.getText()) newPassword.equals(
&& !newPasswordField repeatNewPasswordField
.getText().isBlank(); .getText())
}; && !newPasswordField
.getText()
.isBlank();
};
newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered);
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered); repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
@ -140,6 +145,18 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
.setOnAction(e -> save(currentPasswordField.getText())); .setOnAction(e -> save(currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT); saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton); 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);
} }
/** /**

View File

@ -2,7 +2,7 @@ package envoy.client.util;
import java.util.logging.*; import java.util.logging.*;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.EventBus; 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);
});
});
}
} }

View File

@ -70,6 +70,17 @@
-fx-text-fill: gray; -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 { .received-message {
-fx-alignment: center-left; -fx-alignment: center-left;
-fx-background-radius: 1.3em; -fx-background-radius: 1.3em;

View File

@ -30,6 +30,10 @@
-fx-background-color: black; -fx-background-color: black;
} }
.tooltip {
-fx-text-fill: black;
}
#login-input-field { #login-input-field {
-fx-border-color: black; -fx-border-color: black;
} }

View File

@ -121,8 +121,9 @@ public final class PersistenceManager {
transaction(() -> { transaction(() -> {
// Remove this contact from the contact list of his contacts // 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); remainingContact.getContacts().remove(contact);
contact.contacts.clear();
}); });
remove(contact); remove(contact);
} }

View File

@ -49,8 +49,10 @@ public final class ConnectionManager implements ISocketIdListener {
// Notify contacts of this users offline-going // Notify contacts of this users offline-going
final envoy.server.data.User user = final envoy.server.data.User user =
PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setLastSeen(Instant.now()); if (user != null) {
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
}
// Remove the socket // Remove the socket
sockets.entrySet().removeIf(e -> e.getValue() == socketID); sockets.entrySet().removeIf(e -> e.getValue() == socketID);

View File

@ -38,6 +38,8 @@ public final class GroupMessageStatusChangeProcessor
} }
GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID()); 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 // Any other status than READ is not supposed to be sent to the server
if (statusChange.get() != MessageStatus.READ) { if (statusChange.get() != MessageStatus.READ) {

View File

@ -24,6 +24,10 @@ public final class GroupResizeProcessor implements ObjectProcessor<GroupResize>
final var group = persistenceManager.getGroupByID(groupResize.getGroupID()); final var group = persistenceManager.getGroupByID(groupResize.getGroupID());
final var sender = persistenceManager.getUserByID(groupResize.get().getID()); 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 // Perform the desired operation
switch (groupResize.getOperation()) { switch (groupResize.getOperation()) {
case ADD: case ADD:

View File

@ -32,6 +32,8 @@ public final class MessageStatusChangeProcessor implements ObjectProcessor<Messa
} }
final var msg = persistenceManager.getMessageByID(statusChange.getID()); final var msg = persistenceManager.getMessageByID(statusChange.getID());
if (msg == null)
return;
msg.read(); msg.read();
persistenceManager.updateMessage(msg); persistenceManager.updateMessage(msg);

View File

@ -22,10 +22,16 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) { public void process(UserOperation evt, long socketID, ObjectWriteProxy writeProxy) {
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId); final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketID);
final long contactID = evt.get().getID(); final long contactID = evt.get().getID();
final var sender = persistenceManager.getUserByID(userID); final var recipient = persistenceManager.getUserByID(contactID);
// TODO: Inform the sender if the requested contact has already been deleted
if (recipient == null)
return;
final var sender = persistenceManager.getUserByID(userID);
switch (evt.getOperationType()) { switch (evt.getOperationType()) {
case ADD: case ADD:
logger.log(Level.FINE, logger.log(Level.FINE,
@ -45,7 +51,7 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
sender.setLatestContactDeletion(Instant.now()); sender.setLatestContactDeletion(Instant.now());
// Notify the removed contact on next startup(s) of this deletion // Notify the removed contact on next startup(s) of this deletion
persistenceManager.getUserByID(contactID).setLatestContactDeletion(Instant.now()); recipient.setLatestContactDeletion(Instant.now());
// Notify the removed contact if online // Notify the removed contact if online
if (connectionManager.isOnline(contactID)) if (connectionManager.isOnline(contactID))