diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index 8d3faed..1f88098 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -3,6 +3,8 @@ package envoy.client.data; import java.io.*; import java.util.*; +import javafx.collections.*; + import envoy.client.net.WriteProxy; import envoy.data.*; import envoy.data.Message.MessageStatus; @@ -19,8 +21,9 @@ import envoy.event.MessageStatusChange; */ public class Chat implements Serializable { - protected final Contact recipient; - protected final List messages = new ArrayList<>(); + protected final Contact recipient; + + protected transient ObservableList messages = FXCollections.observableArrayList(); protected int unreadAmount; @@ -29,7 +32,7 @@ public class Chat implements Serializable { */ protected transient long lastWritingEvent; - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; /** * Provides the list of messages that the recipient receives. @@ -41,6 +44,16 @@ public class Chat implements Serializable { */ public Chat(Contact recipient) { this.recipient = recipient; } + private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { + stream.defaultReadObject(); + messages = FXCollections.observableList((List) stream.readObject()); + } + + private void writeObject(ObjectOutputStream stream) throws IOException { + stream.defaultWriteObject(); + stream.writeObject(new ArrayList<>(messages)); + } + @Override public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); } @@ -72,11 +85,9 @@ public class Chat implements Serializable { * * @param writeProxy the write proxy instance used to notify the server about * the message status changes - * @throws IOException if a {@link MessageStatusChange} could not be - * delivered to the server * @since Envoy Client v0.3-alpha */ - public void read(WriteProxy writeProxy) throws IOException { + public void read(WriteProxy writeProxy) { for (int i = messages.size() - 1; i >= 0; --i) { final Message m = messages.get(i); if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break; @@ -127,7 +138,7 @@ public class Chat implements Serializable { * @return all messages in the current chat * @since Envoy Client v0.1-beta */ - public List getMessages() { return messages; } + public ObservableList getMessages() { return messages; } /** * @return the recipient of a message diff --git a/client/src/main/java/envoy/client/data/GroupChat.java b/client/src/main/java/envoy/client/data/GroupChat.java index 87208c3..188709c 100644 --- a/client/src/main/java/envoy/client/data/GroupChat.java +++ b/client/src/main/java/envoy/client/data/GroupChat.java @@ -1,6 +1,5 @@ package envoy.client.data; -import java.io.IOException; import java.time.Instant; import envoy.client.net.WriteProxy; @@ -32,7 +31,7 @@ public final class GroupChat extends Chat { } @Override - public void read(WriteProxy writeProxy) throws IOException { + public void read(WriteProxy writeProxy) { 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; diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index c5e154f..eb3cfd5 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -7,6 +7,8 @@ import java.time.Instant; import java.util.*; import java.util.logging.*; +import javafx.collections.*; + import envoy.client.event.EnvoyCloseEvent; import envoy.data.*; import envoy.data.Message.MessageStatus; @@ -30,12 +32,12 @@ import dev.kske.eventbus.EventListener; public final class LocalDB implements EventListener { // Data - private User user; - private Map users = Collections.synchronizedMap(new HashMap<>()); - private List chats = Collections.synchronizedList(new ArrayList<>()); - private IDGenerator idGenerator; - private CacheMap cacheMap = new CacheMap(); - private String authToken; + private User user; + private Map users = Collections.synchronizedMap(new HashMap<>()); + private ObservableList chats = FXCollections.observableArrayList(); + private IDGenerator idGenerator; + private CacheMap cacheMap = new CacheMap(); + private String authToken; // State management private Instant lastSync = Instant.EPOCH; @@ -129,7 +131,7 @@ public final class LocalDB implements EventListener { if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage"); userFile = new File(dbDir, user.getID() + ".db"); try (var in = new ObjectInputStream(new FileInputStream(userFile))) { - chats = (List) in.readObject(); + chats = FXCollections.observableList((List) in.readObject()); cacheMap = (CacheMap) in.readObject(); lastSync = (Instant) in.readObject(); } finally { @@ -189,8 +191,8 @@ public final class LocalDB implements EventListener { SerializationUtils.write(usersFile, users); // Save user data and last sync time stamp - if (user != null) - SerializationUtils.write(userFile, chats, cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync); + if (user != null) SerializationUtils + .write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync); // Save last login information if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken); @@ -212,10 +214,12 @@ public final class LocalDB implements EventListener { logger.warning("The groupMessage has the unexpected status " + msg.getStatus()); } - @Event(priority = 150, includeSubtypes = true) - private void onMessageStatusChange(MessageStatusChange evt) { - // TODO: Cancel event once EventBus is updated - if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid " + evt); + @Event(priority = 150) + private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); } + + @Event(priority = 150) + private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { + this.getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get())); } @Event(priority = 150) @@ -249,8 +253,8 @@ public final class LocalDB implements EventListener { * @return an optional containing the message * @since Envoy Client v0.1-beta */ - public Optional getMessage(long id) { - return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny(); + public Optional getMessage(long id) { + return (Optional) chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny(); } /** @@ -267,12 +271,7 @@ public final class LocalDB implements EventListener { * sender * @since Envoy Client v0.1-alpha **/ - public List getChats() { return chats; } - - /** - * @param chats the chats to set - */ - public void setChats(List chats) { this.chats = chats; } + public ObservableList getChats() { return chats; } /** * @return the {@link User} who initialized the local database diff --git a/client/src/main/java/envoy/client/ui/ListViewRefresh.java b/client/src/main/java/envoy/client/ui/ListViewRefresh.java deleted file mode 100644 index 635a12e..0000000 --- a/client/src/main/java/envoy/client/ui/ListViewRefresh.java +++ /dev/null @@ -1,31 +0,0 @@ -package envoy.client.ui; - -import javafx.scene.control.*; - -/** - * This is a utility class that provides access to a refreshing mechanism for - * elements that were added without notifying the underlying {@link ListView}. - * - * @author Leon Hofmeister - * @since Envoy Client v0.1-beta - */ -public final class ListViewRefresh { - - private ListViewRefresh() {} - - /** - * Deeply refreshes a {@code listview}, meaning it recomputes every single of - * its {@link ListCell}s. - *

- * While it does work, it is not the most efficient algorithm possible. - * - * @param toRefresh the listView to refresh - * @param the type of its {@code listcells} - * @since Envoy Client v0.1-beta - */ - public static void deepRefresh(ListView toRefresh) { - final var items = toRefresh.getItems(); - toRefresh.setItems(null); - toRefresh.setItems(items); - } -} 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 5299be6..2bd678c 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -11,7 +11,7 @@ import java.util.logging.*; import javafx.animation.RotateTransition; import javafx.application.Platform; -import javafx.collections.*; +import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.fxml.*; import javafx.scene.control.*; @@ -167,7 +167,8 @@ public final class ChatScene implements EventListener, Restorable { messageList.setCellFactory(MessageListCell::new); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); - // JavaFX provides an internal way of populating the context menu of a textarea. + // JavaFX provides an internal way of populating the context menu of a text + // area. // We, however, need additional functionality. messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null))); @@ -186,7 +187,7 @@ public final class ChatScene implements EventListener, Restorable { clip.setArcWidth(43); clientProfilePic.setClip(clip); - chatList.setItems(chats = new FilteredList<>(FXCollections.observableList(localDB.getChats()))); + chatList.setItems(chats = new FilteredList<>(localDB.getChats())); contactLabel.setText(localDB.getUser().getName()); initializeSystemCommandsMap(); @@ -228,12 +229,8 @@ public final class ChatScene implements EventListener, Restorable { // Read current chat or increment unread amount if (chat.equals(currentChat)) { - try { - currentChat.read(writeProxy); - } catch (final IOException e) { - logger.log(Level.WARNING, "Could not read current chat: ", e); - } - Platform.runLater(() -> { ListViewRefresh.deepRefresh(messageList); scrollToMessageListEnd(); }); + currentChat.read(writeProxy); + Platform.runLater(this::scrollToMessageListEnd); } else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount(); // Move chat with most recent unread messages to the top @@ -248,22 +245,12 @@ public final class ChatScene implements EventListener, Restorable { @Event private void onMessageStatusChange(MessageStatusChange evt) { - localDB.getMessage(evt.getID()).ifPresent(message -> { - message.setStatus(evt.get()); - // Update UI if in current chat and the current user was the sender of the - // message - if (currentChat != null && message.getSenderID() == client.getSender().getID()) Platform.runLater(messageList::refresh); - }); - } - @Event - private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { - localDB.getMessage(evt.getID()).ifPresent(groupMessage -> { - ((GroupMessage) groupMessage).getMemberStatuses().replace(evt.getMemberID(), evt.get()); - - // Update UI if in current chat - if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); - }); + // Update UI if in current chat and the current user was the sender of the + // message + if (currentChat != null) localDB.getMessage(evt.getID()) + .filter(msg -> msg.getSenderID() == client.getSender().getID()) + .ifPresent(msg -> Platform.runLater(messageList::refresh)); } @Event @@ -273,7 +260,7 @@ public final class ChatScene implements EventListener, Restorable { .filter(c -> c.getRecipient().getID() == evt.getID()) .findAny() .map(Chat::getRecipient) - .ifPresent(u -> { ((User) u).setStatus(evt.get()); Platform.runLater(() -> ListViewRefresh.deepRefresh(chatList)); }); + .ifPresent(u -> ((User) u).setStatus(evt.get())); } @Event @@ -318,6 +305,7 @@ public final class ChatScene implements EventListener, Restorable { clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); 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)); } @@ -358,18 +346,14 @@ public final class ChatScene implements EventListener, Restorable { // Load the chat currentChat = localDB.getChat(user.getID()).get(); - messageList.setItems(FXCollections.observableList(currentChat.getMessages())); + messageList.setItems(currentChat.getMessages()); final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount(); messageList.scrollTo(scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); deleteContactMenuItem.setText("Delete " + user.getName()); // Read the current chat - try { - currentChat.read(writeProxy); - } catch (final IOException e) { - logger.log(Level.WARNING, "Could not read current chat.", e); - } + currentChat.read(writeProxy); // Discard the pending attachment if (recorder.isRecording()) { @@ -690,7 +674,6 @@ public final class ChatScene implements EventListener, Restorable { localDB.getChats().remove(currentChat); localDB.getChats().add(0, currentChat); }); - ListViewRefresh.deepRefresh(messageList); scrollToMessageListEnd(); // Request a new ID generator if all IDs were used