Use ObservableList in LocalDB and Chat, reduce amount of UI refreshes
This commit is contained in:
parent
86e189a40a
commit
6d7afbaa8f
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
||||||
// 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
|
||||||
|
Reference in New Issue
Block a user