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 super T, U> renderer;
+ private final Function super T, U> 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 super InputEvent> 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 super InputEvent> 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 {}
+}