Merge remote-tracking branch 'origin/develop' into

enhanced-shortcut-mechanism

Conflicts:
	client/src/main/java/envoy/client/ui/SceneContext.java
This commit is contained in:
Leon Hofmeister 2020-10-10 14:23:14 +02:00
commit 31e27ae2e0
Signed by: delvh
GPG Key ID: 3DECE05F6D9A647C
24 changed files with 361 additions and 195 deletions

View File

@ -293,6 +293,9 @@ public final class LocalDB implements EventListener {
}); });
} }
@Event(priority = 500)
private void onOwnStatusChange(OwnStatusChange statusChange) { user.setStatus(statusChange.get()); }
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys * user names as keys

View File

@ -0,0 +1,21 @@
package envoy.client.event;
import envoy.data.User.UserStatus;
import envoy.event.Event;
/**
* Signifies a manual status change of the client user.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class OwnStatusChange extends Event<UserStatus> {
private static final long serialVersionUID = 1L;
/**
* @param value the new user status of the client user
* @since Envoy Client v0.3-beta
*/
public OwnStatusChange(UserStatus value) { super(value); }
}

View File

@ -27,8 +27,6 @@ public final class AlertHelper {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void confirmAction(Alert alert, Runnable action) { public static void confirmAction(Alert alert, Runnable action) {
alert.setHeight(225);
alert.setWidth(400);
alert.setHeaderText(""); alert.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run()); if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
else action.run(); else action.run();

View File

@ -1,15 +1,8 @@
package envoy.client.helper; package envoy.client.helper;
import java.util.logging.Level;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.*; import envoy.client.event.EnvoyCloseEvent;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.StatusTrayIcon; import envoy.client.ui.StatusTrayIcon;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -29,30 +22,21 @@ public final class ShutdownHelper {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void exit() { public static void exit() { exit(false); }
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) Context.getInstance().getStage().setIconified(true);
/**
* Exits Envoy immediately if {@code force = true},
* else it can exit or minimize Envoy, depending on the current state of
* {@link Settings#isHideOnClose()} and {@link StatusTrayIcon#isSupported()}.
*
* @param force whether to close in any case.
* @since Envoy Client v0.2-beta
*/
public static void exit(boolean force) {
if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) Context.getInstance().getStage().setIconified(true);
else { else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent()); EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0); System.exit(0);
} }
} }
/**
* Logs the current user out and reopens
* {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
});
}
} }

View File

@ -117,9 +117,13 @@ public final class Startup extends Application {
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>()); cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>()); cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>()); cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
final var originalStatus = localDB.getUser().getStatus();
try { try {
client.performHandshake(credentials, cacheMap); client.performHandshake(credentials, cacheMap);
if (client.isOnline()) { if (client.isOnline()) {
// Restore the original status as the server automatically returns status ONLINE
client.getSender().setStatus(originalStatus);
loadChatScene(); loadChatScene();
client.initReceiver(localDB, cacheMap); client.initReceiver(localDB, cacheMap);
return true; return true;
@ -174,7 +178,8 @@ public final class Startup extends Application {
private static void loadChatScene() { private static void loadChatScene() {
// Set client user in local database // Set client user in local database
localDB.setUser(client.getSender()); final var user = client.getSender();
localDB.setUser(user);
// Initialize chats in local database // Initialize chats in local database
try { try {
@ -188,8 +193,13 @@ public final class Startup extends Application {
context.initWriteProxy(); context.initWriteProxy();
if (client.isOnline()) context.getWriteProxy().flushCache(); if (client.isOnline()) {
else context.getWriteProxy().flushCache();
// Inform the server that this user has a different user status than expected
if (!user.getStatus().equals(UserStatus.ONLINE)) client.send(new UserStatusChange(user));
} else
// Set all contacts to offline mode // Set all contacts to offline mode
localDB.getChats() localDB.getChats()
.stream() .stream()

View File

@ -6,9 +6,11 @@ import java.awt.TrayIcon.MessageType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.event.OwnStatusChange;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.util.IconUtil; import envoy.client.util.*;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.User.UserStatus;
import dev.kske.eventbus.*; import dev.kske.eventbus.*;
import dev.kske.eventbus.Event; import dev.kske.eventbus.Event;
@ -51,16 +53,32 @@ public final class StatusTrayIcon implements EventListener {
trayIcon.setImageAutoSize(true); trayIcon.setImageAutoSize(true);
trayIcon.setToolTip("You are notified if you have unread messages."); trayIcon.setToolTip("You are notified if you have unread messages.");
final PopupMenu popup = new PopupMenu(); final var popup = new PopupMenu();
final MenuItem exitMenuItem = new MenuItem("Exit"); // Adding the exit menu item
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit()); final var exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true));
popup.add(exitMenuItem); popup.add(exitMenuItem);
// Adding the logout menu item
final var logoutMenuItem = new MenuItem("Logout");
logoutMenuItem.addActionListener(evt -> { hide(); Platform.runLater(UserUtil::logout); });
popup.add(logoutMenuItem);
// Adding the status change items
final var statusSubMenu = new Menu("Change status");
for (final var status : UserStatus.values()) {
final var statusMenuItem = new MenuItem(status.toString().toLowerCase());
statusMenuItem.addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status)));
statusSubMenu.add(statusMenuItem);
}
popup.add(statusSubMenu);
trayIcon.setPopupMenu(popup); trayIcon.setPopupMenu(popup);
// Only display messages if the stage is not focused // Only display messages if the stage is not focused and the current user status
stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue()); // is not BUSY (if BUSY, displayMessages will be false)
stage.focusedProperty().addListener((ov, wasFocused, isFocused) -> displayMessages = !displayMessages && wasFocused ? false : !isFocused);
// Show the window if the user clicks on the icon // Show the window if the user clicks on the icon
trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); })); trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); }));
@ -87,11 +105,13 @@ public final class StatusTrayIcon implements EventListener {
*/ */
public void hide() { SystemTray.getSystemTray().remove(trayIcon); } public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
@Event
private void onOwnStatusChange(OwnStatusChange statusChange) { displayMessages = !statusChange.get().equals(UserStatus.BUSY); }
@Event @Event
private void onMessage(Message message) { private void onMessage(Message message) {
if (displayMessages) trayIcon.displayMessage( if (displayMessages) trayIcon
message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received", .displayMessage(message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received"
message.getText(), : "New message received", message.getText(), MessageType.INFO);
MessageType.INFO);
} }
} }

