Merge branch 'develop' into f/groupMessages
							
								
								
									
										30
									
								
								src/main/java/envoy/client/Main.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
			
		||||
package envoy.client;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Application;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.Startup;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Triggers application startup.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * To allow Maven shading, the main method has to be separated from the
 | 
			
		||||
 * {@link Startup} class which extends {@link Application}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Main.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class Main {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the application.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param args the command line arguments are processed by the
 | 
			
		||||
	 *             client configuration
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static void main(String[] args) { Application.launch(Startup.class, args); }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,7 @@ import envoy.util.SerializationUtils;
 | 
			
		||||
public class Settings {
 | 
			
		||||
 | 
			
		||||
	// Actual settings accessible by the rest of the application
 | 
			
		||||
	private Map<String, SettingsItem<?>>	items;
 | 
			
		||||
	private Map<String, SettingsItem<?>> items;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Settings are stored in this file.
 | 
			
		||||
@@ -93,6 +93,15 @@ public class Settings {
 | 
			
		||||
	 */
 | 
			
		||||
	public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return true if the currently used theme is one of the default themes
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isUsingDefaultTheme() {
 | 
			
		||||
		final var theme = getCurrentTheme();
 | 
			
		||||
		return theme.equals("dark") || theme.equals("light");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true}, if pressing the {@code Enter} key suffices to send a
 | 
			
		||||
	 *         message. Otherwise it has to be pressed in conjunction with the
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@ import javafx.scene.layout.Background;
 | 
			
		||||
import javafx.scene.layout.ColumnConstraints;
 | 
			
		||||
import javafx.scene.layout.GridPane;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Settings;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class offers a text field that is automatically equipped with a clear
 | 
			
		||||
 * button.
 | 
			
		||||
@@ -49,10 +47,7 @@ public class ClearableTextField extends GridPane {
 | 
			
		||||
	public ClearableTextField(String text, int size) {
 | 
			
		||||
		// initializing the textField and the button
 | 
			
		||||
		textField	= new TextField(text);
 | 
			
		||||
		clearButton	= new Button("",
 | 
			
		||||
				new ImageView(IconUtil.load(
 | 
			
		||||
						Settings.getInstance().getCurrentTheme().equals("dark") ? "/icons/clear_button_white.png" : "/icons/clear_button_black.png",
 | 
			
		||||
						size)));
 | 
			
		||||
		clearButton	= new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
 | 
			
		||||
		clearButton.setOnAction(e -> textField.clear());
 | 
			
		||||
		clearButton.setFocusTraversable(false);
 | 
			
		||||
		clearButton.getStyleClass().clear();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,13 @@ package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.util.EnumMap;
 | 
			
		||||
import java.util.EnumSet;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.image.Image;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Settings;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides static utility methods for loading icons from the resource
 | 
			
		||||
 * folder.
 | 
			
		||||
@@ -21,37 +25,115 @@ public class IconUtil {
 | 
			
		||||
	private IconUtil() {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads an icon from the resource folder.
 | 
			
		||||
	 * Loads an image from the resource folder.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param path the path to the icon inside the resource folder
 | 
			
		||||
	 * @return the icon
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads an icon from the resource folder and scales it to a given size.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param path the path to the icon inside the resource folder
 | 
			
		||||
	 * @param size the size to scale the icon to
 | 
			
		||||
	 * @return the scaled icon
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image load(String path, int size) {
 | 
			
		||||
		return new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
 | 
			
		||||
	public static Image load(String path) {
 | 
			
		||||
		Image image = null;
 | 
			
		||||
		try {
 | 
			
		||||
			image = new Image(IconUtil.class.getResource(path).toExternalForm());
 | 
			
		||||
		} catch (final NullPointerException e) {
 | 
			
		||||
			EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
 | 
			
		||||
		}
 | 
			
		||||
		return image;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads an image from the resource folder and scales it to the given size.
 | 
			
		||||
	 *
 | 
			
		||||
	 * Loads icons specified by an enum. The images have to be named like the
 | 
			
		||||
	 * @param path the path to the icon inside the resource folder
 | 
			
		||||
	 * @param size the size to scale the icon to
 | 
			
		||||
	 * @return the scaled image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image load(String path, int size) {
 | 
			
		||||
		Image image = null;
 | 
			
		||||
		try {
 | 
			
		||||
			image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
 | 
			
		||||
		} catch (final NullPointerException e) {
 | 
			
		||||
			EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
 | 
			
		||||
		}
 | 
			
		||||
		return image;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
 | 
			
		||||
	 * resource folder.<br>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's load a sample image {@code /icons/abc.png}.<br>
 | 
			
		||||
	 *          To do that, we only have to call {@code IconUtil.loadIcon("abc")}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
 | 
			
		||||
	 * resource folder and scales it to the given size.<br>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @param size the size of the image to scale to
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
 | 
			
		||||
	 *          To do that, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIcon("abc", 16)}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image whose design depends on the currently active theme
 | 
			
		||||
	 * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
 | 
			
		||||
	 * resource folder.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the "black" or "white" suffix and without
 | 
			
		||||
	 *             the .png suffix
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
 | 
			
		||||
	 *          {@code /icons/light/abc.png}, and load one of them.<br>
 | 
			
		||||
	 *          To do that theme sensitive, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIconThemeSensitive("abc")}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image whose design depends on the currently active theme
 | 
			
		||||
	 * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
 | 
			
		||||
	 * resource folder and scales it to the given size.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @param size the size of the image to scale to
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
 | 
			
		||||
	 *          {@code /icons/light/abc.png}, and load one of them in size 16.<br>
 | 
			
		||||
	 *          To do that theme sensitive, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIconThemeSensitive("abc", 16)}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 * Loads images specified by an enum. The images have to be named like the
 | 
			
		||||
	 * lowercase enum constants with {@code .png} extension and be located inside a
 | 
			
		||||
	 * folder with the lowercase name of the enum, which must be contained inside
 | 
			
		||||
	 * the {@code /icons} folder.
 | 
			
		||||
	 * the {@code /icons/} folder.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param <T>       the enum that specifies the icons to load
 | 
			
		||||
	 * @param <T>       the enum that specifies the images to load
 | 
			
		||||
	 * @param enumClass the class of the enum
 | 
			
		||||
	 * @param size      the size to scale the icons to
 | 
			
		||||
	 * @return a map containing the loaded icons with the corresponding enum
 | 
			
		||||
	 * @param size      the size to scale the images to
 | 
			
		||||
	 * @return a map containing the loaded images with the corresponding enum
 | 
			
		||||
	 *         constants as keys
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
@@ -62,4 +144,17 @@ public class IconUtil {
 | 
			
		||||
			icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
 | 
			
		||||
		return icons;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * This method should be called if the display of an image depends upon the
 | 
			
		||||
	 * currently active theme.<br>
 | 
			
		||||
	 * In case of a default theme, the string returned will be
 | 
			
		||||
	 * ({@code dark/} or {@code light/}), otherwise it will be empty.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return the theme specific folder
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static String themeSpecificSubFolder() {
 | 
			
		||||
		return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -108,7 +108,7 @@ public final class Startup extends Application {
 | 
			
		||||
		groupMessageStatusCache	= new Cache<>();
 | 
			
		||||
 | 
			
		||||
		stage.setTitle("Envoy");
 | 
			
		||||
		stage.getIcons().add(IconUtil.load("/icons/envoy_logo.png"));
 | 
			
		||||
		stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
 | 
			
		||||
 | 
			
		||||
		final var sceneContext = new SceneContext(stage);
 | 
			
		||||
		sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
			
		||||
@@ -135,13 +135,4 @@ public final class Startup extends Application {
 | 
			
		||||
			logger.log(Level.SEVERE, "Unable to save local files: ", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the application.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param args the command line arguments are processed by the
 | 
			
		||||
	 *             {@link ClientConfig}
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static void main(String[] args) { launch(args); }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,9 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
	@FXML
 | 
			
		||||
	private MenuItem deleteContactMenuItem;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ImageView attachmentView;
 | 
			
		||||
 | 
			
		||||
	private LocalDB			localDB;
 | 
			
		||||
	private Client			client;
 | 
			
		||||
	private WriteProxy		writeProxy;
 | 
			
		||||
@@ -103,7 +106,8 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
		messageList.setCellFactory(MessageListCellFactory::new);
 | 
			
		||||
		userList.setCellFactory(ContactListCellFactory::new);
 | 
			
		||||
 | 
			
		||||
		settingsButton.setGraphic(new ImageView(IconUtil.load("/icons/settings.png", 16)));
 | 
			
		||||
		settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", 16)));
 | 
			
		||||
		voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20)));
 | 
			
		||||
 | 
			
		||||
		// Listen to received messages
 | 
			
		||||
		eventBus.register(MessageCreationEvent.class, e -> {
 | 
			
		||||
@@ -220,9 +224,9 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
			if (recorder.isRecording()) {
 | 
			
		||||
				recorder.cancel();
 | 
			
		||||
				recording = false;
 | 
			
		||||
				voiceButton.setText("Record Voice Message");
 | 
			
		||||
			}
 | 
			
		||||
			pendingAttachment = null;
 | 
			
		||||
			attachmentView.setVisible(false);
 | 
			
		||||
 | 
			
		||||
			remainingChars.setVisible(true);
 | 
			
		||||
			remainingChars
 | 
			
		||||
@@ -260,14 +264,23 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
			try {
 | 
			
		||||
				if (!recording) {
 | 
			
		||||
					recording = true;
 | 
			
		||||
					Platform.runLater(() -> voiceButton.setText("Recording..."));
 | 
			
		||||
					Platform.runLater(() -> {
 | 
			
		||||
						voiceButton.setText("Recording");
 | 
			
		||||
						voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", 24)));
 | 
			
		||||
					});
 | 
			
		||||
					recorder.start();
 | 
			
		||||
				} else {
 | 
			
		||||
					pendingAttachment	= new Attachment(recorder.finish(), AttachmentType.VOICE);
 | 
			
		||||
					recording			= false;
 | 
			
		||||
					Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); });
 | 
			
		||||
					Platform.runLater(() -> {
 | 
			
		||||
						voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20)));
 | 
			
		||||
						voiceButton.setText(null);
 | 
			
		||||
						checkPostConditions(false);
 | 
			
		||||
						attachmentView.setImage(IconUtil.loadIconThemeSensitive("attachment_present", 20));
 | 
			
		||||
						attachmentView.setVisible(true);
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			} catch (EnvoyException e) {
 | 
			
		||||
			} catch (final EnvoyException e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not record audio: ", e);
 | 
			
		||||
				Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
 | 
			
		||||
			}
 | 
			
		||||
@@ -303,7 +316,7 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
	private void checkPostConditions(boolean sendKeyPressed) {
 | 
			
		||||
		if (!postingPermanentlyDisabled) {
 | 
			
		||||
			if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
			
		||||
			postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
 | 
			
		||||
			postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
 | 
			
		||||
		} else {
 | 
			
		||||
			final var noMoreMessaging = "Go online to send messages";
 | 
			
		||||
			if (!infoLabel.getText().equals(noMoreMessaging))
 | 
			
		||||
@@ -367,6 +380,7 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
      if (pendingAttachment != null) {
 | 
			
		||||
				builder.setAttachment(pendingAttachment);
 | 
			
		||||
				pendingAttachment = null;
 | 
			
		||||
				attachmentView.setVisible(false);
 | 
			
		||||
			}
 | 
			
		||||
      // Building the final message
 | 
			
		||||
			final var	message	= currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button:pressed {
 | 
			
		||||
	-fx-background-color: darkgray;
 | 
			
		||||
	-fx-background-color: darkviolet;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button:disabled {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
 | 
			
		||||
<?import javafx.geometry.Insets?>
 | 
			
		||||
<?import javafx.geometry.Rectangle2D?>
 | 
			
		||||
<?import javafx.scene.control.Button?>
 | 
			
		||||
<?import javafx.scene.control.ButtonBar?>
 | 
			
		||||
<?import javafx.scene.control.ContextMenu?>
 | 
			
		||||
@@ -9,6 +10,7 @@
 | 
			
		||||
<?import javafx.scene.control.MenuItem?>
 | 
			
		||||
<?import javafx.scene.control.TextArea?>
 | 
			
		||||
<?import javafx.scene.control.Tooltip?>
 | 
			
		||||
<?import javafx.scene.image.ImageView?>
 | 
			
		||||
<?import javafx.scene.layout.ColumnConstraints?>
 | 
			
		||||
<?import javafx.scene.layout.GridPane?>
 | 
			
		||||
<?import javafx.scene.layout.RowConstraints?>
 | 
			
		||||
@@ -23,6 +25,9 @@
 | 
			
		||||
			prefWidth="160.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="ALWAYS"
 | 
			
		||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="ALWAYS"
 | 
			
		||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="7.0"
 | 
			
		||||
			prefWidth="357.0" />
 | 
			
		||||
	</columnConstraints>
 | 
			
		||||
	<rowConstraints>
 | 
			
		||||
		<RowConstraints maxHeight="-Infinity"
 | 
			
		||||
@@ -43,7 +48,7 @@
 | 
			
		||||
			prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1"
 | 
			
		||||
			GridPane.rowSpan="2147483647">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" left="10.0" />
 | 
			
		||||
				<Insets bottom="5.0" left="10.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
			<padding>
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
@@ -68,8 +73,8 @@
 | 
			
		||||
		</Label>
 | 
			
		||||
		<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
			
		||||
			onAction="#settingsButtonClicked" text="_Settings"
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.halignment="RIGHT"
 | 
			
		||||
			GridPane.valignment="CENTER">
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
 | 
			
		||||
			GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
@@ -105,17 +110,21 @@
 | 
			
		||||
			</contextMenu>
 | 
			
		||||
		</ListView>
 | 
			
		||||
		<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
 | 
			
		||||
			GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
			
		||||
			GridPane.valignment="BOTTOM">
 | 
			
		||||
			GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
 | 
			
		||||
			GridPane.rowIndex="5" GridPane.valignment="BOTTOM">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets right="10.0" top="5.0" />
 | 
			
		||||
				<Insets right="10.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
			<buttons>
 | 
			
		||||
				<Button fx:id="voiceButton" disable="true"
 | 
			
		||||
					onAction="#voiceButtonClicked" text="_Record Voice Message" />
 | 
			
		||||
					onAction="#voiceButtonClicked">
 | 
			
		||||
					<padding>
 | 
			
		||||
						<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
					</padding>
 | 
			
		||||
				</Button>
 | 
			
		||||
				<Button fx:id="postButton" defaultButton="true"
 | 
			
		||||
					disable="true" mnemonicParsing="true" onAction="#postMessage"
 | 
			
		||||
					prefHeight="10.0" prefWidth="75.0" text="_Post">
 | 
			
		||||
					text="_Post">
 | 
			
		||||
					<tooltip>
 | 
			
		||||
						<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
			
		||||
							maxWidth="350.0"
 | 
			
		||||
@@ -140,7 +149,8 @@
 | 
			
		||||
			onInputMethodTextChanged="#messageTextUpdated"
 | 
			
		||||
			onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
			
		||||
			prefHeight="200.0" prefWidth="200.0" wrapText="true"
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.rowIndex="4">
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
 | 
			
		||||
			GridPane.rowIndex="4">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
@@ -156,7 +166,7 @@
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
			</padding>
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" left="10.0" right="5.0" top="5.0" />
 | 
			
		||||
				<Insets bottom="10.0" left="10.0" right="5.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
		</Button>
 | 
			
		||||
		<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
			
		||||
@@ -189,5 +199,16 @@
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
			</padding>
 | 
			
		||||
		</Label>
 | 
			
		||||
		<ImageView fx:id="attachmentView" pickOnBounds="true"
 | 
			
		||||
			preserveRatio="true" visible="false" GridPane.columnIndex="1"
 | 
			
		||||
			GridPane.columnSpan="2147483647" GridPane.halignment="RIGHT"
 | 
			
		||||
			GridPane.rowIndex="3">
 | 
			
		||||
			<viewport>
 | 
			
		||||
				<Rectangle2D height="20.0" width="20.0" />
 | 
			
		||||
			</viewport>
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="5.0" right="10.0" top="5.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
		</ImageView>
 | 
			
		||||
	</children>
 | 
			
		||||
</GridPane>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/dark/attachment.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/dark/attachment_present.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB  | 
| 
		 Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/dark/microphone.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
| 
		 Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/light/attachment.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 13 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/light/attachment_present.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/light/forward.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/light/microphone.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 18 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/light/settings.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/icons/microphone_recording.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  |