diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index d8b2ee8..6b0bd83 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -21,13 +21,12 @@ import envoy.event.MessageStatusChange; */ public class Chat implements Serializable { - protected final Contact recipient; - protected transient ObservableList messages = FXCollections.observableArrayList(); - protected int unreadAmount; + protected int unreadAmount; + protected boolean disabled; - private boolean disabled; + protected final Contact recipient; /** * Stores the last time an {@link envoy.event.IsTyping} event has been sent. @@ -58,7 +57,13 @@ public class Chat implements Serializable { @Override public String toString() { - return String.format("%s[recipient=%s,messages=%d, disabled=%b]", getClass().getSimpleName(), recipient, messages.size(), disabled); + return String.format( + "%s[recipient=%s,messages=%d,disabled=%b]", + getClass().getSimpleName(), + recipient, + messages.size(), + disabled + ); } /** @@ -94,11 +99,13 @@ public class Chat implements Serializable { public void read(WriteProxy writeProxy) { for (int i = messages.size() - 1; i >= 0; --i) { final var m = messages.get(i); - if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break; - else { - m.setStatus(MessageStatus.READ); - writeProxy.writeMessageStatusChange(new MessageStatusChange(m)); - } + if (m.getSenderID() == recipient.getID()) + if (m.getStatus() == MessageStatus.READ) + break; + else { + m.setStatus(MessageStatus.READ); + writeProxy.writeMessageStatusChange(new MessageStatusChange(m)); + } } unreadAmount = 0; } @@ -174,10 +181,10 @@ public class Chat implements Serializable { public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); } /** - * Determines whether messages can be sent in this chat. Should be true i.e. for - * chats whose recipient deleted this client as a contact. + * Determines whether messages can be sent in this chat. Should be {@code true} + * i.e. for chats whose recipient deleted this client as a contact. * - * @return whether this {@code Chat} has been disabled + * @return whether this chat has been disabled * @since Envoy Client v0.3-beta */ public boolean isDisabled() { return disabled; } @@ -186,7 +193,7 @@ public class Chat implements Serializable { * Determines whether messages can be sent in this chat. Should be true i.e. for * chats whose recipient deleted this client as a contact. * - * @param disabled the disabled to set + * @param disabled whether this chat should be disabled * @since Envoy Client v0.3-beta */ public void setDisabled(boolean disabled) { this.disabled = disabled; } diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index 7f93c18..a18c3e9 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -44,7 +44,6 @@ public final class LocalDB implements EventListener { private CacheMap cacheMap = new CacheMap(); private String authToken; private boolean contactsChanged; - private Stream changedChats; // Auto save timer private Timer autoSaver; @@ -144,16 +143,29 @@ public final class LocalDB implements EventListener { try (var in = new ObjectInputStream(new FileInputStream(userFile))) { chats = FXCollections.observableList((List) in.readObject()); - // Some chats have changed and should not be overridden by the saved values - if (changedChats != null) { - changedChats.forEach(chat -> { - final var chatIndex = chats.indexOf(chat); - if (chatIndex == -1) return; - else chats.set(chatIndex, chat); + // Some chats have changed and should not be overwritten by the saved values + if (contactsChanged) { + final var contacts = user.getContacts(); + + // Mark chats as disabled if a contact is no longer in this users contact list + final var changedUserChats = chats.stream() + .filter(not(chat -> contacts.contains(chat.getRecipient()))) + .peek(chat -> { chat.setDisabled(true); logger.log(Level.INFO, String.format("Deleted chat with %s.", chat.getRecipient())); }); + + // Also update groups with a different member count + final var changedGroupChats = contacts.stream().filter(Group.class::isInstance).flatMap(group -> { + final var potentialChat = getChat(group.getID()); + if (potentialChat.isEmpty()) return Stream.empty(); + final var chat = potentialChat.get(); + if (group.getContacts().size() != chat.getRecipient().getContacts().size()) { + logger.log(Level.INFO, "Removed one (or more) members from " + group); + return Stream.of(chat); + } else return Stream.empty(); }); + Stream.concat(changedUserChats, changedGroupChats).forEach(chat -> chats.set(chats.indexOf(chat), chat)); // loadUserData can get called two (or more?) times during application lifecycle - changedChats = null; + contactsChanged = false; } cacheMap = (CacheMap) in.readObject(); lastSync = (Instant) in.readObject(); @@ -213,7 +225,7 @@ public final class LocalDB implements EventListener { * @throws IOException if the saving process failed * @since Envoy Client v0.3-alpha */ - @Event(eventType = EnvoyCloseEvent.class, priority = 1000) + @Event(eventType = EnvoyCloseEvent.class, priority = 500) private synchronized void save() { EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database..."); @@ -235,35 +247,34 @@ public final class LocalDB implements EventListener { } } - @Event(priority = 150) + @Event(priority = 500) private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); } - @Event(priority = 150) + @Event(priority = 500) private void onGroupMessage(GroupMessage msg) { // TODO: Cancel event once EventBus is updated if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ) logger.warning("The groupMessage has the unexpected status " + msg.getStatus()); } - @Event(priority = 150) + @Event(priority = 500) private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); } - @Event(priority = 150) + @Event(priority = 500) private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { this.getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get())); } - @Event(priority = 150) + @Event(priority = 500) private void onUserStatusChange(UserStatusChange evt) { getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get())); } - @Event(priority = 200) + @Event(priority = 500) private void onUserOperation(UserOperation operation) { final var eventUser = operation.get(); switch (operation.getOperationType()) { case ADD: - getUsers().put(eventUser.getName(), eventUser); Platform.runLater(() -> chats.add(0, new Chat(eventUser))); break; case REMOVE: @@ -283,10 +294,10 @@ public final class LocalDB implements EventListener { else Platform.runLater(() -> chats.add(new GroupChat(user, newGroup))); } - @Event(priority = 150) + @Event(priority = 500) private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); } - @Event(priority = 150) + @Event(priority = 500) private void onNameChange(NameChange evt) { chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get())); } @@ -305,7 +316,7 @@ public final class LocalDB implements EventListener { * * @since Envoy Client v0.2-beta */ - @Event(eventType = Logout.class, priority = 100) + @Event(eventType = Logout.class, priority = 50) private void onLogout() { autoSaver.cancel(); autoSaveRestart = true; @@ -340,7 +351,10 @@ public final class LocalDB implements EventListener { private void onOwnStatusChange(OwnStatusChange statusChange) { user.setStatus(statusChange.get()); } @Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500) - private void onContactsChangedSinceLastLogin() { if (user != null) contactsChanged = true; } + private void onContactsChangedSinceLastLogin() { contactsChanged = true; } + + @Event(priority = 500) + private void onContactDisabled(ContactDisabled event) { getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); } /** * @return a {@code Map} of all users stored locally with their @@ -369,39 +383,6 @@ public final class LocalDB implements EventListener { */ public Optional getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); } - /** - * Sets the given user as base of this {@code LocalDB}. - * Additionally compares his contacts with the ones returned by the server, in - * case they might have been deleted. - * - * @param user the user to set - * @since Envoy Client v0.2-alpha - */ - public void setUserAndMergeContacts(User user) { - this.user = user; - if (contactsChanged) { - final var contacts = user.getContacts(); - - // Mark chats as disabled if a contact is no longer in this users contact list - final var changedUserChats = chats.stream() - .filter(not(chat -> contacts.contains(chat.getRecipient()))) - .peek(chat -> { chat.setDisabled(true); logger.log(Level.INFO, String.format("Marked %s as blocked.", chat.getRecipient())); }); - - // Also update groups with a different member count - final var changedGroupChats = contacts.stream().filter(Group.class::isInstance).flatMap(group -> { - final var potentialChat = getChat(group.getID()); - if (potentialChat.isEmpty()) return Stream.empty(); - final var chat = potentialChat.get(); - if (group.getContacts().size() != chat.getRecipient().getContacts().size()) { - logger.log(Level.INFO, "Removed one (or more) members from " + group); - return Stream.of(chat); - } else return Stream.empty(); - }); - - changedChats = Stream.concat(changedUserChats, changedGroupChats); - } - } - /** * @return all saved {@link Chat} objects that list the client user as the * sender diff --git a/client/src/main/java/envoy/client/data/Settings.java b/client/src/main/java/envoy/client/data/Settings.java index 9b317cd..8f56774 100644 --- a/client/src/main/java/envoy/client/data/Settings.java +++ b/client/src/main/java/envoy/client/data/Settings.java @@ -68,7 +68,7 @@ public final class Settings implements EventListener { * @throws IOException if an error occurs while saving the themes * @since Envoy Client v0.2-alpha */ - @Event(eventType = EnvoyCloseEvent.class, priority = 900) + @Event(eventType = EnvoyCloseEvent.class) private void save() { EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings..."); diff --git a/client/src/main/java/envoy/client/data/shortcuts/EnvoyShortcutConfig.java b/client/src/main/java/envoy/client/data/shortcuts/EnvoyShortcutConfig.java index 14a110c..0f58e97 100644 --- a/client/src/main/java/envoy/client/data/shortcuts/EnvoyShortcutConfig.java +++ b/client/src/main/java/envoy/client/data/shortcuts/EnvoyShortcutConfig.java @@ -6,6 +6,7 @@ import envoy.client.data.Context; import envoy.client.helper.ShutdownHelper; import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.util.UserUtil; +import envoy.data.User.UserStatus; /** * Envoy-specific implementation of the keyboard-shortcut interaction offered by @@ -40,5 +41,25 @@ public class EnvoyShortcutConfig { () -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE), SceneInfo.SETTINGS_SCENE, SceneInfo.LOGIN_SCENE); + + // Add option to change to status away + instance.addForNotExcluded(new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), + () -> UserUtil.changeStatus(UserStatus.AWAY), + SceneInfo.LOGIN_SCENE); + + // Add option to change to status busy + instance.addForNotExcluded(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), + () -> UserUtil.changeStatus(UserStatus.BUSY), + SceneInfo.LOGIN_SCENE); + + // Add option to change to status offline + instance.addForNotExcluded(new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), + () -> UserUtil.changeStatus(UserStatus.OFFLINE), + SceneInfo.LOGIN_SCENE); + + // Add option to change to status online + instance.addForNotExcluded(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), + () -> UserUtil.changeStatus(UserStatus.ONLINE), + SceneInfo.LOGIN_SCENE); } } diff --git a/client/src/main/java/envoy/client/event/ContactDisabled.java b/client/src/main/java/envoy/client/event/ContactDisabled.java new file mode 100644 index 0000000..c3721fe --- /dev/null +++ b/client/src/main/java/envoy/client/event/ContactDisabled.java @@ -0,0 +1,21 @@ +package envoy.client.event; + +import envoy.data.Contact; +import envoy.event.Event; + +/** + * Signifies that the chat of a contact should be disabled. + * + * @author Leon Hofmeister + * @since Envoy Client v0.3-beta + */ +public class ContactDisabled extends Event { + + private static final long serialVersionUID = 1L; + + /** + * @param contact the contact that should be disabled + * @since Envoy Client v0.3-beta + */ + public ContactDisabled(Contact contact) { super(contact); } +} diff --git a/client/src/main/java/envoy/client/net/Client.java b/client/src/main/java/envoy/client/net/Client.java index e84388d..3b886eb 100644 --- a/client/src/main/java/envoy/client/net/Client.java +++ b/client/src/main/java/envoy/client/net/Client.java @@ -9,7 +9,6 @@ import envoy.client.data.*; import envoy.client.event.EnvoyCloseEvent; import envoy.data.*; import envoy.event.*; -import envoy.event.contact.ContactsChangedSinceLastLogin; import envoy.util.*; import dev.kske.eventbus.*; @@ -30,7 +29,6 @@ public final class Client implements EventListener, Closeable { private Socket socket; private Receiver receiver; private boolean online; - private boolean isHandShake; // Asynchronously initialized during handshake private volatile User sender; @@ -63,7 +61,7 @@ public final class Client implements EventListener, Closeable { */ public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully"); - isHandShake = true; + rejected = false; // Establish TCP connection logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); @@ -78,8 +76,6 @@ public final class Client implements EventListener, Closeable { receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessors(cacheMap.getMap()); - rejected = false; - // Start receiver receiver.start(); @@ -95,19 +91,17 @@ public final class Client implements EventListener, Closeable { if (rejected) { socket.close(); receiver.removeAllProcessors(); - isHandShake = false; return; } if (System.currentTimeMillis() - start > 5000) { - isHandShake = false; + rejected = true; throw new TimeoutException("Did not log in after 5 seconds"); } Thread.sleep(500); } - online = true; - isHandShake = false; + online = true; logger.log(Level.INFO, "Handshake completed."); } @@ -182,23 +176,10 @@ public final class Client implements EventListener, Closeable { } @Event(eventType = HandshakeRejection.class, priority = 1000) - private void onHandshakeRejection() { - rejected = true; - isHandShake = false; - } - - @Event(eventType = ContactsChangedSinceLastLogin.class, priority = 1000) - private void onContactsChangedSinceLastLogin() { - if (!isHandShake) { - logger.log(Level.WARNING, "Received a request to check contacts while not in the handshake"); - - // TODO: consume event once event consumption is implemented - return; - } - } + private void onHandshakeRejection() { rejected = true; } @Override - @Event(eventType = EnvoyCloseEvent.class, priority = 800) + @Event(eventType = EnvoyCloseEvent.class, priority = 50) public void close() { if (online) { logger.log(Level.INFO, "Closing connection..."); diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index cd4ddbc..a93bf81 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -179,7 +179,7 @@ public final class Startup extends Application { // Set client user in local database final var user = client.getSender(); - localDB.setUserAndMergeContacts(user); + localDB.setUser(user); // Initialize chats in local database try { diff --git a/client/src/main/java/envoy/client/ui/control/ChatControl.java b/client/src/main/java/envoy/client/ui/control/ChatControl.java index c42ec28..353bdaf 100644 --- a/client/src/main/java/envoy/client/ui/control/ChatControl.java +++ b/client/src/main/java/envoy/client/ui/control/ChatControl.java @@ -1,13 +1,12 @@ package envoy.client.ui.control; import javafx.geometry.*; -import javafx.scene.control.*; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.layout.*; import envoy.client.data.*; -import envoy.client.util.*; -import envoy.data.User; +import envoy.client.util.IconUtil; /** * Displays a chat using a contact control for the recipient and a label for the @@ -17,7 +16,7 @@ import envoy.data.User; * @author Leon Hofmeister * @since Envoy Client v0.1-beta */ -public final class ChatControl extends Label { +public final class ChatControl extends HBox { private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32), groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32); @@ -32,35 +31,25 @@ public final class ChatControl extends Label { setAlignment(Pos.CENTER_LEFT); setPadding(new Insets(0, 0, 3, 0)); - final var menu = new ContextMenu(); - final var removeMI = new MenuItem(); - removeMI - .setText(chat.isDisabled() ? "Delete " : chat.getRecipient() instanceof User ? "Block " : "Leave group " + chat.getRecipient().getName()); - removeMI.setOnAction(chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient()) : e -> UserUtil.blockContact(chat.getRecipient())); - menu.getItems().add(removeMI); - setContextMenu(menu); - - final var display = new HBox(); - // Profile picture final var contactProfilePic = new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32); - display.getChildren().add(contactProfilePic); + getChildren().add(contactProfilePic); // Spacing final var leftSpacing = new Region(); leftSpacing.setPrefSize(8, 0); leftSpacing.setMinSize(8, 0); leftSpacing.setMaxSize(8, 0); - display.getChildren().add(leftSpacing); + getChildren().add(leftSpacing); // Contact control - display.getChildren().add(new ContactControl(chat.getRecipient())); + getChildren().add(new ContactControl(chat.getRecipient())); // Unread messages if (chat.getUnreadAmount() != 0) { final var spacing = new Region(); - HBox.setHgrow(spacing, Priority.ALWAYS); - display.getChildren().add(spacing); + setHgrow(spacing, Priority.ALWAYS); + getChildren().add(spacing); final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount())); unreadMessagesLabel.setMinSize(15, 15); final var vbox = new VBox(); @@ -68,12 +57,8 @@ public final class ChatControl extends Label { unreadMessagesLabel.setAlignment(Pos.CENTER); unreadMessagesLabel.getStyleClass().add("unread-messages-amount"); vbox.getChildren().add(unreadMessagesLabel); - display.getChildren().add(vbox); + getChildren().add(vbox); } - - setGraphic(display); - - // Set background depending on whether it is disabled or not - getStyleClass().add(chat.isDisabled() ? "disabled-chat" : "list-element"); + getStyleClass().add("list-element"); } } 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 fa072b6..e472380 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -162,7 +162,7 @@ public final class ChatScene implements EventListener, Restorable { // Initialize message and user rendering messageList.setCellFactory(MessageListCell::new); - chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); + chatList.setCellFactory(ChatListCell::new); // JavaFX provides an internal way of populating the context menu of a text // area. @@ -268,22 +268,21 @@ public final class ChatScene implements EventListener, Restorable { @Event private void onUserOperation(UserOperation operation) { - // All ADD dependant logic resides in LocalDB - if (operation.getOperationType().equals(ElementOperation.REMOVE)) Platform.runLater(() -> disableChat(operation.get(), true)); + + // All ADD dependent logic resides in LocalDB + if (operation.getOperationType().equals(ElementOperation.REMOVE)) Platform.runLater(() -> disableChat(new ContactDisabled(operation.get()))); } @Event private void onGroupResize(GroupResize resize) { final var chatFound = localDB.getChat(resize.getGroupID()); - if (chatFound.isEmpty()) return; - Platform.runLater(() -> { + chatFound.ifPresent(chat -> Platform.runLater(() -> { chatList.refresh(); // Update the top-bar status label if all conditions apply - if (currentChat != null && currentChat.getRecipient().equals(chatFound.get().getRecipient())) - topBarStatusLabel.setText(chatFound.get().getRecipient().getContacts().size() + " member" - + (currentChat.getRecipient().getContacts().size() != 1 ? "s" : "")); - }); + if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient())) topBarStatusLabel + .setText(chat.getRecipient().getContacts().size() + " member" + (currentChat.getRecipient().getContacts().size() != 1 ? "s" : "")); + })); } @Event(eventType = NoAttachments.class) @@ -331,7 +330,7 @@ public final class ChatScene implements EventListener, Restorable { */ @FXML private void chatListClicked() { - if (currentChat != null && chatList.getSelectionModel().isEmpty()) return; + if (chatList.getSelectionModel().isEmpty()) return; final var chat = chatList.getSelectionModel().getSelectedItem(); if (chat == null) return; @@ -376,12 +375,12 @@ public final class ChatScene implements EventListener, Restorable { if (currentChat != null) { topBarContactLabel.setText(currentChat.getRecipient().getName()); topBarContactLabel.setVisible(true); + topBarStatusLabel.setVisible(true); if (currentChat.getRecipient() instanceof User) { final var status = ((User) currentChat.getRecipient()).getStatus().toString(); topBarStatusLabel.setText(status); topBarStatusLabel.getStyleClass().clear(); topBarStatusLabel.getStyleClass().add(status.toLowerCase()); - topBarStatusLabel.setVisible(true); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); } else { topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " member" @@ -395,11 +394,7 @@ public final class ChatScene implements EventListener, Restorable { clip.setArcHeight(43); clip.setArcWidth(43); recipientProfilePic.setClip(clip); - messageList.getStyleClass().clear(); messageSearchButton.setVisible(true); - - // Change the background of the message list if the chat is disabled - if (currentChat.isDisabled()) messageList.getStyleClass().add("disabled-chat"); } } @@ -749,18 +744,17 @@ public final class ChatScene implements EventListener, Restorable { * Redesigns the UI when the {@link Chat} of the given contact has been marked * as disabled. * - * @param recipient the contact whose chat got disabled - * @param refreshChatList whether to refresh the chatList + * @param event the contact whose chat got disabled * @since Envoy Client v0.3-beta */ - public void disableChat(Contact recipient, boolean refreshChatList) { - if (refreshChatList) { - chatList.refresh(); + @Event + public void disableChat(ContactDisabled event) { + chatList.refresh(); + final var recipient = event.get(); - // Decrement member count for groups - if (recipient instanceof Group) - topBarStatusLabel.setText(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : "")); - } + // Decrement member count for groups + if (recipient instanceof Group) + topBarStatusLabel.setText(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : "")); if (currentChat != null && currentChat.getRecipient().equals(recipient)) { messageTextArea.setDisable(true); voiceButton.setDisable(true); @@ -792,11 +786,7 @@ public final class ChatScene implements EventListener, Restorable { remainingChars.setVisible(false); pendingAttachment = null; recipientProfilePic.setImage(null); - - // It is a valid case that cancel can cause a NullPointerException - try { - recorder.cancel(); - } catch (final NullPointerException e) {} + if (recorder.isRecording()) recorder.cancel(); } @FXML diff --git a/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java index 1e12021..842f0fd 100644 --- a/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java +++ b/client/src/main/java/envoy/client/ui/listcell/AbstractListCell.java @@ -33,6 +33,7 @@ public abstract class AbstractListCell extends ListCell { setGraphic(renderItem(item)); } else { setGraphic(null); + setCursor(Cursor.DEFAULT); } } diff --git a/client/src/main/java/envoy/client/ui/listcell/ChatListCell.java b/client/src/main/java/envoy/client/ui/listcell/ChatListCell.java new file mode 100644 index 0000000..ccaa6dd --- /dev/null +++ b/client/src/main/java/envoy/client/ui/listcell/ChatListCell.java @@ -0,0 +1,46 @@ +package envoy.client.ui.listcell; + +import javafx.scene.control.*; + +import envoy.client.data.*; +import envoy.client.net.Client; +import envoy.client.ui.control.ChatControl; +import envoy.client.util.UserUtil; +import envoy.data.User; + +/** + * A list cell containing chats represented as chat controls. + * + * @author Leon Hofmeister + * @since Envoy Client v0.3-beta + */ +public class ChatListCell extends AbstractListCell { + + private static final Client client = Context.getInstance().getClient(); + + /** + * @param listView the list view inside of which the cell will be displayed + * @since Envoy Client v0.3-beta + */ + public ChatListCell(ListView listView) { super(listView); } + + @Override + protected ChatControl renderItem(Chat chat) { + if (client.isOnline()) { + final var menu = new ContextMenu(); + final var removeMI = new MenuItem(); + removeMI.setText( + chat.isDisabled() ? "Delete " : chat.getRecipient() instanceof User ? "Block " : "Leave group " + chat.getRecipient().getName()); + removeMI.setOnAction( + chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient()) : e -> UserUtil.disableContact(chat.getRecipient())); + menu.getItems().add(removeMI); + setContextMenu(menu); + } else setContextMenu(null); + + // TODO: replace with icon in ChatControl + final var chatControl = new ChatControl(chat); + if (chat.isDisabled()) chatControl.getStyleClass().add("disabled-chat"); + else chatControl.getStyleClass().remove("disabled-chat"); + return chatControl; + } +} diff --git a/client/src/main/java/envoy/client/util/UserUtil.java b/client/src/main/java/envoy/client/util/UserUtil.java index 404dd23..a15d9cc 100644 --- a/client/src/main/java/envoy/client/util/UserUtil.java +++ b/client/src/main/java/envoy/client/util/UserUtil.java @@ -5,7 +5,7 @@ import java.util.logging.*; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import envoy.client.data.*; +import envoy.client.data.Context; import envoy.client.event.*; import envoy.client.helper.*; import envoy.client.ui.SceneContext.SceneInfo; @@ -70,31 +70,24 @@ public final class UserUtil { } /** - * Removes the given contact. Should not be confused with the method that is - * called when the server reports that a contact has been deleted while the user - * was offline. + * Removes the given contact. * * @param block the contact that should be removed * @since Envoy Client v0.3-beta - * @see LocalDB#setUserAndMergeContacts(envoy.data.User) */ - public static void blockContact(Contact block) { + public static void disableContact(Contact block) { if (!context.getClient().isOnline() || block == null) return; else { final var alert = new Alert(AlertType.CONFIRMATION); alert.setContentText("Are you sure you want to " + (block instanceof User ? "block " : "leave group ") + block.getName() + "?"); AlertHelper.confirmAction(alert, () -> { + final var isUser = block instanceof User; context.getClient() - .send(block instanceof User ? new UserOperation((User) block, ElementOperation.REMOVE) + .send(isUser ? new UserOperation((User) block, ElementOperation.REMOVE) : new GroupResize(context.getLocalDB().getUser(), (Group) block, ElementOperation.REMOVE)); - context.getLocalDB().getChat(block.getID()).ifPresent(chat -> chat.setDisabled(true)); - if (block instanceof User) logger.log(Level.INFO, "A user was blocked."); - else { - block.getContacts().remove(context.getLocalDB().getUser()); - logger.log(Level.INFO, "The user left a group."); - } - final var controller = context.getSceneContext().getController(); - if (controller instanceof ChatScene) ((ChatScene) controller).disableChat(block, true); + if (!isUser) block.getContacts().remove(context.getLocalDB().getUser()); + EventBus.getInstance().dispatch(new ContactDisabled(block)); + logger.log(Level.INFO, isUser ? "A user was blocked." : "The user left a group."); }); } } diff --git a/client/src/main/resources/css/base.css b/client/src/main/resources/css/base.css index bfe4da2..4c46161 100644 --- a/client/src/main/resources/css/base.css +++ b/client/src/main/resources/css/base.css @@ -147,7 +147,7 @@ } .disabled-chat { - -fx-background-color: #F04000; + -fx-background-color: #0000FF; } #quick-select-list .scroll-bar:horizontal{ diff --git a/common/src/main/java/envoy/util/SerializationUtils.java b/common/src/main/java/envoy/util/SerializationUtils.java index 4bc4516..f556657 100644 --- a/common/src/main/java/envoy/util/SerializationUtils.java +++ b/common/src/main/java/envoy/util/SerializationUtils.java @@ -105,7 +105,7 @@ public final class SerializationUtils { file.createNewFile(); } try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) { - for (final var obj : objs) + for (var obj : objs) out.writeObject(obj); } } @@ -119,7 +119,7 @@ public final class SerializationUtils { * @since Envoy Common v0.2-alpha */ public static byte[] writeToByteArray(Object obj) throws IOException { - final var baos = new ByteArrayOutputStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ObjectOutputStream oout = new ObjectOutputStream(baos)) { oout.writeObject(obj); } @@ -137,10 +137,10 @@ public final class SerializationUtils { */ public static void writeBytesWithLength(Object obj, OutputStream out) throws IOException { // Serialize object to byte array - final var objBytes = writeToByteArray(obj); + byte[] objBytes = writeToByteArray(obj); // Get length of byte array in bytes - final var objLen = intToBytes(objBytes.length); + byte[] objLen = intToBytes(objBytes.length); // Write length and byte array out.write(objLen);