Refactoring #55

Merged
kske merged 7 commits from refactoring into develop 2020-09-27 12:06:39 +02:00
5 changed files with 54 additions and 93 deletions
Showing only changes of commit 6d7afbaa8f - Show all commits

View File

@ -3,6 +3,8 @@ package envoy.client.data;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import javafx.collections.*;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
@ -19,8 +21,9 @@ import envoy.event.MessageStatusChange;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected final Contact recipient; protected final Contact recipient;
protected final List<Message> messages = new ArrayList<>();
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount; protected int unreadAmount;
@ -29,7 +32,7 @@ public class Chat implements Serializable {
*/ */
protected transient long lastWritingEvent; 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. * 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; } public Chat(Contact recipient) { this.recipient = recipient; }
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
stream.defaultReadObject();
messages = FXCollections.observableList((List<Message>) stream.readObject());
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject(new ArrayList<>(messages));
}
@Override @Override
public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); } 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 * @param writeProxy the write proxy instance used to notify the server about
* the message status changes * the message status changes
* @throws IOException if a {@link MessageStatusChange} could not be
* delivered to the server
* @since Envoy Client v0.3-alpha * @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) { for (int i = messages.size() - 1; i >= 0; --i) {
final Message m = messages.get(i); final Message m = messages.get(i);
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break; 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 * @return all messages in the current chat
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public List<Message> getMessages() { return messages; } public ObservableList<Message> getMessages() { return messages; }
/** /**
* @return the recipient of a message * @return the recipient of a message

View File

@ -1,6 +1,5 @@
package envoy.client.data; package envoy.client.data;
import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
@ -32,7 +31,7 @@ public final class GroupChat extends Chat {
} }
@Override @Override
public void read(WriteProxy writeProxy) throws IOException { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i); final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break; if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;

View File

@ -7,6 +7,8 @@ import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.logging.*; import java.util.logging.*;
import javafx.collections.*;
import envoy.client.event.EnvoyCloseEvent; import envoy.client.event.EnvoyCloseEvent;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
@ -30,12 +32,12 @@ import dev.kske.eventbus.EventListener;
public final class LocalDB implements EventListener { public final class LocalDB implements EventListener {
// Data // Data
private User user; private User user;
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>()); private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
private List<Chat> chats = Collections.synchronizedList(new ArrayList<>()); private ObservableList<Chat> chats = FXCollections.observableArrayList();
private IDGenerator idGenerator; private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap(); private CacheMap cacheMap = new CacheMap();
private String authToken; private String authToken;
// State management // State management
private Instant lastSync = Instant.EPOCH; 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"); if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
userFile = new File(dbDir, user.getID() + ".db"); userFile = new File(dbDir, user.getID() + ".db");
try (var in = new ObjectInputStream(new FileInputStream(userFile))) { try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
chats = (List<Chat>) in.readObject(); chats = FXCollections.observableList((List<Chat>) in.readObject());
cacheMap = (CacheMap) in.readObject(); cacheMap = (CacheMap) in.readObject();
lastSync = (Instant) in.readObject(); lastSync = (Instant) in.readObject();
} finally { } finally {
@ -189,8 +191,8 @@ public final class LocalDB implements EventListener {
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save user data and last sync time stamp // Save user data and last sync time stamp
if (user != null) if (user != null) SerializationUtils
SerializationUtils.write(userFile, chats, cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync); .write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
// Save last login information // Save last login information
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken); 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()); logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
} }
@Event(priority = 150, includeSubtypes = true) @Event(priority = 150)
private void onMessageStatusChange(MessageStatusChange evt) { private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); }
// TODO: Cancel event once EventBus is updated
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid " + evt); @Event(priority = 150)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
} }
@Event(priority = 150) @Event(priority = 150)
@ -249,8 +253,8 @@ public final class LocalDB implements EventListener {
* @return an optional containing the message * @return an optional containing the message
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Optional<Message> getMessage(long id) { public <T extends Message> Optional<T> getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny(); return (Optional<T>) 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 * sender
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
**/ **/
public List<Chat> getChats() { return chats; } public ObservableList<Chat> getChats() { return chats; }
/**
* @param chats the chats to set
*/
public void setChats(List<Chat> chats) { this.chats = chats; }
/** /**
* @return the {@link User} who initialized the local database * @return the {@link User} who initialized the local database

View File

@ -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.
* <p>
* While it does work, it is <b>not the most efficient algorithm</b> possible.
*
* @param toRefresh the listView to refresh
* @param <T> the type of its {@code listcells}
* @since Envoy Client v0.1-beta
*/
public static <T> void deepRefresh(ListView<T> toRefresh) {
final var items = toRefresh.getItems();
toRefresh.setItems(null);
toRefresh.setItems(items);
}
}

View File

@ -11,7 +11,7 @@ import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.*; import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.FilteredList;
import javafx.fxml.*; import javafx.fxml.*;
import javafx.scene.control.*; import javafx.scene.control.*;
@ -167,7 +167,8 @@ public final class ChatScene implements EventListener, Restorable {
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
chatList.setCellFactory(new ListCellFactory<>(ChatControl::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. // We, however, need additional functionality.
messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null))); messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
@ -186,7 +187,7 @@ public final class ChatScene implements EventListener, Restorable {
clip.setArcWidth(43); clip.setArcWidth(43);
clientProfilePic.setClip(clip); clientProfilePic.setClip(clip);
chatList.setItems(chats = new FilteredList<>(FXCollections.observableList(localDB.getChats()))); chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName()); contactLabel.setText(localDB.getUser().getName());
initializeSystemCommandsMap(); initializeSystemCommandsMap();
@ -228,12 +229,8 @@ public final class ChatScene implements EventListener, Restorable {
// Read current chat or increment unread amount // Read current chat or increment unread amount
if (chat.equals(currentChat)) { if (chat.equals(currentChat)) {
try { currentChat.read(writeProxy);
currentChat.read(writeProxy); Platform.runLater(this::scrollToMessageListEnd);
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not read current chat: ", e);
}
Platform.runLater(() -> { ListViewRefresh.deepRefresh(messageList); scrollToMessageListEnd(); });
} else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount(); } else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top // Move chat with most recent unread messages to the top
@ -248,22 +245,12 @@ public final class ChatScene implements EventListener, Restorable {
@Event @Event
private void onMessageStatusChange(MessageStatusChange evt) { 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 // Update UI if in current chat and the current user was the sender of the
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { // message
localDB.getMessage(evt.getID()).ifPresent(groupMessage -> { if (currentChat != null) localDB.getMessage(evt.getID())
((GroupMessage) groupMessage).getMemberStatuses().replace(evt.getMemberID(), evt.get()); .filter(msg -> msg.getSenderID() == client.getSender().getID())
.ifPresent(msg -> Platform.runLater(messageList::refresh));
kske marked this conversation as resolved
Review

Did you test this? Is the normal refresh enough?

Did you test this? Is the normal refresh enough?
Review

Yes, it is, as the refresh method refreshed all objects inside the list that might have changed.

Yes, it is, as the `refresh` method refreshed all objects inside the list that might have changed.
// Update UI if in current chat
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
});
} }
@Event @Event
@ -273,7 +260,7 @@ public final class ChatScene implements EventListener, Restorable {
.filter(c -> c.getRecipient().getID() == evt.getID()) .filter(c -> c.getRecipient().getID() == evt.getID())
.findAny() .findAny()
.map(Chat::getRecipient) .map(Chat::getRecipient)
.ifPresent(u -> { ((User) u).setStatus(evt.get()); Platform.runLater(() -> ListViewRefresh.deepRefresh(chatList)); }); .ifPresent(u -> ((User) u).setStatus(evt.get()));
} }
@Event @Event
@ -318,6 +305,7 @@ public final class ChatScene implements EventListener, Restorable {
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
// TODO: cache image
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
@ -358,18 +346,14 @@ public final class ChatScene implements EventListener, Restorable {
// Load the chat // Load the chat
currentChat = localDB.getChat(user.getID()).get(); currentChat = localDB.getChat(user.getID()).get();
messageList.setItems(FXCollections.observableList(currentChat.getMessages())); messageList.setItems(currentChat.getMessages());
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount(); final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount();
messageList.scrollTo(scrollIndex); messageList.scrollTo(scrollIndex);
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
deleteContactMenuItem.setText("Delete " + user.getName()); deleteContactMenuItem.setText("Delete " + user.getName());
// Read the current chat // Read the current chat
try { currentChat.read(writeProxy);
currentChat.read(writeProxy);
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not read current chat.", e);
}
// Discard the pending attachment // Discard the pending attachment
if (recorder.isRecording()) { if (recorder.isRecording()) {
@ -690,7 +674,6 @@ public final class ChatScene implements EventListener, Restorable {
localDB.getChats().remove(currentChat); localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat); localDB.getChats().add(0, currentChat);
}); });
ListViewRefresh.deepRefresh(messageList);
scrollToMessageListEnd(); scrollToMessageListEnd();
// Request a new ID generator if all IDs were used // Request a new ID generator if all IDs were used