This repository has been archived on 2021-12-05. You can view files and clone it, but cannot push or open issues or pull requests.
envoy/client/src/main/java/envoy/client/ui/controller/ChatScene.java

910 lines
29 KiB
Java

package envoy.client.ui.controller;
import static envoy.client.ui.SceneInfo.SETTINGS_SCENE;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.*;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.*;
import javafx.animation.RotateTransition;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.*;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.core.*;
import dev.kske.eventbus.core.Event;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus;
import envoy.event.*;
import envoy.event.contact.UserOperation;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.event.*;
import envoy.client.net.*;
import envoy.client.ui.*;
import envoy.client.ui.chatscene.*;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*;
import envoy.client.util.*;
/**
* Controller for the chat scene.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class ChatScene implements Restorable, KeyboardMapping {
@FXML
private ListView<Message> messageList;
@FXML
private ListView<Chat> chatList;
@FXML
private Button postButton;
@FXML
private Button voiceButton;
@FXML
private Button attachmentButton;
@FXML
private Button settingsButton;
@FXML
private Button messageSearchButton;
@FXML
private Button newGroupButton;
@FXML
private Button newContactButton;
@FXML
private Label remainingChars;
@FXML
private Label infoLabel;
@FXML
private Label topBarContactLabel;
@FXML
private Label topBarStatusLabel;
@FXML
private ImageView attachmentView;
@FXML
private ImageView clientProfilePic;
@FXML
private ImageView recipientProfilePic;
@FXML
private TextArea messageTextArea;
@FXML
private TextArea contactSearch;
@FXML
private TabPane tabPane;
@FXML
private Tab contactSearchTab;
@FXML
private Tab groupCreationTab;
@FXML
private HBox contactSpecificOnlineOperations;
@FXML
private HBox ownContactControl;
private Chat currentChat;
private FilteredList<Chat> chats;
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
private boolean isCustomAttachmentImage;
private ChatSceneCommands commands;
private final LocalDB localDB = context.getLocalDB();
private final Client client = context.getClient();
private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext();
private final AudioRecorder recorder = new AudioRecorder();
private final Tooltip onlyIfOnlineTooltip =
new Tooltip("You need to be online to do this");
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE =
IconUtil.loadIconThemeSensitive("attachment_present", 20);
private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
private static final Context context = Context.getInstance();
private static final int MAX_MESSAGE_LENGTH = 255;
private static final int DEFAULT_ICON_SIZE = 16;
/**
* Initializes the appearance of certain visual components.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void initialize() {
eventBus.registerListener(this);
commands = new ChatSceneCommands(messageList, this);
// Initialize message and user rendering
messageList.setCellFactory(MessageListCell::new);
chatList.setCellFactory(ChatListCell::new);
// 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)));
// Set the icons of buttons and image views
settingsButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("settings", 22)));
voiceButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
onlyIfOnlineTooltip.setShowDelay(Duration.millis(250));
final var clip = new Rectangle();
clip.setWidth(43);
clip.setHeight(43);
clip.setArcHeight(43);
clip.setArcWidth(43);
clientProfilePic.setClip(clip);
chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
// Set the design of the box in the upper-left corner
generateOwnStatusControl();
Platform.runLater(() -> {
final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative
// GroupCreationResult might have been returned
if (!newGroupButton.isDisabled())
newGroupButton.setDisable(!online);
newContactButton.setDisable(!online);
if (online)
try {
Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
contactSearchTab.setContent(new FXMLLoader()
.load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml")));
groupCreationTab.setContent(new FXMLLoader()
.load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml")));
} catch (final IOException e) {
logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
}
else {
Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
updateInfoLabel("You are offline", "info-label-warning");
}
});
}
@Event(BackEvent.class)
private void onBackEvent() {
tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal());
}
@Event
@Polymorphic
private void onMessage(Message message) {
// The sender of the message is the recipient of the chat
// Exceptions: this user is the sender (sync) or group message (group is
// recipient)
final var ownMessage = message.getSenderID() == localDB.getUser().getID();
final var recipientID =
message instanceof GroupMessage || ownMessage ? message.getRecipientID()
: message.getSenderID();
localDB.getChat(recipientID).ifPresent(chat -> {
Platform.runLater(() -> {
chat.insert(message);
// Read current chat or increment unread amount
if (chat.equals(currentChat)) {
currentChat.read(writeProxy);
scrollToMessageListEnd();
} else if (!ownMessage && message.getStatus() != MessageStatus.READ)
chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top
chats.getSource().remove(chat);
((ObservableList<Chat>) chats.getSource()).add(0, chat);
if (chat.equals(currentChat))
chatList.getSelectionModel().select(0);
});
});
}
@Event
private void onMessageStatusChange(MessageStatusChange evt) {
// 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));
}
@Event
private void onUserStatusChange(UserStatusChange statusChange) {
Platform.runLater(() -> {
chatList.refresh();
// Replacing the display in the top bar
if (currentChat != null && currentChat.getRecipient().getID() == statusChange.getID()) {
topBarStatusLabel.getStyleClass().clear();
topBarStatusLabel.setText(statusChange.get().toString());
topBarStatusLabel.getStyleClass().add(statusChange.get().toString().toLowerCase());
}
});
}
@Event
private void onUserOperation(UserOperation operation) {
// 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());
chatFound.ifPresent(chat -> Platform.runLater(() -> {
chatList.refresh();
// Update the top-bar status label if all conditions apply
if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient()))
topBarStatusLabel
.setText(chat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
}));
}
@Event(NoAttachments.class)
private void onNoAttachments() {
Platform.runLater(() -> {
attachmentButton.setDisable(true);
voiceButton.setDisable(true);
final var alert = new Alert(AlertType.ERROR);
alert.setTitle("No attachments possible");
alert.setHeaderText("Your current server does not support attachments.");
alert.setContentText("If this is unplanned, please contact your server administrator.");
alert.showAndWait();
});
}
@Event
@Priority(150)
private void onGroupCreationResult(GroupCreationResult result) {
Platform.runLater(() -> newGroupButton.setDisable(result.get() == null));
}
@Event(ThemeChangeEvent.class)
private void onThemeChange() {
ChatControl.reloadDefaultChatIcons();
settingsButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
attachmentView.setImage(
isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new);
if (currentChat != null)
if (currentChat.getRecipient() instanceof User)
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
else
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
}
@Event(Logout.class)
@Priority(200)
private void onLogout() {
eventBus.removeListener(this);
}
@Event(AccountDeletion.class)
private void onAccountDeletion() {
Platform.runLater(chatList::refresh);
}
@Override
public void onRestore() {
updateRemainingCharsLabel();
}
/**
* Actions to perform when the list of contacts has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
if (chatList.getSelectionModel().isEmpty())
return;
final var chat = chatList.getSelectionModel().getSelectedItem();
if (chat == null)
return;
final var user = chat.getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
// Load the chat
currentChat = localDB.getChat(user.getID()).get();
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);
// Read the current chat
currentChat.read(writeProxy);
// Discard the pending attachment
if (recorder.isRecording()) {
recorder.cancel();
voiceButton.setGraphic(new ImageView(
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null);
}
pendingAttachment = null;
updateAttachmentView(false);
remainingChars.setVisible(true);
remainingChars
.setText(String.format("remaining chars: %d/%d",
MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
}
// Enable or disable the necessary UI controls
final var chatEditable = currentChat == null || currentChat.isDisabled();
messageTextArea.setDisable(chatEditable || postingPermanentlyDisabled);
voiceButton.setDisable(!recorder.isSupported() || chatEditable);
attachmentButton.setDisable(chatEditable);
chatList.refresh();
// Design the top bar
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());
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
} else {
topBarStatusLabel
.setText(currentChat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
topBarStatusLabel.getStyleClass().clear();
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
}
final var clip = new Rectangle();
clip.setWidth(43);
clip.setHeight(43);
clip.setArcHeight(43);
clip.setArcWidth(43);
recipientProfilePic.setClip(clip);
messageSearchButton.setVisible(true);
}
}
/**
* Actions to perform when the Settings Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void settingsButtonClicked() {
sceneContext.load(SETTINGS_SCENE);
}
/**
* Actions to perform when the "Add Contact" - Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void addContactButtonClicked() {
tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal());
}
@FXML
private void groupCreationButtonClicked() {
tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal());
}
@FXML
private void voiceButtonClicked() {
new Thread(() -> {
try {
if (!recorder.isRecording()) {
Platform.runLater(() -> {
voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(
IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
});
recorder.start();
} else {
pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss")
.format(LocalDateTime.now())
+ "." + AudioRecorder.FILE_FORMAT,
AttachmentType.VOICE);
Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null);
checkPostConditions(false);
updateAttachmentView(true);
});
}
} catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform
.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
}
}).start();
}
@FXML
private void attachmentButtonClicked() {
// Display file chooser
final var fileChooser = new FileChooser();
fileChooser.setTitle("Add Attachment");
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters()
.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
new FileChooser.ExtensionFilter("Videos", "*.mp4"),
new FileChooser.ExtensionFilter("All Files", "*.*"));
final var file = fileChooser.showOpenDialog(sceneContext.getStage());
if (file != null) {
// Check max file size
if (file.length() > 16E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!")
.showAndWait();
return;
}
// Get attachment type (default is document)
var type = AttachmentType.DOCUMENT;
switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
case "Pictures":
type = AttachmentType.PICTURE;
break;
case "Videos":
type = AttachmentType.VIDEO;
break;
}
// Create the pending attachment
try {
final var fileBytes = Files.readAllBytes(file.toPath());
pendingAttachment = new Attachment(fileBytes, file.getName(), type);
checkPostConditions(false);
// Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE) {
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes),
DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
isCustomAttachmentImage = true;
}
attachmentView.setVisible(true);
} catch (final IOException e) {
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
}
}
}
/**
* Rotates every element in our application by {@code rotations}*360° in {@code an}.
*
* @param rotations the amount of times the scene is rotated by 360°
* @param animationTime the time in seconds that this animation lasts
* @since Envoy Client v0.1-beta
*/
public void doABarrelRoll(int rotations, double animationTime) {
// Limiting the rotations and duration
rotations = Math.min(rotations, 100000);
rotations = Math.max(rotations, 1);
animationTime = Math.min(animationTime, 150);
animationTime = Math.max(animationTime, 0.25);
// contains all Node objects in ChatScene
final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this);
for (final var node : rotatableNodes) {
// Sets the animation duration to {animationTime}
final var rotateTransition =
new RotateTransition(Duration.seconds(animationTime), node);
// rotates every element {rotations} times
rotateTransition.setByAngle(rotations * 360);
rotateTransition.play();
// This is needed as for some strange reason objects could stop before being
// rotated back to 0°
rotateTransition.setOnFinished(e -> node.setRotate(0));
}
}
/**
* Checks the text length of the {@code messageTextArea}, adjusts the {@code remainingChars}
* label and checks whether to send the message automatically.
*
* @param e the key event that will be analyzed for a post request
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long
messageTextUpdated();
// Sending an IsTyping event if none has been sent for
// IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent()
+ IsTyping.millisecondsActive <= System.currentTimeMillis()) {
client.send(new IsTyping(currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow();
}
// KeyPressed will be called before the char has been added to the text, hence
// this is needed for the first char
if (messageTextArea.getText().length() == 1 && e != null)
checkPostConditions(e);
// This is needed for the messageTA context menu
else if (e == null)
checkPostConditions(false);
}
/**
* @param e the keys that have been pressed
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkPostConditions(KeyEvent e) {
final var enterPressed = e.getCode() == KeyCode.ENTER;
final var messagePosted =
enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown()
: false;
if (messagePosted) {
// Removing an inserted line break if added by pressing enter
final var text = messageTextArea.getText();
final var textPosition = messageTextArea.getCaretPosition() - 1;
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
messageTextArea
.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
}
// if control is pressed, the enter press is originally invalidated. Here it'll
// be inserted again
else if (enterPressed && e.isControlDown()) {
var caretPosition = messageTextArea.getCaretPosition();
messageTextArea.setText(new StringBuilder(messageTextArea.getText())
.insert(caretPosition, '\n').toString());
messageTextArea.positionCaret(++caretPosition);
}
checkPostConditions(messagePosted);
}
private void checkPostConditions(boolean postMessage) {
if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && postMessage)
postMessage();
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null
|| currentChat == null);
} else {
final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging))
// Informing the user that he is a f*cking moron and should use Envoy online
// because he ran out of messageIDs to use
updateInfoLabel(noMoreMessaging, "info-label-error");
}
}
/**
* Actions to perform when the text was updated in the messageTextArea.
*
* @since Envoy Client v0.1-beta
*/
@FXML
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));
messageTextArea.positionCaret(MAX_MESSAGE_LENGTH);
messageTextArea.setScrollTop(Double.MAX_VALUE);
}
updateRemainingCharsLabel();
}
/**
* Sets the text and text color of the {@code remainingChars} label.
*
* @since Envoy Client v0.1-beta
*/
private void updateRemainingCharsLabel() {
final var currentLength = messageTextArea.getText().length();
final var remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars
.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
}
/**
* Sends a new {@link Message} or {@link GroupMessage} to the server based on the text entered
* in the {@code messageTextArea} and the given attachment.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void postMessage() {
postingPermanentlyDisabled = !(client.isOnline() || localDB.getIDGenerator().hasNext());
if (postingPermanentlyDisabled) {
postButton.setDisable(true);
messageTextArea.setDisable(true);
messageTextArea.clear();
updateInfoLabel("You need to go online to send more messages", "info-label-error");
return;
}
final var text = messageTextArea.getText().strip();
if (!commands.getChatSceneCommands().executeIfPresent(text)) {
// Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(),
currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text);
// Setting an attachment, if present
if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment);
pendingAttachment = null;
updateAttachmentView(false);
}
// Building the final message
final var message = currentChat.getRecipient() instanceof Group
? builder.buildGroupMessage((Group) currentChat.getRecipient())
: builder.build();
// Send message
writeProxy.writeMessage(message);
// Add message to LocalDB and update UI
currentChat.insert(message);
// Moving currentChat to the top
Platform.runLater(() -> {
chats.getSource().remove(currentChat);
((ObservableList<Chat>) chats.getSource()).add(0, currentChat);
localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat);
chatList.getSelectionModel().select(0);
});
scrollToMessageListEnd();
// Request a new ID generator if all IDs were used
if (!localDB.getIDGenerator().hasNext() && client.isOnline())
client.requestIDGenerator();
}
// Clear text field and disable post button
messageTextArea.setText("");
postButton.setDisable(true);
updateRemainingCharsLabel();
isCustomAttachmentImage = false;
}
/**
* Scrolls to the bottom of the {@code messageList}.
*
* @since Envoy Client v0.1-beta
*/
private void scrollToMessageListEnd() {
messageList.scrollTo(messageList.getItems().size() - 1);
}
/**
* Updates the {@code infoLabel}.
*
* @param text the text to use
* @param infoLabelID the id the the {@code infoLabel} should have so that it can be styled
* accordingly in CSS
* @since Envoy Client v0.1-beta
*/
private void updateInfoLabel(String text, String infoLabelID) {
infoLabel.setText(text);
infoLabel.setId(infoLabelID);
infoLabel.setVisible(true);
}
/**
* Updates the {@code attachmentView} in terms of visibility.<br>
* Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image
* is currently present.
*
* @param visible whether the {@code attachmentView} should be displayed
* @since Envoy Client v0.1-beta
*/
private void updateAttachmentView(boolean visible) {
if (!(attachmentView.getImage() == null
|| attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)))
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
attachmentView.setVisible(visible);
}
@Event(OwnStatusChange.class)
@Priority(50)
private void generateOwnStatusControl() {
// Update the own user status if present
if (ownContactControl.getChildren().get(1) instanceof ContactControl)
((ContactControl) ownContactControl.getChildren().get(1)).replaceInfoLabel();
else {
// Else prepend it to the HBox children
final var ownUserControl = new ContactControl(localDB.getUser());
ownUserControl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(ownUserControl, javafx.scene.layout.Priority.NEVER);
ownContactControl.getChildren().add(1, ownUserControl);
}
}
/**
* Redesigns the UI when the {@link Chat} of the given contact has been marked as disabled.
*
* @param event the contact whose chat got disabled
* @since Envoy Client v0.3-beta
*/
@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" : ""));
if (currentChat != null && currentChat.getRecipient().equals(recipient)) {
messageTextArea.setDisable(true);
voiceButton.setDisable(true);
attachmentButton.setDisable(true);
pendingAttachment = null;
messageList.getStyleClass().clear();
messageList.getStyleClass().add("disabled-chat");
}
}
/**
* Resets every component back to its inital state before a chat was selected.
*
* @since Envoy Client v0.3-beta
*/
public void resetState() {
currentChat = null;
chatList.getSelectionModel().clearSelection();
messageList.getItems().clear();
messageTextArea.setDisable(true);
attachmentView.setImage(null);
topBarContactLabel.setVisible(false);
topBarStatusLabel.setVisible(false);
messageSearchButton.setVisible(false);
messageTextArea.clear();
messageTextArea.setDisable(true);
attachmentButton.setDisable(true);
voiceButton.setDisable(true);
remainingChars.setVisible(false);
pendingAttachment = null;
recipientProfilePic.setImage(null);
if (recorder.isRecording())
recorder.cancel();
}
@FXML
private void copyAndPostMessage() {
final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(messageText), null);
final var image = attachmentView.getImage();
final var messageAttachment = pendingAttachment;
postMessage();
messageTextArea.setText(messageText);
updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank());
attachmentView.setImage(image);
if (attachmentView.getImage() != null)
attachmentView.setVisible(true);
pendingAttachment = messageAttachment;
}
/**
* Clears the current message selection.
*
* @since Envoy Client v0.3-beta
*/
public void clearMessageSelection() {
messageList.getSelectionModel().clearSelection();
}
@FXML
private void searchContacts() {
chats.setPredicate(contactSearch.getText().isBlank() ? c -> true
: c -> c.getRecipient().getName().toLowerCase()
.contains(contactSearch.getText().toLowerCase()));
}
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.<KeyCombination, Runnable>of(
// Delete text before the caret with "Control" + U
new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(messageTextArea.getCaretPosition()));
checkPostConditions(false);
// Delete text after the caret with "Control" + K
}, new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(0, messageTextArea.getCaretPosition()));
checkPostConditions(false);
messageTextArea.positionCaret(messageTextArea.getText().length());
});
}
}