diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index b1c2beb..81d6d97 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -7,8 +7,10 @@ import java.util.List; import java.util.Objects; import envoy.client.net.WriteProxy; -import envoy.data.*; +import envoy.data.Contact; +import envoy.data.Message; import envoy.data.Message.MessageStatus; +import envoy.data.User; import envoy.event.MessageStatusChange; /** @@ -31,6 +33,11 @@ public class Chat implements Serializable { protected int unreadAmount; + /** + * Stores the last time an {@link envoy.event.IsTyping} event has been sent. + */ + protected transient long lastWritingEvent; + private static final long serialVersionUID = 1L; /** @@ -41,16 +48,14 @@ public class Chat implements Serializable { * @param recipient the user who receives the messages * @since Envoy Client v0.1-alpha */ - public Chat(Contact recipient) { - this.recipient = recipient; - } + public Chat(Contact recipient) { this.recipient = recipient; } @Override public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); } /** * Generates a hash code based on the recipient. - * + * * @since Envoy Client v0.1-beta */ @Override @@ -58,14 +63,14 @@ public class Chat implements Serializable { /** * Tests equality to another object based on the recipient. - * + * * @since Envoy Client v0.1-beta */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Chat)) return false; - Chat other = (Chat) obj; + final Chat other = (Chat) obj; return Objects.equals(recipient, other.recipient); } @@ -101,7 +106,7 @@ public class Chat implements Serializable { /** * Inserts a message at the correct place according to its creation date. - * + * * @param message the message to insert * @since Envoy Client v0.1-beta */ @@ -116,13 +121,13 @@ public class Chat implements Serializable { /** * Increments the amount of unread messages. - * + * * @since Envoy Client v0.1-beta */ public void incrementUnreadAmount() { unreadAmount++; } /** - * @return the amount of unread mesages in this chat + * @return the amount of unread messages in this chat * @since Envoy Client v0.1-beta */ public int getUnreadAmount() { return unreadAmount; } @@ -140,14 +145,16 @@ public class Chat implements Serializable { public Contact getRecipient() { return recipient; } /** - * @return whether this {@link Chat} points at a {@link User} - * @since Envoy Client v0.1-beta + * @return the last known time a {@link envoy.event.IsTyping} event has been + * sent + * @since Envoy Client v0.2-beta */ - public boolean isUserChat() { return recipient instanceof User; } + public long getLastWritingEvent() { return lastWritingEvent; } /** - * @return whether this {@link Chat} points at a {@link Group} - * @since Envoy Client v0.1-beta + * Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}. + * + * @since Envoy Client v0.2-beta */ - public boolean isGroupChat() { return recipient instanceof Group; } + public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); } } diff --git a/client/src/main/java/envoy/client/net/Client.java b/client/src/main/java/envoy/client/net/Client.java index a01468f..b642141 100644 --- a/client/src/main/java/envoy/client/net/Client.java +++ b/client/src/main/java/envoy/client/net/Client.java @@ -155,6 +155,9 @@ public class Client implements Closeable { // Process group size changes receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); }); + // Process IsTyping events + receiver.registerProcessor(IsTyping.class, eventBus::dispatch); + // Send event eventBus.register(SendEvent.class, evt -> { try { diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java index a05150c..f9ba2d7 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -33,6 +33,7 @@ import envoy.client.data.audio.AudioRecorder; import envoy.client.data.commands.SystemCommandBuilder; import envoy.client.data.commands.SystemCommandsMap; import envoy.client.event.MessageCreationEvent; +import envoy.client.event.SendEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; import envoy.client.ui.*; @@ -444,10 +445,28 @@ public final class ChatScene implements Restorable { private void checkKeyCombination(KeyEvent e) { // Checks whether the text is too long messageTextUpdated(); + // Sending an IsTyping event if none has been sent for + // IsTyping#millisecondsActive + if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { + eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID()))); + currentChat.lastWritingEventWasNow(); + } // Automatic sending of messages via (ctrl +) enter checkPostConditions(e); } + /** + * Returns the id that should be used to send things to the server: + * the id of 'our' {@link User} if the recipient of that object is another User, + * else the id of the {@link Group} 'our' user is sending to. + * + * @return an id that can be sent to the server + * @since Envoy Client v0.2-beta + */ + private long getChatID() { + return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID(); + } + /** * @param e the keys that have been pressed * @since Envoy Client v0.1-beta @@ -515,7 +534,7 @@ public final class ChatScene implements Restorable { updateInfoLabel("You need to go online to send more messages", "infoLabel-error"); return; } - final var text = messageTextArea.getText().strip(); + final var text = messageTextArea.getText().strip(); if (!messageTextAreaCommands.executeIfAnyPresent(text)) try { // Creating the message and its metadata final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) diff --git a/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java b/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java index 4583aad..3e6071a 100644 --- a/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ContactSearchScene.java @@ -60,8 +60,10 @@ public class ContactSearchScene { private final Consumer handler = e -> { final var contact = e.get(); - if (e.getOperationType() == ElementOperation.ADD) Platform - .runLater(() -> { userList.getItems().remove(contact); if (currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close(); }); + if (e.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> { + userList.getItems().remove(contact); + if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close(); + }); }; private static final EventBus eventBus = EventBus.getInstance(); diff --git a/client/src/main/java/envoy/client/ui/controller/LoginScene.java b/client/src/main/java/envoy/client/ui/controller/LoginScene.java index 177524b..2b03a5b 100644 --- a/client/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/client/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -138,7 +138,7 @@ public final class LoginScene { localDB.setUser(localDB.getUsers().get(identifier)); localDB.initializeUserStorage(); localDB.loadUserData(); - } catch (Exception e) { + } catch (final Exception e) { // User storage empty, wrong user name etc. -> default lastSync } return localDB.getLastSync(); @@ -161,7 +161,7 @@ public final class LoginScene { try { // Try entering offline mode localDB.loadUsers(); - final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier()); + final User clientUser = localDB.getUsers().get(credentials.getIdentifier()); if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); client.setSender(clientUser); loadChatScene(); @@ -223,7 +223,7 @@ public final class LoginScene { // Initialize status tray icon final var trayIcon = new StatusTrayIcon(sceneContext.getStage()); settings.getItems().get("hideOnClose").setChangeHandler(c -> { - if (((Boolean) c)) trayIcon.show(); + if ((Boolean) c) trayIcon.show(); else trayIcon.hide(); }); } diff --git a/client/src/main/java/envoy/client/ui/listcell/ChatControl.java b/client/src/main/java/envoy/client/ui/listcell/ChatControl.java index c2e942c..e19cd54 100644 --- a/client/src/main/java/envoy/client/ui/listcell/ChatControl.java +++ b/client/src/main/java/envoy/client/ui/listcell/ChatControl.java @@ -31,12 +31,12 @@ public class ChatControl extends HBox { // Unread messages if (chat.getUnreadAmount() != 0) { - Region spacing = new Region(); + final var spacing = new Region(); setHgrow(spacing, Priority.ALWAYS); getChildren().add(spacing); final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount())); unreadMessagesLabel.setMinSize(15, 15); - var vBox2 = new VBox(); + final var vBox2 = new VBox(); vBox2.setAlignment(Pos.CENTER_RIGHT); unreadMessagesLabel.setAlignment(Pos.CENTER); unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount"); diff --git a/common/src/main/java/envoy/event/IsTyping.java b/common/src/main/java/envoy/event/IsTyping.java new file mode 100644 index 0000000..f720459 --- /dev/null +++ b/common/src/main/java/envoy/event/IsTyping.java @@ -0,0 +1,45 @@ +package envoy.event; + +/** + * This event should be sent when a user is currently typing something in a + * chat. + *

+ * Project: envoy-client
+ * File: IsTyping.java
+ * Created: 24.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Client v0.2-beta + */ +public class IsTyping extends Event { + + private final long destinationID; + + private static final long serialVersionUID = 1L; + + /** + * The number of milliseconds that this event will be active.
+ * Currently set to 3.5 seconds. + * + * @since Envoy Common v0.2-beta + */ + public static final int millisecondsActive = 3500; + + /** + * Creates a new {@code IsTyping} event with originator and recipient. + * + * @param sourceID the id of the originator + * @param destinationID the id of the contact the user wrote to + * @since Envoy Common v0.2-beta + */ + public IsTyping(Long sourceID, long destinationID) { + super(sourceID); + this.destinationID = destinationID; + } + + /** + * @return the id of the contact in whose chat the user typed something + * @since Envoy Common v0.2-beta + */ + public long getDestinationID() { return destinationID; } +} diff --git a/server/src/main/java/envoy/server/Startup.java b/server/src/main/java/envoy/server/Startup.java index 489a060..ac24c40 100755 --- a/server/src/main/java/envoy/server/Startup.java +++ b/server/src/main/java/envoy/server/Startup.java @@ -69,7 +69,8 @@ public class Startup { new UserStatusChangeProcessor(), new IDGeneratorRequestProcessor(), new UserSearchProcessor(), - new ContactOperationProcessor()))); + new ContactOperationProcessor(), + new IsTypingProcessor()))); // Initialize the current message ID final PersistenceManager persistenceManager = PersistenceManager.getInstance(); diff --git a/server/src/main/java/envoy/server/processors/IsTypingProcessor.java b/server/src/main/java/envoy/server/processors/IsTypingProcessor.java new file mode 100644 index 0000000..bea5f97 --- /dev/null +++ b/server/src/main/java/envoy/server/processors/IsTypingProcessor.java @@ -0,0 +1,37 @@ +package envoy.server.processors; + +import java.io.IOException; + +import envoy.event.IsTyping; +import envoy.server.data.PersistenceManager; +import envoy.server.data.User; +import envoy.server.net.ConnectionManager; +import envoy.server.net.ObjectWriteProxy; + +/** + * This processor handles incoming {@link IsTyping} events. + *

+ * Project: envoy-server-standalone
+ * File: IsTypingProcessor.java
+ * Created: 24.07.2020
+ * + * @author Leon Hofmeister + * @since Envoy Server v0.2-beta + */ +public class IsTypingProcessor implements ObjectProcessor { + + private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); + private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); + + @Override + public Class getInputClass() { return IsTyping.class; } + + @Override + public void process(IsTyping event, long socketID, ObjectWriteProxy writeProxy) throws IOException { + final var contact = persistenceManager.getContactByID(event.get()); + if (contact instanceof User) { + final var destinationID = event.getDestinationID(); + if (connectionManager.isOnline(destinationID)) writeProxy.write(connectionManager.getSocketID(destinationID), event); + } else writeProxy.writeToOnlineContacts(contact.getContacts(), event); + } +}