Add Ability to Change the User Status Freely #90
@ -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
|
||||
* user names as keys
|
||||
|
21
client/src/main/java/envoy/client/event/OwnStatusChange.java
Normal 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.
|
||||
*
|
||||
delvh marked this conversation as resolved
|
||||
* @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); }
|
||||
}
|
@ -27,8 +27,6 @@ public final class AlertHelper {
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static void confirmAction(Alert alert, Runnable action) {
|
||||
alert.setHeight(225);
|
||||
alert.setWidth(400);
|
||||
alert.setHeaderText("");
|
||||
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
|
||||
else action.run();
|
||||
|
@ -1,15 +1,8 @@
|
||||
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.event.*;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.event.EnvoyCloseEvent;
|
||||
import envoy.client.ui.StatusTrayIcon;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import dev.kske.eventbus.EventBus;
|
||||
|
||||
@ -29,30 +22,21 @@ public final class ShutdownHelper {
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static void exit() {
|
||||
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) Context.getInstance().getStage().setIconified(true);
|
||||
public static void exit() { exit(false); }
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
mpk marked this conversation as resolved
mpk
commented
Why exactly do you want the option of force closing the application if you hardcoded it to false when you are calling the method? Why exactly do you want the option of force closing the application if you hardcoded it to false when you are calling the method?
delvh
commented
Because otherwise we encounter a problem in Because otherwise we encounter a problem in `StatusTrayIcon` as I've noticed:
The previous method only checked whether `settings.isHideOnClose()` and `StatusTryIcon.isSupported()` are `true`. If a StatusTrayIcon is shown, both are automatically true. Hence, nothing would happen when you click on Exit in the TrayIcon (except that the stage would be minimized, which is not what is wanted here). That's the reason why this extra method was needed.
mpk
commented
OK than it is clear OK than it is clear
|
||||
if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) Context.getInstance().getStage().setIconified(true);
|
||||
else {
|
||||
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import javafx.stage.Stage;
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.event.*;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.util.UserUtil;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
@ -126,9 +128,22 @@ public final class SceneContext implements EventListener {
|
||||
// Add the option to exit Linux-like with "Control" + "Q" or "Alt" + "F4"
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
|
||||
|
||||
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
|
||||
if (sceneInfo != SceneInfo.LOGIN_SCENE)
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
|
||||
if (sceneInfo != SceneInfo.LOGIN_SCENE) {
|
||||
kske marked this conversation as resolved
kske
commented
(yes, these are both rhetorical questions) 1. Are these key combinations documented anywhere?
2. Can we stop putting scene specific code inside `SceneContext`, that could be completely generic as to what scenes it displays, except for being littered with code specific to `LoginScene` which nobody would expect here?
(yes, these are both rhetorical questions)
delvh
commented
to 1. While I know what you mean, I'd still say that this goes beyond the scope of this branch. It seems as if one of the next branches should cleanup that in SceneContext. I could do it in that branch, but than this branch will have two completely separate tasks on it (and it would be eve larger than it already is). to 1. While I know what you mean, I'd still say that this goes beyond the scope of this branch.
to 2. While I know what you mean, I'd still say that this goes beyond the scope of this branch.
It seems as if one of the next branches should cleanup that in SceneContext. I could do it in that branch, but than this branch will have two completely separate tasks on it (and it would be eve larger than it already is).
kske
commented
No, I think you are right. The cleanup should be performed inside a separate branch. No, I think you are right. The cleanup should be performed inside a separate branch.
delvh
commented
See #91 See #91
|
||||
|
||||
// Add the option to logout using "Control"+"Shift"+"L"
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), UserUtil::logout);
|
||||
|
||||
// Add the option to change status using "Control" + "Shift" +
|
||||
// (o)F(fline)/ A(way)/ B(usy)/(o)N(line)
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.OFFLINE));
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.AWAY));
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.BUSY));
|
||||
accelerators.put(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.ONLINE));
|
||||
}
|
||||
|
||||
// Add the option to open the settings scene with "Control"+"S", if being in
|
||||
// chat scene
|
||||
|
@ -113,9 +113,13 @@ public final class Startup extends Application {
|
||||
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
|
||||
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
|
||||
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
|
||||
final var originalStatus = localDB.getUser().getStatus();
|
||||
try {
|
||||
client.performHandshake(credentials, cacheMap);
|
||||
if (client.isOnline()) {
|
||||
|
||||
// Restore the original status as the server automatically returns status ONLINE
|
||||
client.getSender().setStatus(originalStatus);
|
||||
loadChatScene();
|
||||
client.initReceiver(localDB, cacheMap);
|
||||
return true;
|
||||
@ -170,7 +174,8 @@ public final class Startup extends Application {
|
||||
private static void loadChatScene() {
|
||||
|
||||
// Set client user in local database
|
||||
localDB.setUser(client.getSender());
|
||||
final var user = client.getSender();
|
||||
localDB.setUser(user);
|
||||
|
||||
// Initialize chats in local database
|
||||
try {
|
||||
@ -184,8 +189,13 @@ public final class Startup extends Application {
|
||||
|
||||
context.initWriteProxy();
|
||||
|
||||
if (client.isOnline()) context.getWriteProxy().flushCache();
|
||||
else
|
||||
if (client.isOnline()) {
|
||||
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
|
||||
localDB.getChats()
|
||||
.stream()
|
||||
|
@ -6,9 +6,11 @@ import java.awt.TrayIcon.MessageType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import envoy.client.event.OwnStatusChange;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.util.IconUtil;
|
||||
import envoy.client.util.*;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
import dev.kske.eventbus.Event;
|
||||
@ -51,16 +53,32 @@ public final class StatusTrayIcon implements EventListener {
|
||||
trayIcon.setImageAutoSize(true);
|
||||
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");
|
||||
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit());
|
||||
// Adding the exit menu item
|
||||
final var exitMenuItem = new MenuItem("Exit");
|
||||
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true));
|
||||
popup.add(exitMenuItem);
|
||||
|
||||
// Adding the logout menu item
|
||||
final var logoutMenuItem = new MenuItem("Logout");
|
||||
logoutMenuItem.addActionListener(evt -> { hide(); Platform.runLater(UserUtil::logout); });
|
||||
delvh marked this conversation as resolved
Outdated
kske
commented
This appears to be an error. This appears to be an error.
delvh
commented
Ctrl C, Ctrl V. Ctrl C, Ctrl V.
|
||||
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);
|
||||
|
||||
// Only display messages if the stage is not focused
|
||||
stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
|
||||
// Only display messages if the stage is not focused and the current user status
|
||||
// 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
|
||||
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); }
|
||||
|
||||
@Event
|
||||
delvh marked this conversation as resolved
Outdated
kske
commented
I think the point of the "busy" status is that the user does not want to be disturbed, so maybe put a check in place that prevents the notification in this status. Also, could we keep the previous formatting of this section? The new one looks very confusing to me. I think the point of the "busy" status is that the user does not want to be disturbed, so maybe put a check in place that prevents the notification in this status.
Also, could we keep the previous formatting of this section? The new one looks very confusing to me.
|
||||
private void onOwnStatusChange(OwnStatusChange statusChange) { displayMessages = !statusChange.get().equals(UserStatus.BUSY); }
|
||||
|
||||
@Event
|
||||
private void onMessage(Message message) {
|
||||
if (displayMessages) trayIcon.displayMessage(
|
||||
message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received",
|
||||
message.getText(),
|
||||
MessageType.INFO);
|
||||
if (displayMessages) trayIcon
|
||||
.displayMessage(message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received"
|
||||
: "New message received", message.getText(), MessageType.INFO);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import java.util.Random;
|
||||
import java.util.function.*;
|
||||
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 envoy.client.data.Context;
|
||||
@ -12,8 +13,9 @@ import envoy.client.data.commands.*;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.ui.controller.ChatScene;
|
||||
import envoy.client.util.MessageUtil;
|
||||
import envoy.client.util.*;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
@ -52,7 +54,7 @@ public final class ChatSceneCommands {
|
||||
.build("dabr");
|
||||
|
||||
// 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
|
||||
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")
|
||||
.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);
|
||||
kske marked this conversation as resolved
kske
commented
We implement a standardized way of handling system command failures. Wou don't be able to display an alert if Envoy is running on the command line. We implement a standardized way of handling system command failures. Wou don't be able to display an alert if Envoy is running on the command line.
delvh
commented
While I agree with you, there is a reason why I accepted your solution of automatically displaying an alert in case of a system command only with gritted teeth. But, I'd say this is a branch of its own again and far beyond the scope of this branch. While I agree with you, there is a reason why I accepted your solution of automatically displaying an alert in case of a system command only with gritted teeth.
For now, this has to suffice. In the future I can well imagine that we can pass a consumer accepting an error to the calling method to handle errors in a command.
But, I'd say this is a branch of its own again and far beyond the scope of this branch.
kske
commented
I would suggest to introduce a new exception and catch that at a convenient place. No need for consumers. I however do agree, that this is beyond the scope of this branch. I would suggest to introduce a new exception and catch that at a convenient place. No need for consumers.
I however do agree, that this is beyond the scope of this branch.
|
||||
alert.setContentText("Please provide an existing status");
|
||||
alert.showAndWait();
|
||||
}
|
||||
}).setDescription("Changes your status to the given status.").setNumberOfArguments(1).setDefaults("").build("status");
|
||||
kske
commented
Any reason why the default is "offline"? This should be documented. Any reason why the default is "offline"? This should be documented.
delvh
commented
to 1. Well, normally when executing this command I'd argue that you want to change it, (from "ONLINE" in the standard case) and "OFFLINE" seemed like the most reasonable value. If you want another value, I can change that, i.e. BUSY? to 1. Well, normally when executing this command I'd argue that you want to change it, (from "ONLINE" in the standard case) and "OFFLINE" seemed like the most reasonable value. If you want another value, I can change that, i.e. BUSY?
to 2. Where should I document it? Our wiki is a laughable joke and the only location where it could be partially useful for the consumer would be inside the description of this system command. Should I add it there?
kske
commented
1. In my opinion, there is no sensible default. Rather, the parameter should be mandatory.
2. If you would put the documentation inside the wiki, it would become less of a joke. Maybe it's also possible to display the default values to the user in some way?
delvh
commented
1. Okay, I can remove the default value. Even though that completely defies the purpose of default values.
2. I've already thought about doing that (inspired by Skype)
but that'll require some major changes to the underlying system command system (=>another branch)
kske
commented
To my understanding, default values are only used if there is a default that actually makes sense. In case of status changes, I don't see, why a specific status would be the most frequently used one. To my understanding, default values are only used if there is a default that actually makes sense. In case of status changes, I don't see, why a specific status would be the most frequently used one.
delvh
commented
Have a look at the declaration again. Already fixed. Have a look at the declaration again. Already fixed.
|
||||
|
||||
// Selection of a new message initialization
|
||||
messageDependantAction("s",
|
||||
m -> { messageList.getSelectionModel().clearSelection(); messageList.getSelectionModel().select(m); },
|
||||
|
@ -2,9 +2,8 @@ package envoy.client.ui.control;
|
||||
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.util.IconUtil;
|
||||
@ -23,6 +22,8 @@ public final class ChatControl extends HBox {
|
||||
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
|
||||
|
||||
/**
|
||||
* Creates a new {@code ChatControl}.
|
||||
*
|
||||
* @param chat the chat to display
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -31,13 +32,7 @@ public final class ChatControl extends HBox {
|
||||
setPadding(new Insets(0, 0, 3, 0));
|
||||
|
||||
// Profile picture
|
||||
ImageView contactProfilePic = new ImageView(chat instanceof GroupChat ? groupIcon : userIcon);
|
||||
final var clip = new Rectangle();
|
||||
clip.setWidth(32);
|
||||
clip.setHeight(32);
|
||||
clip.setArcHeight(32);
|
||||
clip.setArcWidth(32);
|
||||
contactProfilePic.setClip(clip);
|
||||
final var contactProfilePic = new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32);
|
||||
kske marked this conversation as resolved
kske
commented
As we are using a dedicated component for this, why not handle the selection of the correct profile picture inside As we are using a dedicated component for this, why not handle the selection of the correct profile picture inside `ProfilePicImageView`?
delvh
commented
Well, if I do that right inside this branch, then I'll add a (transient) Image to Chat (that will be for now always null) whose value I'm going to null check inside the mentioned constructor. Should I really do that here? Well, if I do that right inside this branch, then I'll add a (transient) Image to Chat (that will be for now always null) whose value I'm going to null check inside the mentioned constructor. Should I really do that here?
kske
commented
I don't see why the solution would be that complicated, but you might also resolve this in a separate branch. I don't see why the solution would be that complicated, but you might also resolve this in a separate branch.
|
||||
getChildren().add(contactProfilePic);
|
||||
|
||||
// Spacing
|
||||
|
@ -15,25 +15,37 @@ import envoy.data.*;
|
||||
*/
|
||||
public final class ContactControl extends VBox {
|
||||
|
||||
private final Contact contact;
|
||||
|
||||
/**
|
||||
* @param contact the contact to display
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ContactControl(Contact contact) {
|
||||
this.contact = contact;
|
||||
|
||||
// Name label
|
||||
final var nameLabel = new Label(contact.getName());
|
||||
getChildren().add(nameLabel);
|
||||
kske
commented
I did not locate the source of the problem, but the group size label changes its color occasionally when a member of the group changes his status to "busy". If this is intended, how would we go about multiple group members with different statuses? I did not locate the source of the problem, but the group size label changes its color occasionally when a member of the group changes his status to "busy".
If this is intended, how would we go about multiple group members with different statuses?
delvh
commented
Wait what? How's that possible? That shouldn't be possible at all. Wait what? How's that possible? That shouldn't be possible at all.
delvh
commented
I'd love to hear how that problem is caused because I have no clue what could cause it. I'd love to hear how that problem is caused because I have no clue what could cause it.
delvh
commented
Are you sure you managed to change the color of the group size label? Are you sure you managed to change the color of the group size label?
I've tried reproducing using the instructions you just gave, but could not reproduce it.
kske
commented
I will provide some screenshots in a few hours. I will provide some screenshots in a few hours.
kske
commented
How to reproduce: Preparation:
Step-by-step reproduction:
There might be a more general reproduction, however this is what I tried at the moment. How to reproduce:
Preparation:
- 2 users inside a group
Step-by-step reproduction:
1. Start the server and both clients
2. Select the group chat in Client A and the chat with Client A in Client B
3. Change the status of Client A to "busy"
4. Select the group chat in Client B
5. Observe the red member count over the chat list
![grafik](/attachments/66a0e9b5-d075-4a52-a4d1-53c3a7e15ae6)
There might be a more general reproduction, however this is what I tried at the moment.
delvh
commented
The issue is that you rejected my idea to use a separate component (ContactControl) there. That would have prevented that. The issue is that you rejected my idea to use a separate component (ContactControl) there. That would have prevented that.
delvh
commented
Because we had a case here of not reinitializing the value correctly. Because we had a case here of not reinitializing the value correctly.
|
||||
|
||||
// Online status (user) or member count (group)
|
||||
if (contact instanceof User) {
|
||||
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"));
|
||||
}
|
||||
getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact) : new GroupSizeLabel((Group) contact));
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -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"); }
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.fxml.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.image.*;
|
||||
@ -29,7 +30,7 @@ import envoy.client.event.*;
|
||||
import envoy.client.net.*;
|
||||
import envoy.client.ui.*;
|
||||
import envoy.client.ui.chatscene.*;
|
||||
import envoy.client.ui.control.ChatControl;
|
||||
import envoy.client.ui.control.*;
|
||||
import envoy.client.ui.listcell.*;
|
||||
import envoy.client.util.*;
|
||||
import envoy.data.*;
|
||||
@ -51,12 +52,6 @@ import dev.kske.eventbus.Event;
|
||||
*/
|
||||
public final class ChatScene implements EventListener, Restorable {
|
||||
|
||||
@FXML
|
||||
private GridPane scene;
|
||||
|
||||
@FXML
|
||||
private Label contactLabel;
|
||||
|
||||
@FXML
|
||||
private ListView<Message> messageList;
|
||||
|
||||
@ -84,33 +79,33 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
@FXML
|
||||
private Button newContactButton;
|
||||
|
||||
@FXML
|
||||
private TextArea messageTextArea;
|
||||
|
||||
@FXML
|
||||
private Label remainingChars;
|
||||
|
||||
@FXML
|
||||
private Label infoLabel;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteContactMenuItem;
|
||||
|
||||
@FXML
|
||||
private ImageView attachmentView;
|
||||
|
||||
@FXML
|
||||
private Label topBarContactLabel;
|
||||
|
||||
@FXML
|
||||
private Label topBarStatusLabel;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteContactMenuItem;
|
||||
|
||||
@FXML
|
||||
private ImageView attachmentView;
|
||||
|
||||
@FXML
|
||||
private ImageView clientProfilePic;
|
||||
|
||||
@FXML
|
||||
private ImageView recipientProfilePic;
|
||||
|
||||
@FXML
|
||||
private TextArea messageTextArea;
|
||||
|
||||
@FXML
|
||||
private TextArea contactSearch;
|
||||
|
||||
@ -129,6 +124,12 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
@FXML
|
||||
private HBox contactSpecificOnlineOperations;
|
||||
|
||||
@FXML
|
||||
private HBox ownContactControl;
|
||||
|
||||
@FXML
|
||||
private Region spaceBetweenUserAndSettingsButton;
|
||||
|
||||
private Chat currentChat;
|
||||
private FilteredList<Chat> chats;
|
||||
private boolean recording;
|
||||
@ -188,10 +189,15 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
clientProfilePic.setClip(clip);
|
||||
|
||||
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);
|
||||
delvh marked this conversation as resolved
delvh
commented
@DieGurke The region you requested is already here, but for some strange reason it doesn't work as expected. @DieGurke The region you requested is already here, but for some strange reason it doesn't work as expected.
Any idea why?
Feel free to take on that challenge, because I've tried it for about twenty minutes and could not get it to work.
mpk
commented
I could't fix it neither. Preferably you open an issue for that. I could't fix it neither. Preferably you open an issue for that.
|
||||
generateOwnStatusControl();
|
||||
|
||||
delvh marked this conversation as resolved
Outdated
kske
commented
This seems to be a leftover from development. This seems to be a leftover from development.
|
||||
Platform.runLater(() -> {
|
||||
final var online = client.isOnline();
|
||||
|
||||
// no check will be performed in case it has already been disabled - a negative
|
||||
// GroupCreationResult might have been returned
|
||||
if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online);
|
||||
@ -251,8 +257,19 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
.ifPresent(msg -> Platform.runLater(messageList::refresh));
|
||||
}
|
||||
|
||||
@Event(eventType = UserStatusChange.class)
|
||||
private void onUserStatusChange() { Platform.runLater(chatList::refresh); }
|
||||
@Event
|
||||
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
|
||||
private void onContactOperation(ContactOperation operation) {
|
||||
@ -297,8 +314,9 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
|
||||
messageList.setCellFactory(MessageListCell::new);
|
||||
// TODO: cache image
|
||||
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||
if (currentChat != null)
|
||||
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||
}
|
||||
|
||||
@Event(eventType = Logout.class, priority = 200)
|
||||
@ -359,6 +377,7 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
} else {
|
||||
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
|
||||
topBarStatusLabel.getStyleClass().clear();
|
||||
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||
}
|
||||
final var clip = new Rectangle();
|
||||
@ -699,6 +718,21 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
attachmentView.setVisible(visible);
|
||||
}
|
||||
|
||||
@Event(eventType = OwnStatusChange.class, priority = 50)
|
||||
private void generateOwnStatusControl() {
|
||||
delvh marked this conversation as resolved
kske
commented
You could annotate this as an event handler directly. You could annotate this as an event handler directly.
|
||||
|
||||
mpk
commented
I do not see the point of doing this as you can just check if it is null if I understand that correctly. If not please correct and explane the exact purpose to me. I do not see the point of doing this as you can just check if it is null if I understand that correctly. If not please correct and explane the exact purpose to me.
delvh
commented
As you can see a few lines above, this method is annotated with an Event: It gets called every time the user changes his own status. But thinking about it, it may be possible, however I do not know if it'll work as bug-free as this implementation: As soon as you initialize this variable somewhere else, you might encounter a bug that duplicates it. As you can see a few lines above, this method is annotated with an Event: It gets called every time the user changes his own status.
Hence I always have to replace it then.
And for the first part: I cannot do that because initially this component is no part of the HBox (you remember, the thing with importing the by now outdated jar into the SceneBuilder...). To avoid that, it gets dynamically added.
But thinking about it, it may be possible, however I do not know if it'll work as bug-free as this implementation: As soon as you initialize this variable somewhere else, you might encounter a bug that duplicates it.
kske
commented
What if, instead of regenerating the whole control, we would just change the displayed information in the event handler? What if, instead of regenerating the whole control, we would just change the displayed information in the event handler?
delvh
commented
It could be possible if we make some changes to the underlying It could be possible if we make some changes to the underlying `ContactControl`.
The question however is: Should it be done?
delvh
commented
Oh, and also: there is a difference between ownUserControl and ownContactControl. Oh, and also: there is a difference between ownUserControl and ownContactControl.
delvh
commented
Better? Better?
mpk
commented
Yes better Yes better
|
||||
// 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
|
||||
|
||||
@FXML
|
||||
|
@ -5,8 +5,6 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
|
||||
/**
|
||||
* Displays options for downloading {@link envoy.data.Attachment}s.
|
||||
*
|
||||
@ -47,7 +45,7 @@ public final class DownloadSettingsPane extends SettingsPane {
|
||||
final var directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Select the directory where attachments should be saved to");
|
||||
directoryChooser.setInitialDirectory(settings.getDownloadLocation());
|
||||
final var selectedDirectory = directoryChooser.showDialog(Context.getInstance().getSceneContext().getStage());
|
||||
final var selectedDirectory = directoryChooser.showDialog(context.getSceneContext().getStage());
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
currentPath.setText(selectedDirectory.getAbsolutePath());
|
||||
|
@ -4,8 +4,8 @@ import javafx.scene.control.*;
|
||||
|
||||
import envoy.client.data.SettingsItem;
|
||||
import envoy.client.event.ThemeChangeEvent;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.ui.StatusTrayIcon;
|
||||
import envoy.client.util.UserUtil;
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
import dev.kske.eventbus.EventBus;
|
||||
@ -57,14 +57,13 @@ public final class GeneralSettingsPane extends SettingsPane {
|
||||
|
||||
final var statusComboBox = new ComboBox<UserStatus>();
|
||||
statusComboBox.getItems().setAll(UserStatus.values());
|
||||
statusComboBox.setValue(UserStatus.ONLINE);
|
||||
statusComboBox.setValue(context.getLocalDB().getUser().getStatus());
|
||||
statusComboBox.setTooltip(new Tooltip("Change your current status"));
|
||||
// TODO add action when value is changed
|
||||
statusComboBox.setOnAction(e -> {});
|
||||
statusComboBox.setOnAction(e -> UserUtil.changeStatus(statusComboBox.getValue()));
|
||||
getChildren().add(statusComboBox);
|
||||
|
||||
kske
commented
How is this possible? How is this possible?
delvh
commented
Artifact from when I called it using the selectionModel before stumbling upon this method. Artifact from when I called it using the selectionModel before stumbling upon this method.
Yes, agreed, their Javadoc doesn't mention anything about it,so I'll remove it.
|
||||
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");
|
||||
logoutTooltip.setWrapText(true);
|
||||
logoutButton.setTooltip(logoutTooltip);
|
||||
|
@ -5,7 +5,6 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.net.Client;
|
||||
|
||||
/**
|
||||
@ -20,7 +19,7 @@ import envoy.client.net.Client;
|
||||
*/
|
||||
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.");
|
||||
|
||||
|
@ -2,7 +2,7 @@ package envoy.client.ui.settings;
|
||||
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.data.*;
|
||||
|
||||
/**
|
||||
* @author Kai S. K. Engelbart
|
||||
@ -12,7 +12,8 @@ public abstract class SettingsPane extends VBox {
|
||||
|
||||
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; }
|
||||
|
||||
|
@ -14,7 +14,6 @@ import javafx.scene.input.InputEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.ui.control.ProfilePicImageView;
|
||||
import envoy.client.util.IconUtil;
|
||||
import envoy.event.*;
|
||||
@ -66,7 +65,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
|
||||
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
|
||||
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) {
|
||||
|
||||
|
64
client/src/main/java/envoy/client/util/UserUtil.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
-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;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
-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;
|
||||
}
|
||||
|
||||
|
@ -22,14 +22,15 @@
|
||||
<?import javafx.scene.text.Font?>
|
||||
<?import javafx.stage.Screen?>
|
||||
|
||||
<GridPane fx:id="scene" maxHeight="-Infinity"
|
||||
maxWidth="-Infinity" minHeight="400.0" minWidth="500.0"
|
||||
prefHeight="${screen.visualBounds.height}" prefWidth="${screen.visualBounds.width}"
|
||||
<GridPane maxHeight="-Infinity" maxWidth="-Infinity"
|
||||
minHeight="400.0" minWidth="500.0"
|
||||
prefHeight="${screen.visualBounds.height}"
|
||||
prefWidth="${screen.visualBounds.width}"
|
||||
xmlns="http://javafx.com/javafx/11.0.1"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="envoy.client.ui.controller.ChatScene">
|
||||
<fx:define>
|
||||
<Screen fx:factory="getPrimary" fx:id="screen" />
|
||||
<Screen fx:factory="getPrimary" fx:id="screen" />
|
||||
</fx:define>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="NEVER"
|
||||
@ -160,44 +161,25 @@
|
||||
fitHeight="43.0" fitWidth="43.0" pickOnBounds="true"
|
||||
preserveRatio="true">
|
||||
<HBox.margin>
|
||||
<Insets left="15.0" top="5.0" />
|
||||
<Insets left="15.0" top="5.0" right="10.0" />
|
||||
</HBox.margin>
|
||||
</ImageView>
|
||||
<Label id="transparent-background" fx:id="contactLabel"
|
||||
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">
|
||||
<HBox id="transparent-background" fx:id="ownContactControl">
|
||||
<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"
|
||||
prefWidth="30.0" text="">
|
||||
prefWidth="30.0" text="" alignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<VBox.margin>
|
||||
<Insets />
|
||||
</VBox.margin>
|
||||
<HBox.margin>
|
||||
<Insets bottom="35.0" left="5.0" top="35.0"/>
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
</children>
|
||||
<HBox.margin>
|
||||
<Insets right="10.0" />
|
||||
</HBox.margin>
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
</VBox>
|
||||
</HBox>
|
||||
</children>
|
||||
<GridPane.margin>
|
||||
<Insets bottom="1.0" right="1.0" />
|
||||
|
@ -22,14 +22,14 @@ public final class ConnectionManager implements ISocketIdListener {
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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();
|
||||
|
||||
@ -44,11 +44,11 @@ public final class ConnectionManager implements ISocketIdListener {
|
||||
@Override
|
||||
public void socketCancelled(long socketID) {
|
||||
if (!pendingSockets.remove(socketID)) {
|
||||
|
||||
// Notify contacts of this users offline-going
|
||||
envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
|
||||
user.setStatus(UserStatus.OFFLINE);
|
||||
final envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
|
||||
user.setLastSeen(Instant.now());
|
||||
UserStatusChangeProcessor.updateUserStatus(user);
|
||||
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
|
||||
|
||||
// Remove the socket
|
||||
sockets.entrySet().removeIf(e -> e.getValue() == socketID);
|
||||
|
@ -46,42 +46,40 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
|
||||
// Acquire a user object (or reject the handshake if that's impossible)
|
||||
User user = null;
|
||||
if (!credentials.isRegistration()) {
|
||||
try {
|
||||
user = persistenceManager.getUserByName(credentials.getIdentifier());
|
||||
if (!credentials.isRegistration()) try {
|
||||
user = persistenceManager.getUserByName(credentials.getIdentifier());
|
||||
|
||||
// Check if the user is already online
|
||||
if (connectionManager.isOnline(user.getID())) {
|
||||
logger.warning(user + " is already online!");
|
||||
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticate with password or token
|
||||
if (credentials.usesToken()) {
|
||||
|
||||
// Check the token
|
||||
if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now())
|
||||
|| !user.getAuthToken().equals(credentials.getPassword())) {
|
||||
logger.info(user + " tried to use an invalid token.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
// Check the password hash
|
||||
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
|
||||
logger.info(user + " has entered the wrong password.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (NoResultException e) {
|
||||
logger.info("The requested user does not exist.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||
// Check if the user is already online
|
||||
if (connectionManager.isOnline(user.getID())) {
|
||||
logger.warning(user + " is already online!");
|
||||
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
// Authenticate with password or token
|
||||
if (credentials.usesToken()) {
|
||||
|
||||
// Check the token
|
||||
if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now())
|
||||
|| !user.getAuthToken().equals(credentials.getPassword())) {
|
||||
logger.info(user + " tried to use an invalid token.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
|
||||
// Check the password hash
|
||||
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
|
||||
logger.info(user + " has entered the wrong password.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||
return;
|
||||
}
|
||||
} catch (final NoResultException e) {
|
||||
logger.info("The requested user does not exist.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
|
||||
// Validate user name
|
||||
if (!Bounds.isValidContactName(credentials.getIdentifier())) {
|
||||
@ -98,7 +96,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
logger.info("The requested user already exists.");
|
||||
writeProxy.write(socketID, new HandshakeRejection(USERNAME_TAKEN));
|
||||
return;
|
||||
} catch (NoResultException e) {
|
||||
} catch (final NoResultException e) {
|
||||
// Creation of a new user
|
||||
user = new User();
|
||||
user.setName(credentials.getIdentifier());
|
||||
@ -115,18 +113,17 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
connectionManager.registerUser(user.getID(), socketID);
|
||||
|
||||
// Change status and notify contacts about it
|
||||
user.setStatus(ONLINE);
|
||||
UserStatusChangeProcessor.updateUserStatus(user);
|
||||
UserStatusChangeProcessor.updateUserStatus(user, ONLINE);
|
||||
|
||||
// Process token request
|
||||
if (credentials.requestToken()) {
|
||||
String token;
|
||||
|
||||
if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now())) {
|
||||
if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now()))
|
||||
mpk marked this conversation as resolved
mpk
commented
Are you sure all of the code in the body of this if statement gets executed if you remove the curly brackets? Are you sure all of the code in the body of this if statement gets executed if you remove the curly brackets?
delvh
commented
Was my formatter, so I'd guess so. Otherwise blame Eclipse. Was my formatter, so I'd guess so. Otherwise blame Eclipse.
Same for below.
delvh
commented
And yes, it's only one statement, so why shouldn't it? And yes, it's only one statement, so why shouldn't it?
mpk
commented
Because of the comment but I guess it does not count Because of the comment but I guess it does not count
|
||||
|
||||
// Reuse existing token and delay expiration date
|
||||
token = user.getAuthToken();
|
||||
} else {
|
||||
else {
|
||||
|
||||
// Generate new token
|
||||
token = AuthTokenGenerator.nextToken();
|
||||
@ -141,13 +138,13 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
pendingMessages.removeIf(GroupMessage.class::isInstance);
|
||||
logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");
|
||||
|
||||
for (var msg : pendingMessages) {
|
||||
for (final var msg : pendingMessages) {
|
||||
final var msgCommon = msg.toCommon();
|
||||
if (msg.getCreationDate().isAfter(credentials.getLastSync())) {
|
||||
if (msg.getCreationDate().isAfter(credentials.getLastSync()))
|
||||
mpk marked this conversation as resolved
mpk
commented
same as above same as above
delvh
commented
Still only one statement. Still only one statement.
|
||||
|
||||
// Sync without side effects
|
||||
writeProxy.write(socketID, msgCommon);
|
||||
} else if (msg.getStatus() == SENT) {
|
||||
else if (msg.getStatus() == SENT) {
|
||||
|
||||
// Send the message
|
||||
writeProxy.write(socketID, msgCommon);
|
||||
@ -162,10 +159,10 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
} 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 + "...");
|
||||
|
||||
for (var gmsg : pendingGroupMessages) {
|
||||
for (final var gmsg : pendingGroupMessages) {
|
||||
final var gmsgCommon = gmsg.toCommon();
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
} else
|
||||
|
||||
// Just send the message without updating if it was received in the past
|
||||
writeProxy.write(socketID, gmsgCommon);
|
||||
}
|
||||
} else {
|
||||
|
||||
// Sending group message status changes
|
||||
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()))
|
||||
mpk marked this conversation as resolved
mpk
commented
same as above same as above
delvh
commented
Still only one statement Still only one statement
|
||||
gmsg.getMemberMessageStatus()
|
||||
.forEach((memberID, memberStatus) -> writeProxy.write(socketID,
|
||||
new GroupMessageStatusChange(gmsg.getID(), memberStatus, gmsg.getLastStatusChangeDate(), memberID)));
|
||||
}
|
||||
|
||||
// Deliver just a status change instead of the whole message
|
||||
if (gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate())
|
||||
|
@ -28,18 +28,20 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
|
||||
logger.warning("Received an unnecessary UserStatusChange");
|
||||
return;
|
||||
}
|
||||
updateUserStatus(input);
|
||||
updateUserStatus(persistenceManager.getUserByID(input.getID()), input.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link UserStatus} for a given user. Both offline contacts and
|
||||
* 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
|
||||
*/
|
||||
|
||||
public static void updateUserStatus(User user) {
|
||||
public static void updateUserStatus(User user, UserStatus newStatus) {
|
||||
user.setStatus(newStatus);
|
||||
|
||||
// Handling for newly logged in clients
|
||||
persistenceManager.updateContact(user);
|
||||
@ -48,12 +50,6 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
|
||||
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
|
||||
* user needs to login (open a socket) before changing his status.
|
||||
|
This construction is complicated even for my standards.
What about "Signifies a manual status change of the client user".