diff --git a/src/main/java/envoy/client/data/Chat.java b/src/main/java/envoy/client/data/Chat.java index 1eeed6a..739d335 100644 --- a/src/main/java/envoy/client/data/Chat.java +++ b/src/main/java/envoy/client/data/Chat.java @@ -24,10 +24,10 @@ import envoy.event.MessageStatusChange; * @author Kai S. K. Engelbart * @since Envoy Client v0.1-alpha */ -public final class Chat implements Serializable { +public class Chat implements Serializable { - private final Contact recipient; - private final List messages = new ArrayList<>(); + protected final Contact recipient; + protected final List messages = new ArrayList<>(); private static final long serialVersionUID = 1L; @@ -39,7 +39,9 @@ public final 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()); } diff --git a/src/main/java/envoy/client/data/GroupChat.java b/src/main/java/envoy/client/data/GroupChat.java new file mode 100644 index 0000000..3c1e98d --- /dev/null +++ b/src/main/java/envoy/client/data/GroupChat.java @@ -0,0 +1,54 @@ +package envoy.client.data; + +import java.io.IOException; +import java.time.LocalDateTime; + +import envoy.client.net.WriteProxy; +import envoy.data.Contact; +import envoy.data.GroupMessage; +import envoy.data.Message.MessageStatus; +import envoy.data.User; +import envoy.event.GroupMessageStatusChange; + +/** + * Represents a chat between a user and a group + * as a list of messages. + *

+ * Project: envoy-client
+ * File: GroupChat.java
+ * Created: 05.07.2020
+ * + * @author Maximilian Käfer + * @since Envoy Client v0.1-beta + */ +public class GroupChat extends Chat { + + private final User sender; + + private static final long serialVersionUID = 1L; + + /** + * @param sender the user sending the messages + * @param recipient the group whose members receive the messages + * @since Envoy Client v0.1-beta + */ + public GroupChat(User sender, Contact recipient) { + super(recipient); + this.sender = sender; + } + + @Override + public void read(WriteProxy writeProxy) throws IOException { + for (int i = messages.size() - 1; i >= 0; --i) { + final GroupMessage gmsg = (GroupMessage) messages.get(i); + if (gmsg.getSenderID() != sender.getID()) { + if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break; + else { + gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ); + writeProxy + .writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID())); + } + } + } + } +} diff --git a/src/main/java/envoy/client/data/LocalDB.java b/src/main/java/envoy/client/data/LocalDB.java index d8993f5..7899510 100644 --- a/src/main/java/envoy/client/data/LocalDB.java +++ b/src/main/java/envoy/client/data/LocalDB.java @@ -82,7 +82,11 @@ public abstract class LocalDB { getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); }); // Create missing chats - user.getContacts().stream().filter(u -> !u.equals(user) && getChat(u.getID()).isEmpty()).map(Chat::new).forEach(chats::add); + user.getContacts() + .stream() + .filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty()) + .map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c)) + .forEach(chats::add); } /** diff --git a/src/main/java/envoy/client/net/Client.java b/src/main/java/envoy/client/net/Client.java index 5cf432a..7348abe 100644 --- a/src/main/java/envoy/client/net/Client.java +++ b/src/main/java/envoy/client/net/Client.java @@ -53,23 +53,34 @@ public class Client implements Closeable { * will block for up to 5 seconds. If the handshake does exceed this time limit, * an exception is thrown. * - * @param credentials the login credentials of the user - * @param receivedMessageCache a message cache containing all unread - * messages - * from the server that can be relayed - * after - * initialization - * @param receivedMessageStatusChangeCache an event cache containing all - * received messageStatusChangeEvents - * from the server that can be relayed - * after initialization + * @param credentials the login credentials of the + * user + * @param receivedMessageCache a message cache containing all + * unread messages from the server + * that can be relayed after + * initialization + * @param receivedGroupMessageCache a groupMessage cache containing + * all unread groupMessages from + * the server that can be relayed + * after initialization + * @param receivedMessageStatusChangeCache an event cache containing all + * received + * messageStatusChangeEvents from + * the server that can be relayed + * after initialization + * @param receivedGroupMessageStatusChangeCache an event cache containing all + * received + * groupMessageStatusChangeEvents + * from the server that can be + * relayed after initialization * @throws TimeoutException if the server could not be reached * @throws IOException if the login credentials could not be written * @throws InterruptedException if the current thread is interrupted while * waiting for the handshake response */ - public void performHandshake(LoginCredentials credentials, Cache receivedMessageCache, - Cache receivedMessageStatusChangeCache) throws TimeoutException, IOException, InterruptedException { + public void performHandshake(LoginCredentials credentials, Cache receivedMessageCache, Cache receivedGroupMessageCache, + Cache receivedMessageStatusChangeCache, Cache receivedGroupMessageStatusChangeCache) + throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully"); // Establish TCP connection @@ -83,7 +94,9 @@ public class Client implements Closeable { // Register user creation processor, contact list processor and message cache receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessor(Message.class, receivedMessageCache); + receiver.registerProcessor(GroupMessage.class, receivedGroupMessageCache); receiver.registerProcessor(MessageStatusChange.class, receivedMessageStatusChangeCache); + receiver.registerProcessor(GroupMessageStatusChange.class, receivedGroupMessageStatusChangeCache); receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); }); rejected = false; @@ -122,38 +135,65 @@ public class Client implements Closeable { * Initializes the {@link Receiver} used to process data sent from the server to * this client. * - * @param localDB the local database used to persist - * the current - * {@link IDGenerator} - * @param receivedMessageCache a message cache containing all unread - * messages - * from the server that can be relayed - * after - * initialization - * @param receivedMessageStatusChangeCache an event cache containing all - * received messageStatusChangeEvents - * from the server that can be relayed - * after initialization + * @param localDB the local database used to + * persist + * the current + * {@link IDGenerator} + * @param receivedMessageCache a message cache containing all + * unread + * messages + * from the server that can be + * relayed + * after + * initialization + * @param receivedGroupMessageCache a groupMessage cache containing + * all + * unread + * groupMessages + * from the server that can be + * relayed + * after + * initialization + * @param receivedMessageStatusChangeCache an event cache containing all + * received + * messageStatusChangeEvents + * from the server that can be + * relayed + * after initialization + * @param receivedGroupMessageStatusChangeCache an event cache containing all + * received + * groupMessageStatusChangeEvents + * from the server that can be + * relayed after initialization * @throws IOException if no {@link IDGenerator} is present and none could be * requested from the server * @since Envoy Client v0.2-alpha */ - public void initReceiver(LocalDB localDB, Cache receivedMessageCache, Cache receivedMessageStatusChangeCache) + public void initReceiver(LocalDB localDB, Cache receivedMessageCache, Cache receivedGroupMessageCache, + Cache receivedMessageStatusChangeCache, Cache receivedGroupMessageStatusChangeCache) throws IOException { checkOnline(); // Process incoming messages - final ReceivedMessageProcessor receivedMessageProcessor = new ReceivedMessageProcessor(); - final MessageStatusChangeProcessor messageStatusChangeEventProcessor = new MessageStatusChangeProcessor(); + final ReceivedMessageProcessor receivedMessageProcessor = new ReceivedMessageProcessor(); + final ReceivedGroupMessageProcessor receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor(); + final MessageStatusChangeProcessor messageStatusChangeProcessor = new MessageStatusChangeProcessor(); + final GroupMessageStatusChangeProcessor groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor(); + + receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor); receiver.registerProcessor(Message.class, receivedMessageProcessor); - // Relay cached unread messages + receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor); + + receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor); + // Relay cached unread messages and unread groupMessages receivedMessageCache.setProcessor(receivedMessageProcessor); + receivedGroupMessageCache.setProcessor(receivedGroupMessageProcessor); // Process message status changes - receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeEventProcessor); - receivedMessageStatusChangeCache.setProcessor(messageStatusChangeEventProcessor); + receivedMessageStatusChangeCache.setProcessor(messageStatusChangeProcessor); + receivedGroupMessageStatusChangeCache.setProcessor(groupMessageStatusChangeProcessor); // Process user status changes receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch); diff --git a/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java b/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java new file mode 100644 index 0000000..ce3dc73 --- /dev/null +++ b/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java @@ -0,0 +1,29 @@ +package envoy.client.net; + +import java.util.function.Consumer; +import java.util.logging.Logger; + +import envoy.data.Message.MessageStatus; +import envoy.event.EventBus; +import envoy.event.GroupMessageStatusChange; +import envoy.util.EnvoyLog; + +/** + * Project: envoy-client
+ * File: GroupMessageStatusChangePocessor.java
+ * Created: 03.07.2020
+ * + * @author Maximilian Käfer + * @since Envoy Client v0.1-beta + */ +public class GroupMessageStatusChangeProcessor implements Consumer { + + private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class); + + @Override + public void accept(GroupMessageStatusChange evt) { + if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt); + else EventBus.getInstance().dispatch(evt); + } + +} diff --git a/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java b/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java new file mode 100644 index 0000000..877d0f0 --- /dev/null +++ b/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java @@ -0,0 +1,33 @@ +package envoy.client.net; + +import java.util.function.Consumer; +import java.util.logging.Logger; + +import envoy.client.event.MessageCreationEvent; +import envoy.data.GroupMessage; +import envoy.data.Message.MessageStatus; +import envoy.event.EventBus; +import envoy.util.EnvoyLog; + +/** + * Project: envoy-client
+ * File: ReceivedGroupMessageProcessor.java
+ * Created: 13.06.2020
+ * + * @author Maximilian Käfer + * @since Envoy Client v0.1-beta + */ +public class ReceivedGroupMessageProcessor implements Consumer { + + private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class); + + @Override + public void accept(GroupMessage groupMessage) { + if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ) + logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus()); + + // Dispatch event + EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage)); + } + +} diff --git a/src/main/java/envoy/client/net/Receiver.java b/src/main/java/envoy/client/net/Receiver.java index bfd231c..f90d10b 100644 --- a/src/main/java/envoy/client/net/Receiver.java +++ b/src/main/java/envoy/client/net/Receiver.java @@ -78,8 +78,10 @@ public class Receiver extends Thread { // Get appropriate processor @SuppressWarnings("rawtypes") final Consumer processor = processors.get(obj.getClass()); - if (processor == null) logger.log(Level.WARNING, - String.format("The received object has the class %s for which no processor is defined.", obj.getClass())); + if (processor == null) + logger.log(Level.WARNING, String.format( + "The received object has the %s for which no processor is defined.", + obj.getClass())); else processor.accept(obj); } } catch (final SocketException e) { diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index 0240e84..54a72d8 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -15,7 +15,9 @@ import envoy.client.data.*; import envoy.client.net.Client; import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.controller.LoginScene; +import envoy.data.GroupMessage; import envoy.data.Message; +import envoy.event.GroupMessageStatusChange; import envoy.event.MessageStatusChange; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; @@ -43,7 +45,9 @@ public final class Startup extends Application { private LocalDB localDB; private Client client; private Cache messageCache; + private Cache groupMessageCache; private Cache messageStatusCache; + private Cache groupMessageStatusCache; private static final ClientConfig config = ClientConfig.getInstance(); private static final Logger logger = EnvoyLog.getLogger(Startup.class); @@ -99,14 +103,17 @@ public final class Startup extends Application { // Initialize client and unread message cache client = new Client(); messageCache = new Cache<>(); + groupMessageCache = new Cache<>(); messageStatusCache = new Cache<>(); + groupMessageStatusCache = new Cache<>(); stage.setTitle("Envoy"); stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); final var sceneContext = new SceneContext(stage); sceneContext.load(SceneInfo.LOGIN_SCENE); - sceneContext.getController().initializeData(client, localDB, messageCache, messageStatusCache, sceneContext); + sceneContext.getController() + .initializeData(client, localDB, messageCache, groupMessageCache, messageStatusCache, groupMessageStatusCache, sceneContext); } /** diff --git a/src/main/java/envoy/client/ui/controller/ChatScene.java b/src/main/java/envoy/client/ui/controller/ChatScene.java index 956ce21..8b05221 100644 --- a/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -17,9 +17,7 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; -import envoy.client.data.Chat; -import envoy.client.data.LocalDB; -import envoy.client.data.Settings; +import envoy.client.data.*; import envoy.client.data.audio.AudioRecorder; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; @@ -32,9 +30,7 @@ import envoy.client.ui.listcell.MessageControl; import envoy.client.ui.listcell.MessageListCellFactory; import envoy.data.*; import envoy.data.Attachment.AttachmentType; -import envoy.event.EventBus; -import envoy.event.MessageStatusChange; -import envoy.event.UserStatusChange; +import envoy.event.*; import envoy.event.contact.ContactOperation; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; @@ -116,9 +112,8 @@ public final class ChatScene implements Restorable { // Listen to received messages eventBus.register(MessageCreationEvent.class, e -> { final var message = e.get(); - localDB.getChat(message.getSenderID()).ifPresent(chat -> { + localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> { chat.insert(message); - if (chat.equals(currentChat)) { try { currentChat.read(writeProxy); @@ -133,11 +128,17 @@ public final class ChatScene implements Restorable { // Listen to message status changes eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> { message.setStatus(e.get()); - // Update UI if in current chat if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); })); + eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> { + ((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get()); + + // Update UI if in current chat + if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); + })); + // Listen to user status changes eventBus.register(UserStatusChange.class, e -> userList.getItems() @@ -152,7 +153,7 @@ public final class ChatScene implements Restorable { switch (e.getOperationType()) { case ADD: localDB.getUsers().put(contact.getName(), contact); - localDB.getChats().add(new Chat(contact)); + localDB.getChats().add(contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact)); Platform.runLater(() -> userList.getItems().add(contact)); break; case REMOVE: @@ -354,8 +355,8 @@ public final class ChatScene implements Restorable { } /** - * Sends a new message to the server based on the text entered in the - * messageTextArea. + * Sends a new {@link Message} or {@link GroupMessage} to the server based on + * the text entered in the {@code messageTextArea} and the given attachment. * * @since Envoy Client v0.1-beta */ @@ -371,15 +372,18 @@ public final class ChatScene implements Restorable { } final var text = messageTextArea.getText().strip(); try { - // Create and send message - final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) + // Creating the message and its metadata + final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) .setText(text); - if (pendingAttachment != null) { + // Setting an attachment, if present + if (pendingAttachment != null) { builder.setAttachment(pendingAttachment); pendingAttachment = null; attachmentView.setVisible(false); } - final var message = builder.build(); + // Building the final message + final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient()) + : builder.build(); // Send message writeProxy.writeMessage(message); diff --git a/src/main/java/envoy/client/ui/controller/LoginScene.java b/src/main/java/envoy/client/ui/controller/LoginScene.java index 5933665..897cd08 100644 --- a/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -16,13 +16,9 @@ import envoy.client.net.Client; import envoy.client.ui.ClearableTextField; import envoy.client.ui.SceneContext; import envoy.client.ui.Startup; -import envoy.data.LoginCredentials; -import envoy.data.Message; -import envoy.data.User; +import envoy.data.*; import envoy.data.User.UserStatus; -import envoy.event.EventBus; -import envoy.event.HandshakeRejection; -import envoy.event.MessageStatusChange; +import envoy.event.*; import envoy.exception.EnvoyException; import envoy.util.Bounds; import envoy.util.EnvoyLog; @@ -59,7 +55,9 @@ public final class LoginScene { private Client client; private LocalDB localDB; private Cache receivedMessageCache; + private Cache receivedGroupMessageCache; private Cache receivedMessageStatusChangeCache; + private Cache receivedGroupMessageStatusChangeCache; private SceneContext sceneContext; private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); @@ -77,27 +75,40 @@ public final class LoginScene { /** * Loads the login dialog using the FXML file {@code LoginDialog.fxml}. * - * @param client the client used to perform the - * handshake - * @param localDB the local database used for offline - * login - * @param receivedMessageCache the cache storing messages received - * during - * the handshake - * @param receivedMessageStatusChangeCache the cache storing - * messageStatusChangeEvents received - * during handshake - * @param sceneContext the scene context used to initialize - * the chat - * scene + * @param client the client used to perform the + * handshake + * @param localDB the local database used for + * offline + * login + * @param receivedMessageCache the cache storing messages + * received + * during + * the handshake + * @param receivedGroupMessageCache the cache storing groupMessages + * received during the handshake + * @param receivedMessageStatusChangeCache the cache storing + * messageStatusChangeEvents + * received + * during handshake + * @param receivedGroupMessageStatusChangeCache the cache storing + * groupMessageStatusChangeEvents + * received + * during handshake + * @param sceneContext the scene context used to + * initialize + * the chat + * scene * @since Envoy Client v0.1-beta */ - public void initializeData(Client client, LocalDB localDB, Cache receivedMessageCache, - Cache receivedMessageStatusChangeCache, SceneContext sceneContext) { + public void initializeData(Client client, LocalDB localDB, Cache receivedMessageCache, Cache receivedGroupMessageCache, + Cache receivedMessageStatusChangeCache, Cache receivedGroupMessageStatusChangeCache, + SceneContext sceneContext) { this.client = client; this.localDB = localDB; this.receivedMessageCache = receivedMessageCache; + this.receivedGroupMessageCache = receivedGroupMessageCache; this.receivedMessageStatusChangeCache = receivedMessageStatusChangeCache; + this.receivedGroupMessageStatusChangeCache = receivedGroupMessageStatusChangeCache; this.sceneContext = sceneContext; // Prepare handshake @@ -146,9 +157,17 @@ public final class LoginScene { private void performHandshake(LoginCredentials credentials) { try { - client.performHandshake(credentials, receivedMessageCache, receivedMessageStatusChangeCache); + client.performHandshake(credentials, + receivedMessageCache, + receivedGroupMessageCache, + receivedMessageStatusChangeCache, + receivedGroupMessageStatusChangeCache); if (client.isOnline()) { - client.initReceiver(localDB, receivedMessageCache, receivedMessageStatusChangeCache); + client.initReceiver(localDB, + receivedMessageCache, + receivedGroupMessageCache, + receivedMessageStatusChangeCache, + receivedGroupMessageStatusChangeCache); loadChatScene(); } } catch (IOException | InterruptedException | TimeoutException e) { @@ -212,6 +231,8 @@ public final class LoginScene { // Relay unread messages from cache if (receivedMessageCache != null && client.isOnline()) receivedMessageCache.relay(); + if (receivedGroupMessageCache != null && client.isOnline()) receivedGroupMessageCache.relay(); if (receivedMessageStatusChangeCache != null && client.isOnline()) receivedMessageStatusChangeCache.relay(); + if (receivedGroupMessageStatusChangeCache != null && client.isOnline()) receivedGroupMessageStatusChangeCache.relay(); } } diff --git a/src/main/java/envoy/client/ui/listcell/MessageControl.java b/src/main/java/envoy/client/ui/listcell/MessageControl.java index a5a9404..39d590e 100644 --- a/src/main/java/envoy/client/ui/listcell/MessageControl.java +++ b/src/main/java/envoy/client/ui/listcell/MessageControl.java @@ -57,12 +57,14 @@ public class MessageControl extends VBox { textLabel.setWrapText(true); getChildren().add(textLabel); // Setting the message status icon and background color - if (message.getRecipientID() != client.getID()) { + if (message.getSenderID() == client.getID()) { final var statusIcon = new ImageView(statusImages.get(message.getStatus())); statusIcon.setPreserveRatio(true); getChildren().add(statusIcon); getStyleClass().add("own-message"); - } else getStyleClass().add("received-message"); + } else { + getStyleClass().add("received-message"); + } // Adjusting height and weight of the cell to the corresponding ListView paddingProperty().setValue(new Insets(5, 20, 5, 20)); }