View File

@ -4,7 +4,8 @@ import java.util.Random;
import java.util.function.*; import java.util.function.*;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.scene.control.ListView; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualFlow;
import envoy.client.data.Context; import envoy.client.data.Context;
@ -12,8 +13,9 @@ import envoy.client.data.commands.*;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.ChatScene; import envoy.client.ui.controller.ChatScene;
import envoy.client.util.MessageUtil; import envoy.client.util.*;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.User.UserStatus;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
@ -52,7 +54,7 @@ public final class ChatSceneCommands {
.build("dabr"); .build("dabr");
// Logout initialization // Logout initialization
builder.setAction(text -> ShutdownHelper.logout()).setDescription("Logs you out.").buildNoArg("logout"); builder.setAction(text -> UserUtil.logout()).setDescription("Logs you out.").buildNoArg("logout");
// Exit initialization // Exit initialization
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program.").build("exit", false); builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program.").build("exit", false);
@ -63,6 +65,17 @@ public final class ChatSceneCommands {
.setDescription("Opens the settings screen") .setDescription("Opens the settings screen")
.buildNoArg("settings"); .buildNoArg("settings");
// Status change initialization
builder.setAction(text -> {
try {
UserUtil.changeStatus(Enum.valueOf(UserStatus.class, text.get(0).toUpperCase()));
} catch (final IllegalArgumentException e) {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Please provide an existing status");
alert.showAndWait();
}
}).setDescription("Changes your status to the given status.").setNumberOfArguments(1).setDefaults("").build("status");
// Selection of a new message initialization // Selection of a new message initialization
messageDependantAction("s", messageDependantAction("s",
m -> { messageList.getSelectionModel().clearSelection(); messageList.getSelectionModel().select(m); }, m -> { messageList.getSelectionModel().clearSelection(); messageList.getSelectionModel().select(m); },

View File

@ -2,9 +2,8 @@ package envoy.client.ui.control;
import javafx.geometry.*; import javafx.geometry.*;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.*; import javafx.scene.image.Image;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
@ -23,6 +22,8 @@ public final class ChatControl extends HBox {
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32); groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
/** /**
* Creates a new {@code ChatControl}.
*
* @param chat the chat to display * @param chat the chat to display
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -31,13 +32,7 @@ public final class ChatControl extends HBox {
setPadding(new Insets(0, 0, 3, 0)); setPadding(new Insets(0, 0, 3, 0));
// Profile picture // Profile picture
ImageView contactProfilePic = new ImageView(chat instanceof GroupChat ? groupIcon : userIcon); final var contactProfilePic = new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32);
final var clip = new Rectangle();
clip.setWidth(32);
clip.setHeight(32);
clip.setArcHeight(32);
clip.setArcWidth(32);
contactProfilePic.setClip(clip);
getChildren().add(contactProfilePic); getChildren().add(contactProfilePic);
// Spacing // Spacing

View File

@ -15,25 +15,37 @@ import envoy.data.*;
*/ */
public final class ContactControl extends VBox { public final class ContactControl extends VBox {
private final Contact contact;
/** /**
* @param contact the contact to display * @param contact the contact to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ContactControl(Contact contact) { public ContactControl(Contact contact) {
this.contact = contact;
// Name label // Name label
final var nameLabel = new Label(contact.getName()); final var nameLabel = new Label(contact.getName());
getChildren().add(nameLabel); getChildren().add(nameLabel);
// Online status (user) or member count (group) // Online status (user) or member count (group)
if (contact instanceof User) { getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact) : new GroupSizeLabel((Group) contact));
final var status = ((User) contact).getStatus().toString();
final var statusLabel = new Label(status);
statusLabel.getStyleClass().add(status.toLowerCase());
getChildren().add(statusLabel);
} else {
getChildren().add(new Label(contact.getContacts().size() + " members"));
}
getStyleClass().add("list-element"); getStyleClass().add("list-element");
} }
/**
* Replaces the info label of this {@code ContactControl} with an updated
* version.
* <p>
* This method should be called when the status of the underlying user or the
* size of the underlying group has changed.
*
* @since Envoy Client v0.3-beta
* @apiNote will produce buggy results if contact control gets updated so that
* the info label is no longer on index 1.
*/
public void replaceInfoLabel() {
getChildren().set(1, contact instanceof User ? new UserStatusLabel((User) contact) : new GroupSizeLabel((Group) contact));
}
} }

View File

@ -0,0 +1,20 @@
package envoy.client.ui.control;
import javafx.scene.control.Label;
import envoy.data.Group;
/**
* Displays the amount of members in a {@link Group}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class GroupSizeLabel extends Label {
/**
* @param recipient the group whose members to show
* @since Envoy Client v0.3-beta
*/
public GroupSizeLabel(Group recipient) { super(recipient.getContacts().size() + " members"); }
}

View File

@ -0,0 +1,23 @@
package envoy.client.ui.control;
import javafx.scene.control.Label;
import envoy.data.User;
/**
* Displays the status of a {@link User}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class UserStatusLabel extends Label {
/**
* @param user the user whose status to display
* @since Envoy Client v0.3-beta
*/
public UserStatusLabel(User user) {
super(user.getStatus().toString());
getStyleClass().add(user.getStatus().toString().toLowerCase());
}
}

View File

@ -13,6 +13,7 @@ import javafx.application.Platform;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.FilteredList;
import javafx.fxml.*; import javafx.fxml.*;
import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.*; import javafx.scene.image.*;
@ -29,7 +30,7 @@ import envoy.client.event.*;
import envoy.client.net.*; import envoy.client.net.*;
import envoy.client.ui.*; import envoy.client.ui.*;
import envoy.client.ui.chatscene.*; import envoy.client.ui.chatscene.*;
import envoy.client.ui.control.ChatControl; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*; import envoy.client.ui.listcell.*;
import envoy.client.util.*; import envoy.client.util.*;
import envoy.data.*; import envoy.data.*;
@ -51,12 +52,6 @@ import dev.kske.eventbus.Event;
*/ */
public final class ChatScene implements EventListener, Restorable { public final class ChatScene implements EventListener, Restorable {
@FXML
private GridPane scene;
@FXML
private Label contactLabel;
@FXML @FXML
private ListView<Message> messageList; private ListView<Message> messageList;
@ -84,33 +79,33 @@ public final class ChatScene implements EventListener, Restorable {
@FXML @FXML
private Button newContactButton; private Button newContactButton;
@FXML
private TextArea messageTextArea;
@FXML @FXML
private Label remainingChars; private Label remainingChars;
@FXML @FXML
private Label infoLabel; private Label infoLabel;
@FXML
private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
@FXML @FXML
private Label topBarContactLabel; private Label topBarContactLabel;
@FXML @FXML
private Label topBarStatusLabel; private Label topBarStatusLabel;
@FXML
private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
@FXML @FXML
private ImageView clientProfilePic; private ImageView clientProfilePic;
@FXML @FXML
private ImageView recipientProfilePic; private ImageView recipientProfilePic;
@FXML
private TextArea messageTextArea;
@FXML @FXML
private TextArea contactSearch; private TextArea contactSearch;
@ -129,6 +124,12 @@ public final class ChatScene implements EventListener, Restorable {
@FXML @FXML
private HBox contactSpecificOnlineOperations; private HBox contactSpecificOnlineOperations;
@FXML
private HBox ownContactControl;
@FXML
private Region spaceBetweenUserAndSettingsButton;
private Chat currentChat; private Chat currentChat;
private FilteredList<Chat> chats; private FilteredList<Chat> chats;
private boolean recording; private boolean recording;
@ -188,10 +189,15 @@ public final class ChatScene implements EventListener, Restorable {
clientProfilePic.setClip(clip); clientProfilePic.setClip(clip);
chatList.setItems(chats = new FilteredList<>(localDB.getChats())); chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName());
// Set the design of the box in the upper-left corner
settingsButton.setAlignment(Pos.BOTTOM_RIGHT);
HBox.setHgrow(spaceBetweenUserAndSettingsButton, Priority.ALWAYS);
generateOwnStatusControl();
Platform.runLater(() -> { Platform.runLater(() -> {
final var online = client.isOnline(); final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative // no check will be performed in case it has already been disabled - a negative
// GroupCreationResult might have been returned // GroupCreationResult might have been returned
if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online); if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online);
@ -251,8 +257,19 @@ public final class ChatScene implements EventListener, Restorable {
.ifPresent(msg -> Platform.runLater(messageList::refresh)); .ifPresent(msg -> Platform.runLater(messageList::refresh));
} }
@Event(eventType = UserStatusChange.class) @Event
private void onUserStatusChange() { Platform.runLater(chatList::refresh); } private void onUserStatusChange(UserStatusChange statusChange) {
Platform.runLater(() -> {
chatList.refresh();
// Replacing the display in the top bar
if (currentChat != null && currentChat.getRecipient().getID() == statusChange.getID()) {
topBarStatusLabel.getStyleClass().clear();
topBarStatusLabel.setText(statusChange.get().toString());
topBarStatusLabel.getStyleClass().add(statusChange.get().toString().toLowerCase());
}
});
}
@Event @Event
private void onContactOperation(ContactOperation operation) { private void onContactOperation(ContactOperation operation) {
@ -297,6 +314,7 @@ public final class ChatScene implements EventListener, Restorable {
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
// TODO: cache image // TODO: cache image
if (currentChat != null)
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
@ -359,6 +377,7 @@ public final class ChatScene implements EventListener, Restorable {
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
} else { } else {
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members"); topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
topBarStatusLabel.getStyleClass().clear();
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
final var clip = new Rectangle(); final var clip = new Rectangle();
@ -699,6 +718,21 @@ public final class ChatScene implements EventListener, Restorable {
attachmentView.setVisible(visible); attachmentView.setVisible(visible);
} }
@Event(eventType = OwnStatusChange.class, priority = 50)
private void generateOwnStatusControl() {
// Update the own user status if present
if (ownContactControl.getChildren().get(0) instanceof ContactControl)
((ContactControl) ownContactControl.getChildren().get(0)).replaceInfoLabel();
else {
// Else prepend it to the HBox children
final var ownUserControl = new ContactControl(localDB.getUser());
ownUserControl.setAlignment(Pos.CENTER_LEFT);
ownContactControl.getChildren().add(0, ownUserControl);
}
}
// Context menu actions // Context menu actions
@FXML @FXML

View File

@ -5,8 +5,6 @@ import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import envoy.client.data.Context;
/** /**
* Displays options for downloading {@link envoy.data.Attachment}s. * Displays options for downloading {@link envoy.data.Attachment}s.
* *
@ -47,7 +45,7 @@ public final class DownloadSettingsPane extends SettingsPane {
final var directoryChooser = new DirectoryChooser(); final var directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select the directory where attachments should be saved to"); directoryChooser.setTitle("Select the directory where attachments should be saved to");
directoryChooser.setInitialDirectory(settings.getDownloadLocation()); directoryChooser.setInitialDirectory(settings.getDownloadLocation());
final var selectedDirectory = directoryChooser.showDialog(Context.getInstance().getSceneContext().getStage()); final var selectedDirectory = directoryChooser.showDialog(context.getSceneContext().getStage());
if (selectedDirectory != null) { if (selectedDirectory != null) {
currentPath.setText(selectedDirectory.getAbsolutePath()); currentPath.setText(selectedDirectory.getAbsolutePath());

View File

@ -4,8 +4,8 @@ import javafx.scene.control.*;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.StatusTrayIcon; import envoy.client.ui.StatusTrayIcon;
import envoy.client.util.UserUtil;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -57,14 +57,13 @@ public final class GeneralSettingsPane extends SettingsPane {
final var statusComboBox = new ComboBox<UserStatus>(); final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values()); statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(UserStatus.ONLINE); statusComboBox.setValue(context.getLocalDB().getUser().getStatus());
statusComboBox.setTooltip(new Tooltip("Change your current status")); statusComboBox.setTooltip(new Tooltip("Change your current status"));
// TODO add action when value is changed statusComboBox.setOnAction(e -> UserUtil.changeStatus(statusComboBox.getValue()));
statusComboBox.setOnAction(e -> {});
getChildren().add(statusComboBox); getChildren().add(statusComboBox);
final var logoutButton = new Button("Logout"); final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> ShutdownHelper.logout()); logoutButton.setOnAction(e -> UserUtil.logout());
final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account"); final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account");
logoutTooltip.setWrapText(true); logoutTooltip.setWrapText(true);
logoutButton.setTooltip(logoutTooltip); logoutButton.setTooltip(logoutTooltip);

View File

@ -5,7 +5,6 @@ import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import envoy.client.data.Context;
import envoy.client.net.Client; import envoy.client.net.Client;
/** /**
@ -20,7 +19,7 @@ import envoy.client.net.Client;
*/ */
public abstract class OnlineOnlySettingsPane extends SettingsPane { public abstract class OnlineOnlySettingsPane extends SettingsPane {
protected final Client client = Context.getInstance().getClient(); protected final Client client = context.getClient();
private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account."); private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account.");

View File

@ -2,7 +2,7 @@ package envoy.client.ui.settings;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import envoy.client.data.Settings; import envoy.client.data.*;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -13,6 +13,7 @@ public abstract class SettingsPane extends VBox {
protected String title; protected String title;
protected static final Settings settings = Settings.getInstance(); protected static final Settings settings = Settings.getInstance();
protected static final Context context = Context.getInstance();
protected SettingsPane(String title) { this.title = title; } protected SettingsPane(String title) { this.title = title; }

View File

@ -14,7 +14,6 @@ import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import envoy.client.data.Context;
import envoy.client.ui.control.ProfilePicImageView; import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
import envoy.event.*; import envoy.event.*;
@ -66,7 +65,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home"))); pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif")); pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
final var file = pictureChooser.showOpenDialog(Context.getInstance().getSceneContext().getStage()); final var file = pictureChooser.showOpenDialog(context.getSceneContext().getStage());
if (file != null) { if (file != null) {

View File

@ -0,0 +1,64 @@
package envoy.client.util;
import java.util.logging.Level;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Context;
import envoy.client.event.*;
import envoy.client.helper.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.data.User.UserStatus;
import envoy.event.UserStatusChange;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/**
* Contains methods that change something about the currently logged in user.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class UserUtil {
private UserUtil() {}
/**
* Logs the current user out and reopens
* {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
});
}
/**
* Notifies the application that the status of the currently logged in user has
* changed.
*
* @param newStatus the new status
* @since Envoy Client v0.3-beta
*/
public static void changeStatus(UserStatus newStatus) {
// Sending the already active status is a valid action
if (newStatus.equals(Context.getInstance().getLocalDB().getUser().getStatus())) return;
else {
EventBus.getInstance().dispatch(new OwnStatusChange(newStatus));
if (Context.getInstance().getClient().isOnline())
Context.getInstance().getClient().send(new UserStatusChange(Context.getInstance().getLocalDB().getUser().getID(), newStatus));
}
}
}

View File

@ -6,7 +6,7 @@
-fx-background-radius: 15.0px; -fx-background-radius: 15.0px;
} }
.list-cell:selected, .menu-item:hover { .list-cell:selected, .menu-item:hover, .combo-box-popup .list-view .list-cell:selected {
-fx-background-color: #454c4f; -fx-background-color: #454c4f;
} }

View File

@ -18,7 +18,7 @@
-fx-background-color: lightgray; -fx-background-color: lightgray;
} }
#message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item, #quick-select-list { #message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item, .combo-box-popup .list-view .list-cell, #quick-select-list {
-fx-background-color: #222222; -fx-background-color: #222222;
} }

View File

@ -22,9 +22,10 @@
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<?import javafx.stage.Screen?> <?import javafx.stage.Screen?>
<GridPane fx:id="scene" maxHeight="-Infinity" <GridPane maxHeight="-Infinity" maxWidth="-Infinity"
maxWidth="-Infinity" minHeight="400.0" minWidth="500.0" minHeight="400.0" minWidth="500.0"
prefHeight="${screen.visualBounds.height}" prefWidth="${screen.visualBounds.width}" prefHeight="${screen.visualBounds.height}"
prefWidth="${screen.visualBounds.width}"
xmlns="http://javafx.com/javafx/11.0.1" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.ChatScene"> fx:controller="envoy.client.ui.controller.ChatScene">
@ -160,44 +161,25 @@
fitHeight="43.0" fitWidth="43.0" pickOnBounds="true" fitHeight="43.0" fitWidth="43.0" pickOnBounds="true"
preserveRatio="true"> preserveRatio="true">
<HBox.margin> <HBox.margin>
<Insets left="15.0" top="5.0" /> <Insets left="15.0" top="5.0" right="10.0" />
</HBox.margin> </HBox.margin>
</ImageView> </ImageView>
<Label id="transparent-background" fx:id="contactLabel" <HBox id="transparent-background" fx:id="ownContactControl">
prefHeight="27.0" prefWidth="134.0">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<font>
<Font size="18.0" />
</font>
<HBox.margin>
<Insets left="10.0" top="5.0" />
</HBox.margin>
</Label>
<Region id="transparent-background" prefHeight="77.0"
prefWidth="115.0" />
<VBox id="transparent-background" alignment="CENTER_RIGHT"
prefHeight="200.0" prefWidth="100.0" spacing="5.0">
<children> <children>
<Button fx:id="settingsButton" mnemonicParsing="true" <Region id="transparent-background" prefWidth="120"
fx:id="spaceBetweenUserAndSettingsButton" />
<Button fx:id="settingsButton" mnemonicParsing="false"
onAction="#settingsButtonClicked" prefHeight="30.0" onAction="#settingsButtonClicked" prefHeight="30.0"
prefWidth="30.0" text=""> prefWidth="30.0" text="" alignment="CENTER">
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
<VBox.margin> <HBox.margin>
<Insets /> <Insets bottom="35.0" left="5.0" top="35.0"/>
</VBox.margin> </HBox.margin>
</Button> </Button>
</children> </children>
<HBox.margin> </HBox>
<Insets right="10.0" />
</HBox.margin>
<opaqueInsets>
<Insets />
</opaqueInsets>
</VBox>
</children> </children>
<GridPane.margin> <GridPane.margin>
<Insets bottom="1.0" right="1.0" /> <Insets bottom="1.0" right="1.0" />

View File

@ -22,14 +22,14 @@ public final class ConnectionManager implements ISocketIdListener {
* *
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
private Set<Long> pendingSockets = new HashSet<>(); private final Set<Long> pendingSockets = new HashSet<>();
/** /**
* Contains all socket IDs that have acquired a user ID as keys to these IDs. * Contains all socket IDs that have acquired a user ID as keys to these IDs.
* *
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
private Map<Long, Long> sockets = new HashMap<>(); private final Map<Long, Long> sockets = new HashMap<>();
private static ConnectionManager connectionManager = new ConnectionManager(); private static ConnectionManager connectionManager = new ConnectionManager();
@ -44,11 +44,11 @@ public final class ConnectionManager implements ISocketIdListener {
@Override @Override
public void socketCancelled(long socketID) { public void socketCancelled(long socketID) {
if (!pendingSockets.remove(socketID)) { if (!pendingSockets.remove(socketID)) {
// Notify contacts of this users offline-going // Notify contacts of this users offline-going
envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); final envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setStatus(UserStatus.OFFLINE);
user.setLastSeen(Instant.now()); user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user); 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

@ -46,8 +46,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Acquire a user object (or reject the handshake if that's impossible) // Acquire a user object (or reject the handshake if that's impossible)
User user = null; User user = null;
if (!credentials.isRegistration()) { if (!credentials.isRegistration()) try {
try {
user = persistenceManager.getUserByName(credentials.getIdentifier()); user = persistenceManager.getUserByName(credentials.getIdentifier());
// Check if the user is already online // Check if the user is already online
@ -67,7 +66,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN)); writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
return; return;
} }
} else { } else
// Check the password hash // Check the password hash
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) { if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
@ -75,13 +74,12 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
} } catch (final NoResultException e) {
} catch (NoResultException e) {
logger.info("The requested user does not exist."); logger.info("The requested user does not exist.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
} else { else {
// Validate user name // Validate user name
if (!Bounds.isValidContactName(credentials.getIdentifier())) { if (!Bounds.isValidContactName(credentials.getIdentifier())) {
@ -98,7 +96,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
logger.info("The requested user already exists."); logger.info("The requested user already exists.");
writeProxy.write(socketID, new HandshakeRejection(USERNAME_TAKEN)); writeProxy.write(socketID, new HandshakeRejection(USERNAME_TAKEN));
return; return;
} catch (NoResultException e) { } catch (final NoResultException e) {
// Creation of a new user // Creation of a new user
user = new User(); user = new User();
user.setName(credentials.getIdentifier()); user.setName(credentials.getIdentifier());
@ -115,18 +113,17 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
connectionManager.registerUser(user.getID(), socketID); connectionManager.registerUser(user.getID(), socketID);
// Change status and notify contacts about it // Change status and notify contacts about it
user.setStatus(ONLINE); UserStatusChangeProcessor.updateUserStatus(user, ONLINE);
UserStatusChangeProcessor.updateUserStatus(user);
// Process token request // Process token request
if (credentials.requestToken()) { if (credentials.requestToken()) {
String token; String token;
if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now())) { if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now()))
// Reuse existing token and delay expiration date // Reuse existing token and delay expiration date
token = user.getAuthToken(); token = user.getAuthToken();
} else { else {
// Generate new token // Generate new token
token = AuthTokenGenerator.nextToken(); token = AuthTokenGenerator.nextToken();
@ -141,13 +138,13 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
pendingMessages.removeIf(GroupMessage.class::isInstance); pendingMessages.removeIf(GroupMessage.class::isInstance);
logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "..."); logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");
for (var msg : pendingMessages) { for (final var msg : pendingMessages) {
final var msgCommon = msg.toCommon(); final var msgCommon = msg.toCommon();
if (msg.getCreationDate().isAfter(credentials.getLastSync())) { if (msg.getCreationDate().isAfter(credentials.getLastSync()))
// Sync without side effects // Sync without side effects
writeProxy.write(socketID, msgCommon); writeProxy.write(socketID, msgCommon);
} else if (msg.getStatus() == SENT) { else if (msg.getStatus() == SENT) {
// Send the message // Send the message
writeProxy.write(socketID, msgCommon); writeProxy.write(socketID, msgCommon);
@ -162,10 +159,10 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
} else writeProxy.write(socketID, new MessageStatusChange(msgCommon)); } else writeProxy.write(socketID, new MessageStatusChange(msgCommon));
} }
List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance().getPendingGroupMessages(user, credentials.getLastSync()); final List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance().getPendingGroupMessages(user, credentials.getLastSync());
logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user + "..."); logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user + "...");
for (var gmsg : pendingGroupMessages) { for (final var gmsg : pendingGroupMessages) {
final var gmsgCommon = gmsg.toCommon(); final var gmsgCommon = gmsg.toCommon();
// Deliver the message to the user if he hasn't received it yet // Deliver the message to the user if he hasn't received it yet
@ -189,20 +186,18 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
} }
PersistenceManager.getInstance().updateMessage(gmsg); PersistenceManager.getInstance().updateMessage(gmsg);
} else { } else
// Just send the message without updating if it was received in the past // Just send the message without updating if it was received in the past
writeProxy.write(socketID, gmsgCommon); writeProxy.write(socketID, gmsgCommon);
}
} else { } else {
// Sending group message status changes // Sending group message status changes
if (gmsg.getStatus() == SENT && gmsg.getLastStatusChangeDate().isAfter(gmsg.getCreationDate()) if (gmsg.getStatus() == SENT && gmsg.getLastStatusChangeDate().isAfter(gmsg.getCreationDate())
|| gmsg.getStatus() == RECEIVED && gmsg.getLastStatusChangeDate().isAfter(gmsg.getReceivedDate())) { || gmsg.getStatus() == RECEIVED && gmsg.getLastStatusChangeDate().isAfter(gmsg.getReceivedDate()))
gmsg.getMemberMessageStatus() gmsg.getMemberMessageStatus()
.forEach((memberID, memberStatus) -> writeProxy.write(socketID, .forEach((memberID, memberStatus) -> writeProxy.write(socketID,
new GroupMessageStatusChange(gmsg.getID(), memberStatus, gmsg.getLastStatusChangeDate(), memberID))); new GroupMessageStatusChange(gmsg.getID(), memberStatus, gmsg.getLastStatusChangeDate(), memberID)));
}
// Deliver just a status change instead of the whole message // Deliver just a status change instead of the whole message
if (gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate()) if (gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate())

View File

@ -28,18 +28,20 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
logger.warning("Received an unnecessary UserStatusChange"); logger.warning("Received an unnecessary UserStatusChange");
return; return;
} }
updateUserStatus(input); updateUserStatus(persistenceManager.getUserByID(input.getID()), input.get());
} }
/** /**
* Sets the {@link UserStatus} for a given user. Both offline contacts and * Sets the {@link UserStatus} for a given user. Both offline contacts and
* currently online contacts are notified. * currently online contacts are notified.
* *
* @param user the {@link UserStatusChange} that signals the change * @param user the user whose status has changed
* @param newStatus the new status of that user
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public static void updateUserStatus(User user) { public static void updateUserStatus(User user, UserStatus newStatus) {
user.setStatus(newStatus);
// Handling for newly logged in clients // Handling for newly logged in clients
persistenceManager.updateContact(user); persistenceManager.updateContact(user);
@ -48,12 +50,6 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
writeProxy.writeToOnlineContacts(user.getContacts(), new UserStatusChange(user.getID(), user.getStatus())); writeProxy.writeToOnlineContacts(user.getContacts(), new UserStatusChange(user.getID(), user.getStatus()));
} }
/**
* @param evt the {@link UserStatusChange}
* @since Envoy Server Standalone v0.1-alpha
*/
public static void updateUserStatus(UserStatusChange evt) { updateUserStatus(persistenceManager.getUserByID(evt.getID())); }
/** /**
* This method is only called by the LoginCredentialProcessor because every * This method is only called by the LoginCredentialProcessor because every
* user needs to login (open a socket) before changing his status. * user needs to login (open a socket) before changing his status.