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.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<Message> messages = new ArrayList<>();
protected final Contact recipient;
protected transient ObservableList<Message> 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<Message>) 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<Message> getMessages() { return messages; }
public ObservableList<Message> getMessages() { return messages; }
/**
* @return the recipient of a message

View File

@ -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;

View File

@ -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<String, User> users = Collections.synchronizedMap(new HashMap<>());
private List<Chat> chats = Collections.synchronizedList(new ArrayList<>());
private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap();
private String authToken;
private User user;
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
private ObservableList<Chat> 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<Chat>) in.readObject();
chats = FXCollections.observableList((List<Chat>) 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.<GroupMessage>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<Message> getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
public <T extends Message> Optional<T> getMessage(long id) {
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
* @since Envoy Client v0.1-alpha
**/
public List<Chat> getChats() { return chats; }
/**
* @param chats the chats to set
*/
public void setChats(List<Chat> chats) { this.chats = chats; }
public ObservableList<Chat> getChats() { return chats; }
/**
* @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.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));
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.
}
@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