diff --git a/client/.classpath b/client/.classpath index 234db15..99d4cc0 100644 --- a/client/.classpath +++ b/client/.classpath @@ -13,13 +13,14 @@ + - + diff --git a/client/.settings/org.eclipse.jdt.core.prefs b/client/.settings/org.eclipse.jdt.core.prefs index 65c71af..acfcecc 100644 --- a/client/.settings/org.eclipse.jdt.core.prefs +++ b/client/.settings/org.eclipse.jdt.core.prefs @@ -18,6 +18,7 @@ org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.doc.comment.support=enabled org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore diff --git a/client/src/main/java/envoy/client/data/commands/SystemCommandsMap.java b/client/src/main/java/envoy/client/data/commands/SystemCommandsMap.java index cfcb0a4..ed75f24 100644 --- a/client/src/main/java/envoy/client/data/commands/SystemCommandsMap.java +++ b/client/src/main/java/envoy/client/data/commands/SystemCommandsMap.java @@ -37,7 +37,9 @@ public final class SystemCommandsMap { * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ - public void add(String command, SystemCommand systemCommand) { if (isValidKey(command)) systemCommands.put(command, systemCommand); } + public void add(String command, SystemCommand systemCommand) { + if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand); + } /** * This method checks if the input String is a key in the map and returns the @@ -60,7 +62,7 @@ public final class SystemCommandsMap { * @return the wrapped system command, if present * @since Envoy Client v0.2-beta */ - public Optional get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input))); } + public Optional get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); } /** * This method ensures that the "/" of a {@link SystemCommand} is stripped.
diff --git a/client/src/main/java/envoy/client/net/Client.java b/client/src/main/java/envoy/client/net/Client.java index b642141..15e3c01 100644 --- a/client/src/main/java/envoy/client/net/Client.java +++ b/client/src/main/java/envoy/client/net/Client.java @@ -158,6 +158,12 @@ public class Client implements Closeable { // Process IsTyping events receiver.registerProcessor(IsTyping.class, eventBus::dispatch); + // Process PasswordChangeResults + receiver.registerProcessor(PasswordChangeResult.class, eventBus::dispatch); + + // Process ProfilePicChanges + receiver.registerProcessor(ProfilePicChange.class, eventBus::dispatch); + // Send event eventBus.register(SendEvent.class, evt -> { try { @@ -193,7 +199,7 @@ public class Client implements Closeable { * @param evt the event to send * @throws IOException if the event did not reach the server */ - public void sendEvent(Event evt) throws IOException { writeObject(evt); } + public void sendEvent(Event evt) throws IOException { if (online) writeObject(evt); } /** * Requests a new {@link IDGenerator} from the server. 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 a041b9d..173784f 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -20,7 +20,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; @@ -43,6 +42,7 @@ import envoy.client.net.Client; import envoy.client.net.WriteProxy; import envoy.client.ui.*; import envoy.client.ui.listcell.*; +import envoy.client.util.ReflectionUtil; import envoy.data.*; import envoy.data.Attachment.AttachmentType; import envoy.event.*; @@ -161,7 +161,7 @@ public final class ChatScene implements Restorable { rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5)))); messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE))); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); - Rectangle clip = new Rectangle(); + final Rectangle clip = new Rectangle(); clip.setWidth(43); clip.setHeight(43); clip.setArcHeight(43); @@ -175,7 +175,7 @@ public final class ChatScene implements Restorable { // The sender of the message is the recipient of the chat // Exceptions: this user is the sender (sync) or group message (group is // recipient) - final long recipientID = message instanceof GroupMessage || message.getSenderID() == localDB.getUser().getID() ? message.getRecipientID() + final var recipientID = message instanceof GroupMessage || message.getSenderID() == localDB.getUser().getID() ? message.getRecipientID() : message.getSenderID(); localDB.getChat(recipientID).ifPresent(chat -> { chat.insert(message); @@ -229,9 +229,8 @@ public final class ChatScene implements Restorable { switch (e.getOperationType()) { case ADD: if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact); - final Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), 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))); @@ -276,7 +275,7 @@ public final class ChatScene implements Restorable { chatList.setItems(chats); contactLabel.setText(localDB.getUser().getName()); MessageControl.setLocalDB(localDB); - MessageControl.setSceneContext(sceneContext); + MessageControl.setSceneContext(sceneContext); if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); @@ -296,7 +295,7 @@ public final class ChatScene implements Restorable { private void chatListClicked() { if (chatList.getSelectionModel().isEmpty()) return; - final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient(); + final var user = chatList.getSelectionModel().getSelectedItem().getRecipient(); if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes @@ -337,7 +336,7 @@ public final class ChatScene implements Restorable { if (currentChat != null) { topBarContactLabel.setText(currentChat.getRecipient().getName()); if (currentChat.getRecipient() instanceof User) { - String status = ((User) currentChat.getRecipient()).getStatus().toString(); + final String status = ((User) currentChat.getRecipient()).getStatus().toString(); topBarStatusLabel.setText(status); topBarStatusLabel.getStyleClass().add(status.toLowerCase()); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); @@ -345,7 +344,7 @@ public final class ChatScene implements Restorable { topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members"); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); } - Rectangle clip = new Rectangle(); + final Rectangle clip = new Rectangle(); clip.setWidth(43); clip.setHeight(43); clip.setArcHeight(43); @@ -364,7 +363,7 @@ public final class ChatScene implements Restorable { @FXML private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); - sceneContext.getController().initializeData(sceneContext); + sceneContext.getController().initializeData(sceneContext, client); } /** @@ -430,7 +429,7 @@ public final class ChatScene implements Restorable { } // Get attachment type (default is document) - AttachmentType type = AttachmentType.DOCUMENT; + var type = AttachmentType.DOCUMENT; switch (fileChooser.getSelectedExtensionFilter().getDescription()) { case "Pictures": type = AttachmentType.PICTURE; @@ -476,9 +475,14 @@ public final class ChatScene implements Restorable { * @since Envoy Client v0.1-beta */ private void doABarrelRoll(int rotations, double animationTime) { - // contains all Node objects in ChatScene in alphabetical order - final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea, postButton, - remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton }; + // Limiting the rotations and duration + rotations = Math.min(rotations, 100000); + rotations = Math.max(rotations, 1); + animationTime = Math.min(animationTime, 150); + animationTime = Math.max(animationTime, 0.25); + + // contains all Node objects in ChatScene + final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this); for (final var node : rotatableNodes) { // Sets the animation duration to {animationTime} final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node); @@ -505,7 +509,7 @@ public final class ChatScene implements Restorable { messageTextUpdated(); // Sending an IsTyping event if none has been sent for // IsTyping#millisecondsActive - if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { + if (currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID()))); currentChat.lastWritingEventWasNow(); } @@ -514,9 +518,9 @@ public final class ChatScene implements Restorable { } /** - * Returns the id that should be used to send things to the server: - * the id of 'our' {@link User} if the recipient of that object is another User, - * else the id of the {@link Group} 'our' user is sending to. + * Returns the id that should be used to send things to the server: the id of + * 'our' {@link User} if the recipient of that object is another User, else the + * id of the {@link Group} 'our' user is sending to. * * @return an id that can be sent to the server * @since Envoy Client v0.2-beta @@ -570,8 +574,8 @@ public final class ChatScene implements Restorable { * @since Envoy Client v0.1-beta */ private void updateRemainingCharsLabel() { - final int currentLength = messageTextArea.getText().length(); - final int remainingLength = MAX_MESSAGE_LENGTH - currentLength; + final var currentLength = messageTextArea.getText().length(); + final var remainingLength = MAX_MESSAGE_LENGTH - currentLength; remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH)); remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1)); } @@ -660,9 +664,8 @@ public final class ChatScene implements Restorable { /** * Updates the {@code attachmentView} in terms of visibility.
- * Additionally resets the shown image to - * {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently - * present. + * Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} + * if another image is currently present. * * @param visible whether the {@code attachmentView} should be displayed * @since Envoy Client v0.1-beta @@ -695,6 +698,6 @@ public final class ChatScene implements Restorable { @FXML private void searchContacts() { chats.setPredicate(contactSearch.getText().isBlank() ? c -> true - : c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase())); + : c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase())); } } diff --git a/client/src/main/java/envoy/client/ui/controller/SettingsScene.java b/client/src/main/java/envoy/client/ui/controller/SettingsScene.java index f3358a9..5656588 100644 --- a/client/src/main/java/envoy/client/ui/controller/SettingsScene.java +++ b/client/src/main/java/envoy/client/ui/controller/SettingsScene.java @@ -3,10 +3,9 @@ package envoy.client.ui.controller; import javafx.fxml.FXML; import javafx.scene.control.*; +import envoy.client.net.Client; import envoy.client.ui.SceneContext; -import envoy.client.ui.settings.DownloadSettingsPane; -import envoy.client.ui.settings.GeneralSettingsPane; -import envoy.client.ui.settings.SettingsPane; +import envoy.client.ui.settings.*; /** * Project: envoy-client
@@ -28,10 +27,14 @@ public class SettingsScene { /** * @param sceneContext enables the user to return to the chat scene + * @param client the {@code Client} used to get the current user and to + * check if this user is online * @since Envoy Client v0.1-beta */ - public void initializeData(SceneContext sceneContext) { + public void initializeData(SceneContext sceneContext, Client client) { this.sceneContext = sceneContext; + settingsList.getItems().add(new GeneralSettingsPane()); + settingsList.getItems().add(new UserSettingsPane(sceneContext, client.getSender(), client.isOnline())); settingsList.getItems().add(new DownloadSettingsPane(sceneContext)); } @@ -45,8 +48,6 @@ public class SettingsScene { if (!empty && item != null) setGraphic(new Label(item.getTitle())); } }); - - settingsList.getItems().add(new GeneralSettingsPane()); } @FXML diff --git a/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java index e294b15..45e424d 100644 --- a/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java +++ b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java @@ -1,5 +1,6 @@ package envoy.client.ui.listcell; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.ContentDisplay; import javafx.scene.control.ListCell; @@ -34,7 +35,12 @@ public abstract class AbstractListCell extends ListCell { @Override protected final void updateItem(T item, boolean empty) { super.updateItem(item, empty); - setGraphic(empty || item == null ? null : renderItem(item)); + if (!(empty || item == null)) { + setCursor(Cursor.HAND); + setGraphic(renderItem(item)); + } else { + setGraphic(null); + } } /** diff --git a/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java b/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java index ad2b364..6b981a7 100644 --- a/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java +++ b/client/src/main/java/envoy/client/ui/listcell/GenericListCell.java @@ -19,7 +19,7 @@ import javafx.scene.control.ListView; */ public final class GenericListCell extends AbstractListCell { - private Function renderer; + private final Function renderer; /** * @param listView the list view inside of which the cell will be displayed diff --git a/client/src/main/java/envoy/client/ui/settings/DownloadSettingsPane.java b/client/src/main/java/envoy/client/ui/settings/DownloadSettingsPane.java index bdc1352..cce597e 100644 --- a/client/src/main/java/envoy/client/ui/settings/DownloadSettingsPane.java +++ b/client/src/main/java/envoy/client/ui/settings/DownloadSettingsPane.java @@ -1,11 +1,8 @@ package envoy.client.ui.settings; import javafx.geometry.Insets; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.Label; +import javafx.scene.control.*; import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import envoy.client.ui.SceneContext; @@ -31,18 +28,22 @@ public class DownloadSettingsPane extends SettingsPane { */ public DownloadSettingsPane(SceneContext sceneContext) { super("Download"); - final var vbox = new VBox(15); - vbox.setPadding(new Insets(15)); + setSpacing(15); + setPadding(new Insets(15)); // checkbox to disable asking final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName()); checkBox.setSelected(settings.isDownloadSavedWithoutAsking()); + checkBox.setTooltip(new Tooltip("Determines whether a \"Select save location\" - dialogue will be shown when saving attachments.")); checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected())); - vbox.getChildren().add(checkBox); + getChildren().add(checkBox); // Displaying the default path to save to - vbox.getChildren().add(new Label(settings.getItems().get("downloadLocation").getDescription() + ":")); - final var hbox = new HBox(20); - final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath()); + final var pathLabel = new Label(settings.getItems().get("downloadLocation").getDescription() + ":"); + pathLabel.setWrapText(true); + getChildren().add(pathLabel); + final var hbox = new HBox(20); + Tooltip.install(hbox, new Tooltip("Determines the location where attachments will be saved to.")); + final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath()); hbox.getChildren().add(currentPath); // Setting the default path @@ -59,7 +60,6 @@ public class DownloadSettingsPane extends SettingsPane { } }); hbox.getChildren().add(button); - vbox.getChildren().add(hbox); - getChildren().add(vbox); + getChildren().add(hbox); } } diff --git a/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java b/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java index af46fbb..36755a9 100644 --- a/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java +++ b/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java @@ -1,9 +1,7 @@ package envoy.client.ui.settings; -import java.util.List; - import javafx.scene.control.ComboBox; -import javafx.scene.layout.VBox; +import javafx.scene.control.Tooltip; import envoy.client.data.SettingsItem; import envoy.client.event.ThemeChangeEvent; @@ -25,30 +23,36 @@ public class GeneralSettingsPane extends SettingsPane { */ public GeneralSettingsPane() { super("General"); - final var vbox = new VBox(); + setSpacing(10); // TODO: Support other value types - List.of("hideOnClose", "enterToSend") - .stream() - .map(settings.getItems()::get) - .map(i -> new SettingsCheckbox((SettingsItem) i)) - .forEach(vbox.getChildren()::add); + final var settingsItems = settings.getItems(); + final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem) settingsItems.get("hideOnClose")); + hideOnCloseCheckbox.setTooltip(new Tooltip("If selected, Envoy will still be present in the task bar when closed.")); + getChildren().add(hideOnCloseCheckbox); + + final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem) settingsItems.get("enterToSend")); + final var enterToSendTooltip = new Tooltip( + "If selected, messages can be sent pressing \"Enter\". They can always be sent by pressing \"Ctrl\" + \"Enter\""); + enterToSendTooltip.setWrapText(true); + enterToSendCheckbox.setTooltip(enterToSendTooltip); + getChildren().add(enterToSendCheckbox); final var combobox = new ComboBox(); combobox.getItems().add("dark"); combobox.getItems().add("light"); + combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in.")); combobox.setValue(settings.getCurrentTheme()); combobox.setOnAction( e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); }); - vbox.getChildren().add(combobox); + getChildren().add(combobox); final var statusComboBox = new ComboBox(); statusComboBox.getItems().setAll(UserStatus.values()); statusComboBox.setValue(UserStatus.ONLINE); + statusComboBox.setTooltip(new Tooltip("Change your current status")); // TODO add action when value is changed statusComboBox.setOnAction(e -> {}); - vbox.getChildren().add(statusComboBox); - - getChildren().add(vbox); + getChildren().add(statusComboBox); } } diff --git a/client/src/main/java/envoy/client/ui/settings/SettingsPane.java b/client/src/main/java/envoy/client/ui/settings/SettingsPane.java index 49f8abd..be63f26 100644 --- a/client/src/main/java/envoy/client/ui/settings/SettingsPane.java +++ b/client/src/main/java/envoy/client/ui/settings/SettingsPane.java @@ -1,6 +1,6 @@ package envoy.client.ui.settings; -import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; import envoy.client.data.Settings; @@ -12,7 +12,7 @@ import envoy.client.data.Settings; * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ -public abstract class SettingsPane extends Pane { +public abstract class SettingsPane extends VBox { protected String title; diff --git a/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java b/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java new file mode 100644 index 0000000..cad265b --- /dev/null +++ b/client/src/main/java/envoy/client/ui/settings/UserSettingsPane.java @@ -0,0 +1,203 @@ +package envoy.client.ui.settings; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.*; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.InputEvent; +import javafx.scene.layout.HBox; +import javafx.stage.FileChooser; + +import envoy.client.event.SendEvent; +import envoy.client.ui.IconUtil; +import envoy.client.ui.SceneContext; +import envoy.client.ui.custom.ProfilePicImageView; +import envoy.client.util.ReflectionUtil; +import envoy.data.User; +import envoy.event.*; +import envoy.util.Bounds; +import envoy.util.EnvoyLog; + +/** + * Project: envoy-client
+ * File: UserSettingsPane.java
+ * Created: 31.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.2-beta + */ +public class UserSettingsPane extends SettingsPane { + + private boolean profilePicChanged, usernameChanged, validPassword; + private byte[] currentImageBytes; + private String newUsername, newPassword = ""; + + private final ImageView profilePic = new ProfilePicImageView(null, 60); + private final TextField usernameTextField = new TextField(); + private final PasswordField currentPasswordField = new PasswordField(); + private final PasswordField newPasswordField = new PasswordField(); + private final PasswordField repeatNewPasswordField = new PasswordField(); + private final Button saveButton = new Button("Save"); + + private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your acount."); + + private static final EventBus eventBus = EventBus.getInstance(); + private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); + + /** + * Creates a new {@code UserSettingsPane}. + * + * @param sceneContext the {@code SceneContext} to block input to Envoy + * @param user the user who wants to customize his profile + * @param online whether this user is currently online + * @since Envoy Client v0.2-beta + */ + public UserSettingsPane(SceneContext sceneContext, User user, boolean online) { + super("User"); + setSpacing(10); + + // Display of profile pic change mechanism + final var hbox = new HBox(); + // TODO: display current profile pic + profilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon")); + profilePic.setCursor(Cursor.HAND); + profilePic.setFitWidth(60); + profilePic.setFitHeight(60); + profilePic.setOnMouseClicked(e -> { + if (!online) return; + final var pictureChooser = new FileChooser(); + + pictureChooser.setTitle("Select a new profile pic"); + pictureChooser.setInitialDirectory(new File(System.getProperty("user.home"))); + pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif")); + + final var file = pictureChooser.showOpenDialog(sceneContext.getStage()); + + if (file != null) { + + // Check max file size + if (file.length() > 5E6) { + new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!").showAndWait(); + return; + } + + try { + currentImageBytes = Files.readAllBytes(file.toPath()); + profilePic.setImage(new Image(new ByteArrayInputStream(currentImageBytes))); + profilePicChanged = true; + } catch (final IOException e1) { + e1.printStackTrace(); + } + } + }); + hbox.getChildren().add(profilePic); + + // Displaying the username change mechanism + final var username = user.getName(); + newUsername = username; + usernameTextField.setText(username); + final EventHandler textChanged = e -> { + newUsername = usernameTextField.getText(); + usernameChanged = newUsername != username; + }; + usernameTextField.setOnInputMethodTextChanged(textChanged); + usernameTextField.setOnKeyTyped(textChanged); + hbox.getChildren().add(usernameTextField); + getChildren().add(hbox); + + // "Displaying" the password change mechanism + final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() }; + final Label[] passwordLabels = { new Label("Enter current password:"), new Label("Enter new password:"), + new Label("Repeat new password:") }; + + final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField }; + final EventHandler passwordEntered = e -> { + newPassword = newPasswordField.getText(); + validPassword = newPassword.equals(repeatNewPasswordField.getText()) + && !newPasswordField.getText().isBlank(); + }; + newPasswordField.setOnInputMethodTextChanged(passwordEntered); + newPasswordField.setOnKeyTyped(passwordEntered); + repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered); + repeatNewPasswordField.setOnKeyTyped(passwordEntered); + + for (int i = 0; i < passwordHBoxes.length; i++) { + final var hBox2 = passwordHBoxes[i]; + passwordLabels[i].setWrapText(true); + hBox2.getChildren().add(passwordLabels[i]); + hBox2.getChildren().add(passwordFields[i]); + getChildren().add(hBox2); + } + + // Displaying the save button + saveButton.setOnAction(e -> save(user.getID(), currentPasswordField.getText())); + saveButton.setAlignment(Pos.BOTTOM_RIGHT); + getChildren().add(saveButton); + + final var offline = !online; + ReflectionUtil.getAllDeclaredNodeVariables(this).forEach(node -> node.setDisable(offline)); + if (offline) { + final var infoLabel = new Label("You shall not pass!\n(... Unless you would happen to be online)"); + infoLabel.setId("infoLabel-warning"); + infoLabel.setWrapText(true); + getChildren().add(infoLabel); + + Tooltip.install(this, beOnlineReminder); + } else Tooltip.uninstall(this, beOnlineReminder); + } + + /** + * Saves the given input and sends the changed input to the server + * + * @param username the new username + * @since Envoy Client v0.2-beta + */ + private void save(long userID, String oldPassword) { + + // The profile pic was changed + if (profilePicChanged) { + final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID); + eventBus.dispatch(profilePicChangeEvent); + eventBus.dispatch(new SendEvent(profilePicChangeEvent)); + logger.log(Level.INFO, "The user just changed his profile pic."); + } + + // The username was changed + final var validContactName = Bounds.isValidContactName(newUsername); + if (usernameChanged && validContactName) { + final var nameChangeEvent = new NameChange(userID, newUsername); + eventBus.dispatch(new SendEvent(nameChangeEvent)); + eventBus.dispatch(nameChangeEvent); + logger.log(Level.INFO, "The user just changed his name to " + newUsername + "."); + } else if (!validContactName) { + final var alert = new Alert(AlertType.ERROR); + alert.setTitle("Invalid username"); + alert.setContentText("The entered username does not conform with the naming limitations: " + Bounds.CONTACT_NAME_PATTERN); + alert.showAndWait(); + logger.log(Level.INFO, "An invalid username was requested."); + return; + } + + // The password was changed + if (validPassword) { + eventBus.dispatch(new SendEvent(new PasswordChangeRequest(newPassword, oldPassword, userID))); + logger.log(Level.INFO, "The user just tried to change his password!"); + } else if (!(validPassword || newPassword.isBlank())) { + final var alert = new Alert(AlertType.ERROR); + alert.setTitle("Unequal Password"); + alert.setContentText("Repeated password is unequal to the chosen new password"); + alert.showAndWait(); + return; + } + } +} diff --git a/client/src/main/java/envoy/client/util/ReflectionUtil.java b/client/src/main/java/envoy/client/util/ReflectionUtil.java new file mode 100644 index 0000000..132f5da --- /dev/null +++ b/client/src/main/java/envoy/client/util/ReflectionUtil.java @@ -0,0 +1,88 @@ +package envoy.client.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.scene.Node; + +/** + * Project: envoy-client
+ * File: ReflectionUtil.java
+ * Created: 02.08.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.2-beta + */ +public class ReflectionUtil { + + private ReflectionUtil() {} + + /** + * Gets all declared variables of the given instance that have the specified + * class
+ * (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a + * GUI class). + *

+ * Important: If you are using a module, you first need to declare
+ * "opens {your_package} to envoy.client.util;" in your module-info.java
. + * + * @param the type of the object + * @param the type to return + * @param instance the instance of a given class whose values are to be + * evaluated + * @param typeToReturn the type of variable to return + * @return all variables in the given instance that have the requested type + * @throws RuntimeException if an exception occurs + * @since Envoy Client v0.2-beta + */ + public static Stream getAllDeclaredVariablesOfTypeAsStream(T instance, Class typeToReturn) { + return Arrays.stream(instance.getClass().getDeclaredFields()).filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> { + try { + field.setAccessible(true); + final var value = field.get(instance); + field.setAccessible(false); + return value; + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }).map(typeToReturn::cast);// field -> + // typeToReturn.isAssignableFrom(field.getClass())).map(typeToReturn::cast); + } + + /** + * Gets all declared variables of the given instance that are children of + * {@code Node}. + *

+ * Important: If you are using a module, you first need to declare
+ * "opens {your_package} to envoy.client.util;" in your module-info.java
. + * + * @param the type of the instance + * @param instance the instance of a given class whose values are to be + * evaluated + * @return all variables of the given object that have the requested type as + * {@code Stream} + * @since Envoy Client v0.2-beta + */ + public static Stream getAllDeclaredNodeVariablesAsStream(T instance) { + return getAllDeclaredVariablesOfTypeAsStream(instance, Node.class); + } + + /** + * Gets all declared variables of the given instance that are children of + * {@code Node}
+ *

+ * Important: If you are using a module, you first need to declare
+ * "opens {your_package} to envoy.client.util;" in your module-info.java
. + * + * @param the type of the instance + * @param instance the instance of a given class whose values are to be + * evaluated + * @return all variables of the given object that have the requested type + * @since Envoy Client v0.2-beta + */ + public static List getAllDeclaredNodeVariables(T instance) { + return getAllDeclaredNodeVariablesAsStream(instance).collect(Collectors.toList()); + } +} diff --git a/client/src/main/java/envoy/client/util/package-info.java b/client/src/main/java/envoy/client/util/package-info.java new file mode 100644 index 0000000..c6b342e --- /dev/null +++ b/client/src/main/java/envoy/client/util/package-info.java @@ -0,0 +1,11 @@ +/** + * This package contains utility classes for use in envoy-client. + *

+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 02.08.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.2-beta + */ +package envoy.client.util; diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index 2b39906..778f673 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -19,6 +19,7 @@ module envoy { requires javafx.graphics; opens envoy.client.ui to javafx.graphics, javafx.fxml; - opens envoy.client.ui.controller to javafx.graphics, javafx.fxml; + opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util; opens envoy.client.ui.custom to javafx.graphics, javafx.fxml; + opens envoy.client.ui.settings to envoy.client.util; } diff --git a/client/src/main/resources/fxml/ChatScene.fxml b/client/src/main/resources/fxml/ChatScene.fxml index 8fb2fe2..5bf8cd1 100644 --- a/client/src/main/resources/fxml/ChatScene.fxml +++ b/client/src/main/resources/fxml/ChatScene.fxml @@ -54,7 +54,7 @@ - - + + + + diff --git a/common/src/main/java/envoy/event/PasswordChangeRequest.java b/common/src/main/java/envoy/event/PasswordChangeRequest.java new file mode 100644 index 0000000..dd088df --- /dev/null +++ b/common/src/main/java/envoy/event/PasswordChangeRequest.java @@ -0,0 +1,46 @@ +package envoy.event; + +import envoy.data.Contact; + +/** + * Project: envoy-common
+ * File: PasswordChangeRequest.java
+ * Created: 31.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Common v0.2-beta + */ +public class PasswordChangeRequest extends Event { + + private final long id; + private final String oldPassword; + + private static final long serialVersionUID = 0L; + + /** + * @param newPassword the new password of that user + * @param oldPassword the old password of that user + * @param userID the ID of the user who wants to change his password + * @since Envoy Common v0.2-beta + */ + public PasswordChangeRequest(String newPassword, String oldPassword, long userID) { + super(newPassword); + this.oldPassword = oldPassword; + id = userID; + } + + /** + * @return the ID of the {@link Contact} this event is related to + * @since Envoy Common v0.2-alpha + */ + public long getID() { return id; } + + /** + * @return the old password of the underlying user + * @since Envoy Common v0.2-beta + */ + public String getOldPassword() { return oldPassword; } + + @Override + public String toString() { return "PasswordChangeRequest[id=" + id + "]"; } +} diff --git a/common/src/main/java/envoy/event/PasswordChangeResult.java b/common/src/main/java/envoy/event/PasswordChangeResult.java new file mode 100644 index 0000000..6252980 --- /dev/null +++ b/common/src/main/java/envoy/event/PasswordChangeResult.java @@ -0,0 +1,26 @@ +package envoy.event; + +/** + * This class acts as a notice to the user whether his + * {@link envoy.event.PasswordChangeRequest} was successful. + *

+ * Project: envoy-common
+ * File: PasswordChangeResult.java
+ * Created: 01.08.2020
+ * + * @author Leon Hofmeister + * @since Envoy Common v0.2-beta + */ +public class PasswordChangeResult extends Event { + + private static final long serialVersionUID = 1L; + + /** + * Creates an instance of {@code PasswordChangeResult}. + * + * @param value whether the preceding {@link envoy.event.PasswordChangeRequest} + * was successful. + * @since Envoy Common v0.2-beta + */ + public PasswordChangeResult(boolean value) { super(value); } +} diff --git a/common/src/main/java/envoy/event/ProfilePicChange.java b/common/src/main/java/envoy/event/ProfilePicChange.java new file mode 100644 index 0000000..c06c545 --- /dev/null +++ b/common/src/main/java/envoy/event/ProfilePicChange.java @@ -0,0 +1,32 @@ +package envoy.event; + +/** + * Project: envoy-common
+ * File: ProfilePicChange.java
+ * Created: 31.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Common v0.2-beta + */ +public class ProfilePicChange extends Event { + + private final long id; + + private static final long serialVersionUID = 0L; + + /** + * @param value the byte[] of the new image + * @param userID the ID of the user who changed his profile pic + * @since Envoy Common v0.2-beta + */ + public ProfilePicChange(byte[] value, long userID) { + super(value); + id = userID; + } + + /** + * @return the ID of the user changing his profile pic + * @since Envoy Common v0.2-beta + */ + public long getId() { return id; } +} diff --git a/server/src/main/java/envoy/server/Startup.java b/server/src/main/java/envoy/server/Startup.java index ac24c40..c131fce 100755 --- a/server/src/main/java/envoy/server/Startup.java +++ b/server/src/main/java/envoy/server/Startup.java @@ -70,7 +70,10 @@ public class Startup { new IDGeneratorRequestProcessor(), new UserSearchProcessor(), new ContactOperationProcessor(), - new IsTypingProcessor()))); + new IsTypingProcessor(), + new NameChangeProcessor(), + new ProfilePicChangeProcessor(), + new PasswordChangeRequestProcessor()))); // Initialize the current message ID final PersistenceManager persistenceManager = PersistenceManager.getInstance(); diff --git a/server/src/main/java/envoy/server/data/Contact.java b/server/src/main/java/envoy/server/data/Contact.java index 43a9983..a1e626d 100644 --- a/server/src/main/java/envoy/server/data/Contact.java +++ b/server/src/main/java/envoy/server/data/Contact.java @@ -18,7 +18,7 @@ import javax.persistence.*; */ @Entity -@Table(name = "contacts") +@Table(name = "contacts", uniqueConstraints = { @UniqueConstraint(columnNames = { "name" }) }) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public abstract class Contact { diff --git a/server/src/main/java/envoy/server/processors/PasswordChangeRequestProcessor.java b/server/src/main/java/envoy/server/processors/PasswordChangeRequestProcessor.java new file mode 100644 index 0000000..84ebc7a --- /dev/null +++ b/server/src/main/java/envoy/server/processors/PasswordChangeRequestProcessor.java @@ -0,0 +1,35 @@ +package envoy.server.processors; + +import java.io.IOException; +import java.util.logging.Level; + +import envoy.event.PasswordChangeRequest; +import envoy.event.PasswordChangeResult; +import envoy.server.data.PersistenceManager; +import envoy.server.net.ObjectWriteProxy; +import envoy.server.util.PasswordUtil; +import envoy.util.EnvoyLog; + +/** + * Project: envoy-server-standalone
+ * File: PasswordChangeRequestProcessor.java
+ * Created: 31.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Server v0.2-beta + */ +public class PasswordChangeRequestProcessor implements ObjectProcessor { + + @Override + public void process(PasswordChangeRequest event, long socketID, ObjectWriteProxy writeProxy) throws IOException { + final var persistenceManager = PersistenceManager.getInstance(); + final var user = persistenceManager.getUserByID(event.getID()); + final var logger = EnvoyLog.getLogger(PasswordChangeRequestProcessor.class); + final var correctAuthentication = PasswordUtil.validate(event.getOldPassword(), user.getPasswordHash()); + if (correctAuthentication) { + user.setPasswordHash(PasswordUtil.hash(event.get())); + logger.log(Level.INFO, user + " changed his password"); + } else logger.log(Level.INFO, user + " tried changing his password but provided insufficient authentication"); + writeProxy.write(socketID, new PasswordChangeResult(correctAuthentication)); + } +} diff --git a/server/src/main/java/envoy/server/processors/ProfilePicChangeProcessor.java b/server/src/main/java/envoy/server/processors/ProfilePicChangeProcessor.java new file mode 100644 index 0000000..10069a8 --- /dev/null +++ b/server/src/main/java/envoy/server/processors/ProfilePicChangeProcessor.java @@ -0,0 +1,20 @@ +package envoy.server.processors; + +import java.io.IOException; + +import envoy.event.ProfilePicChange; +import envoy.server.net.ObjectWriteProxy; + +/** + * Project: envoy-server-standalone
+ * File: ProfilePicChangeProcessor.java
+ * Created: 01.08.2020
+ * + * @author Leon Hofmeister + * @since Envoy Server v0.2-beta + */ +public class ProfilePicChangeProcessor implements ObjectProcessor { + + @Override + public void process(ProfilePicChange event, long socketID, ObjectWriteProxy writeProxy) throws IOException {} +}