Merged branch 'develop' into f/groups

Contains several bug fixes
This commit is contained in:
DieGurke 2020-06-10 22:23:59 +02:00
commit 3439aee112
14 changed files with 160 additions and 112 deletions

View File

@ -148,12 +148,22 @@ public abstract class LocalDB {
* Searches for a message by ID.
*
* @param id the ID of the message to search for
* @return the message with the corresponding ID, or {@code null} if no message
* has been found
* @return an optional containing the message
* @since Envoy Client v0.1-beta
*/
public Message getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny().orElse(null);
public Optional<Message> getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
}
/**
* Searches for a chat by recipient ID.
*
* @param recipientID the ID of the chat's recipient
* @return an optional containing the chat
* @since Envoy Client v0.1-beta
*/
public Optional<Chat> getChat(long recipientID) {
return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny();
}
/**

View File

@ -91,7 +91,7 @@ public class Client implements Closeable {
SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream());
// Wait for a maximum of five seconds to acquire the sender object
long start = System.currentTimeMillis();
final long start = System.currentTimeMillis();
while (sender == null) {
// Quit immediately after handshake rejection
@ -141,7 +141,7 @@ public class Client implements Closeable {
receiver.registerProcessor(MessageStatusChangeEvent.class, new MessageStatusChangeEventProcessor());
// Process user status changes
receiver.registerProcessor(UserStatusChangeEvent.class, new UserStatusChangeProcessor(localDB));
receiver.registerProcessor(UserStatusChangeEvent.class, eventBus::dispatch);
// Process message ID generation
receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
@ -162,7 +162,7 @@ public class Client implements Closeable {
eventBus.register(SendEvent.class, evt -> {
try {
sendEvent(evt.get());
} catch (IOException e) {
} catch (final IOException e) {
e.printStackTrace();
}
});
@ -221,7 +221,7 @@ public class Client implements Closeable {
*/
public Map<String, Contact> getUsers() {
checkOnline();
Map<String, Contact> users = new HashMap<>();
final Map<String, Contact> users = new HashMap<>();
contacts.forEach(u -> users.put(u.getName(), u));
users.put(sender.getName(), sender);
return users;

View File

@ -1,33 +0,0 @@
package envoy.client.net;
import java.util.function.Consumer;
import envoy.client.data.LocalDB;
import envoy.data.User;
import envoy.event.EventBus;
import envoy.event.UserStatusChangeEvent;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>UserStatusChangeProcessor.java</strong><br>
* Created: <strong>2 Feb 2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-alpha
*/
public class UserStatusChangeProcessor implements Consumer<UserStatusChangeEvent> {
private final LocalDB localDB;
/**
* @param localDB the local database in which status updates will by applied
* @since Envoy Client v0.3-alpha
*/
public UserStatusChangeProcessor(LocalDB localDB) { this.localDB = localDB; }
@Override
public void accept(UserStatusChangeEvent evt) {
localDB.getUsers().values().stream().filter(u -> u.getID() == evt.getID()).map(User.class::cast).findFirst().get().setStatus(evt.get());
EventBus.getInstance().dispatch(evt);
}
}

View File

@ -49,7 +49,7 @@ public class WriteProxy {
client.sendMessage(msg);
// Update message state to SENT in localDB
localDB.getMessage(msg.getID()).nextStatus();
localDB.getMessage(msg.getID()).ifPresent(Message::nextStatus);
} catch (IOException e) {
logger.log(Level.SEVERE, "Could not send cached message", e);
}

View File

@ -3,8 +3,10 @@ package envoy.client.ui;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import envoy.data.Contact;
import envoy.data.Group;
import envoy.data.User;
/**
@ -26,9 +28,36 @@ public class ContactListCell extends ListCell<Contact> {
@Override
protected void updateItem(Contact contact, boolean empty) {
super.updateItem(contact, empty);
if (!empty && contact != null) {
final var name = new Label(contact.getName());
setGraphic(contact instanceof User ? new VBox(name, new Label(((User) contact).getStatus().toString())) : new VBox(name));
if (empty || contact == null) {
setText(null);
setGraphic(null);
} else {
// the infoLabel displays specific contact info, i.e. status of a user or amount
// of members in a group
Label infoLabel = null;
if (contact instanceof User) {
// user specific info
infoLabel = new Label(((User) contact).getStatus().toString());
Color textColor = null;
switch (((User) contact).getStatus()) {
case ONLINE:
textColor = Color.LIMEGREEN;
break;
case AWAY:
textColor = Color.ORANGERED;
break;
case BUSY:
textColor = Color.RED;
break;
case OFFLINE:
textColor = Color.GRAY;
break;
}
infoLabel.setTextFill(textColor);
} else
// group specific infos
infoLabel = new Label(String.valueOf(((Group) contact).getContacts().size()) + " members");
setGraphic(new VBox(new Label(contact.getName()), infoLabel));
}
}
}

View File

@ -45,10 +45,15 @@ public class MessageListCell extends ListCell<Message> {
@Override
protected void updateItem(Message message, boolean empty) {
super.updateItem(message, empty);
setGraphic(!empty && message != null ? new HBox(
if(empty || message == null) {
setText(null);
setGraphic(null);
} else {
setGraphic(new HBox(
new VBox(
new Label(dateFormat.format(message.getCreationDate())),
new Label(message.getText())),
new Label("", new ImageView(statusImages.get(message.getStatus())))) : null);
new Label("", new ImageView(statusImages.get(message.getStatus())))));
}
}
}

View File

@ -115,6 +115,7 @@ public final class SceneContext {
sceneStack.push(scene);
stage.setScene(scene);
applyCSS();
stage.sizeToScene();
stage.show();
} catch (IOException e) {
throw new RuntimeException(e);
@ -131,6 +132,7 @@ public final class SceneContext {
if (!sceneStack.isEmpty()) {
stage.setScene(sceneStack.peek());
applyCSS();
stage.sizeToScene();
}
stage.show();
}

View File

@ -9,6 +9,7 @@ import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
@ -22,9 +23,7 @@ import envoy.client.net.WriteProxy;
import envoy.client.ui.ContactListCell;
import envoy.client.ui.MessageListCell;
import envoy.client.ui.SceneContext;
import envoy.data.Contact;
import envoy.data.Message;
import envoy.data.MessageBuilder;
import envoy.data.*;
import envoy.event.EventBus;
import envoy.event.MessageStatusChangeEvent;
import envoy.event.UserStatusChangeEvent;
@ -74,6 +73,11 @@ public final class ChatScene {
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
private static final int MAX_MESSAGE_LENGTH = 255;
/**
* Initializes the appearance of certain visual components.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void initialize() {
@ -84,23 +88,35 @@ public final class ChatScene {
// Listen to received messages
eventBus.register(MessageCreationEvent.class, e -> {
final var message = e.get();
final var chat = localDB.getChats().stream().filter(c -> c.getRecipient().getID() == message.getSenderID()).findAny().get();
localDB.getChat(message.getSenderID()).ifPresent(chat -> {
chat.getMessages().add(message);
// Update UI if in current chat
if (chat == currentChat) Platform.runLater(() -> messageList.getItems().add(message));
if (chat == currentChat)
Platform.runLater(messageList::refresh);
});
});
// Listen to message status changes
eventBus.register(MessageStatusChangeEvent.class, e -> {
final var message = localDB.getMessage(e.getID());
eventBus.register(MessageStatusChangeEvent.class, e ->
localDB.getMessage(e.getID()).ifPresent(message -> {
message.setStatus(e.get());
// Update UI if in current chat
if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
});
}));
// Listen to user status changes
eventBus.register(UserStatusChangeEvent.class, e -> Platform.runLater(userList::refresh));
eventBus.register(UserStatusChangeEvent.class, e ->
userList.getItems()
.stream()
.filter(c -> c.getID() == e.getID())
.findAny()
.ifPresent(u -> {
((User) u).setStatus(e.get());
Platform.runLater(userList::refresh);
})
);
// Listen to contacts changes
eventBus.register(ContactOperationEvent.class, e -> {
@ -121,6 +137,8 @@ public final class ChatScene {
}
/**
* Initializes all necessary data via dependency injection-
*
* @param sceneContext the scene context used to load other scenes
* @param localDB the local database form which chats and users are loaded
* @param client the client used to request ID generators
@ -137,6 +155,11 @@ public final class ChatScene {
userList.setItems(FXCollections.observableList(localDB.getChats().stream().map(Chat::getRecipient).collect(Collectors.toList())));
}
/**
* Actions to perform when the list of contacts has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void userListClicked() {
final Contact user = userList.getSelectionModel().getSelectedItem();
@ -146,10 +169,8 @@ public final class ChatScene {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
// Load the chat or create a new one and add it to the LocalDB
currentChat = localDB.getChats()
.stream()
.filter(c -> c.getRecipient().getID() == user.getID())
.findAny()
currentChat = localDB
.getChat(user.getID())
.orElseGet(() -> { final var chat = new Chat(user); localDB.getChats().add(chat); return chat; });
messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
@ -161,32 +182,35 @@ public final class ChatScene {
messageTextArea.setDisable(currentChat == null);
}
@FXML
private void postButtonClicked() { postMessage(); }
/**
* Actions to perform when the Settings Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
}
/**
* Actions to perform when the "Add Contact" - Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void addContactButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
}
/**
* Actions to perform when the text was updated in the messageTextArea.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void messageTextUpdated(KeyEvent e) {
// TODO: Letting go of ctrl automatically posts a message. Needs to be fixed
// soon.
// Automatic sending of messages via (ctrl +) enter
if (!postButton.isDisabled() && settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|| !settings.isEnterToSend() && e.getCode() == KeyCode.CONTROL)
postMessage();
else postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
private void messageTextUpdated() {
// Truncating messages that are too long and staying at the same position
if (messageTextArea.getText().length() >= MAX_MESSAGE_LENGTH) {
messageTextArea.setText(messageTextArea.getText().substring(0, MAX_MESSAGE_LENGTH));
@ -201,33 +225,34 @@ public final class ChatScene {
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
}
/**
* Actions to perform when a key has been entered.
*
* @param e the Keys that have been entered
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkKeyCombination(KeyEvent e) {
// Automatic sending of messages via (ctrl +) enter
if (!postButton.isDisabled() && settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown())
postMessage();
postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
}
/**
* Sends a new message to the server based on the text entered in the
* messageTextArea.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void postMessage() {
// Create and send message
sendMessage(new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(messageTextArea.getText().strip())
.build());
// Clear text field and disable post button
messageTextArea.setText("");
postButton.setDisable(true);
}
/**
* Sends a message to the server and appends it to the current chat. If all
* message IDs have been used, a new ID generator is requested.
*
* @param message the message to send
* @since Envoy Client v0.1-beta
*/
private void sendMessage(Message message) {
try {
// Create and send message
final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(messageTextArea.getText().strip())
.build();
// Send message
writeProxy.writeMessage(message);
@ -240,6 +265,11 @@ public final class ChatScene {
} catch (final IOException e) {
logger.log(Level.SEVERE, "Error sending message", e);
new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait();
}
// Clear text field and disable post button
messageTextArea.setText("");
postButton.setDisable(true);
}
}

View File

@ -104,9 +104,7 @@ public final class LoginScene {
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
clearPasswordFields();
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
} else {
performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), registerCheckBox.isSelected()));
}
} else performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), registerCheckBox.isSelected()));
}
@FXML
@ -120,7 +118,6 @@ public final class LoginScene {
// Make repeat password field and label visible / invisible
repeatPasswordField.setVisible(registerCheckBox.isSelected());
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
clearPasswordFields();
}
@ -148,12 +145,12 @@ public final class LoginScene {
try {
// Try entering offline mode
localDB.loadUsers();
User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
new Alert(AlertType.WARNING, "A connection to the server could not be established. Starting in offline mode.").showAndWait();
loadChatScene();
} catch (Exception e) {
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
System.exit(1);
}

View File

@ -8,6 +8,7 @@ import javafx.scene.layout.VBox;
import envoy.client.data.Settings;
import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
/**
@ -27,7 +28,7 @@ public class GeneralSettingsPane extends SettingsPane {
*/
public GeneralSettingsPane() {
super("General");
var vbox = new VBox();
final var vbox = new VBox();
// TODO: Support other value types
List.of("onCloseMode", "enterToSend")
@ -36,7 +37,7 @@ public class GeneralSettingsPane extends SettingsPane {
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
.forEach(vbox.getChildren()::add);
var combobox = new ComboBox<String>();
final var combobox = new ComboBox<String>();
combobox.getItems().add("dark");
combobox.getItems().add("light");
combobox.setValue(settings.getCurrentTheme());
@ -44,6 +45,13 @@ public class GeneralSettingsPane extends SettingsPane {
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
vbox.getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(UserStatus.ONLINE);
// TODO add action when value is changed
statusComboBox.setOnAction(e -> {});
vbox.getChildren().add(statusComboBox);
getChildren().add(vbox);
}
}

View File

@ -26,8 +26,8 @@
<Label fx:id="contactLabel" prefHeight="16.0" prefWidth="250.0" text="Select a contact to chat with" GridPane.columnSpan="2" />
<Button fx:id="settingsButton" mnemonicParsing="true" onAction="#settingsButtonClicked" text="_Settings" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.valignment="CENTER" />
<ListView fx:id="messageList" prefHeight="257.0" prefWidth="155.0" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1" GridPane.rowSpan="2" />
<Button fx:id="postButton" defaultButton="true" disable="true" mnemonicParsing="true" onAction="#postButtonClicked" prefHeight="10.0" prefWidth="65.0" text="_Post" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="CENTER" />
<TextArea fx:id="messageTextArea" disable="true" onInputMethodTextChanged="#messageTextUpdated" onKeyPressed="#messageTextUpdated" onKeyTyped="#messageTextUpdated" prefHeight="200.0" prefWidth="200.0" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<Button fx:id="postButton" defaultButton="true" disable="true" mnemonicParsing="true" onAction="#postMessage" prefHeight="10.0" prefWidth="65.0" text="_Post" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="CENTER" />
<TextArea fx:id="messageTextArea" disable="true" onInputMethodTextChanged="#messageTextUpdated" onKeyPressed="#checkKeyCombination" onKeyTyped="#messageTextUpdated" prefHeight="200.0" prefWidth="200.0" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<Button mnemonicParsing="true" onAction="#addContactButtonClicked" text="_Add Contacts" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="CENTER">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />