Merge branch 'develop' into f/groupMessages
							
								
								
									
										34
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						@@ -40,6 +40,18 @@
 | 
				
			|||||||
			<artifactId>javafx-fxml</artifactId>
 | 
								<artifactId>javafx-fxml</artifactId>
 | 
				
			||||||
			<version>11.0.2</version>
 | 
								<version>11.0.2</version>
 | 
				
			||||||
		</dependency>
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.openjfx</groupId>
 | 
				
			||||||
 | 
								<artifactId>javafx-graphics </artifactId>
 | 
				
			||||||
 | 
								<version>11</version>
 | 
				
			||||||
 | 
								<classifier>win</classifier>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.openjfx</groupId>
 | 
				
			||||||
 | 
								<artifactId>javafx-graphics </artifactId>
 | 
				
			||||||
 | 
								<version>11</version>
 | 
				
			||||||
 | 
								<classifier>linux</classifier>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
	</dependencies>
 | 
						</dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<build>
 | 
						<build>
 | 
				
			||||||
@@ -61,23 +73,23 @@
 | 
				
			|||||||
		<plugins>
 | 
							<plugins>
 | 
				
			||||||
			<plugin>
 | 
								<plugin>
 | 
				
			||||||
				<groupId>org.apache.maven.plugins</groupId>
 | 
									<groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
				<artifactId>maven-assembly-plugin</artifactId>
 | 
									<artifactId>maven-shade-plugin</artifactId>
 | 
				
			||||||
				<version>3.2.0</version>
 | 
									<version>3.2.4</version>
 | 
				
			||||||
				<executions>
 | 
									<executions>
 | 
				
			||||||
					<execution>
 | 
										<execution>
 | 
				
			||||||
						<phase>package</phase>
 | 
											<phase>package</phase>
 | 
				
			||||||
						<goals>
 | 
											<goals>
 | 
				
			||||||
							<goal>single</goal>
 | 
												<goal>shade</goal>
 | 
				
			||||||
						</goals>
 | 
											</goals>
 | 
				
			||||||
						<configuration>
 | 
											<configuration>
 | 
				
			||||||
							<archive>
 | 
												<shadedArtifactAttached>true</shadedArtifactAttached>
 | 
				
			||||||
								<manifest>
 | 
												<sharedClassifierName>envoy</sharedClassifierName>
 | 
				
			||||||
									<mainClass>envoy.client.ui.Startup</mainClass>
 | 
												<transformers>
 | 
				
			||||||
								</manifest>
 | 
													<transformer
 | 
				
			||||||
							</archive>
 | 
														implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
 | 
				
			||||||
							<descriptorRefs>
 | 
														<mainClass>envoy.client.Main</mainClass>
 | 
				
			||||||
								<descriptorRef>jar-with-dependencies</descriptorRef>
 | 
													</transformer>
 | 
				
			||||||
							</descriptorRefs>
 | 
												</transformers>
 | 
				
			||||||
						</configuration>
 | 
											</configuration>
 | 
				
			||||||
					</execution>
 | 
										</execution>
 | 
				
			||||||
				</executions>
 | 
									</executions>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -93,6 +93,15 @@ public class Settings {
 | 
				
			|||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
 | 
						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
 | 
						 * @return {@code true}, if pressing the {@code Enter} key suffices to send a
 | 
				
			||||||
	 *         message. Otherwise it has to be pressed in conjunction with the
 | 
						 *         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.ColumnConstraints;
 | 
				
			||||||
import javafx.scene.layout.GridPane;
 | 
					import javafx.scene.layout.GridPane;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import envoy.client.data.Settings;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This class offers a text field that is automatically equipped with a clear
 | 
					 * This class offers a text field that is automatically equipped with a clear
 | 
				
			||||||
 * button.
 | 
					 * button.
 | 
				
			||||||
@@ -49,10 +47,7 @@ public class ClearableTextField extends GridPane {
 | 
				
			|||||||
	public ClearableTextField(String text, int size) {
 | 
						public ClearableTextField(String text, int size) {
 | 
				
			||||||
		// initializing the textField and the button
 | 
							// initializing the textField and the button
 | 
				
			||||||
		textField	= new TextField(text);
 | 
							textField	= new TextField(text);
 | 
				
			||||||
		clearButton	= new Button("",
 | 
							clearButton	= new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
 | 
				
			||||||
				new ImageView(IconUtil.load(
 | 
					 | 
				
			||||||
						Settings.getInstance().getCurrentTheme().equals("dark") ? "/icons/clear_button_white.png" : "/icons/clear_button_black.png",
 | 
					 | 
				
			||||||
						size)));
 | 
					 | 
				
			||||||
		clearButton.setOnAction(e -> textField.clear());
 | 
							clearButton.setOnAction(e -> textField.clear());
 | 
				
			||||||
		clearButton.setFocusTraversable(false);
 | 
							clearButton.setFocusTraversable(false);
 | 
				
			||||||
		clearButton.getStyleClass().clear();
 | 
							clearButton.getStyleClass().clear();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,9 +2,13 @@ package envoy.client.ui;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import java.util.EnumMap;
 | 
					import java.util.EnumMap;
 | 
				
			||||||
import java.util.EnumSet;
 | 
					import java.util.EnumSet;
 | 
				
			||||||
 | 
					import java.util.logging.Level;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javafx.scene.image.Image;
 | 
					import javafx.scene.image.Image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import envoy.client.data.Settings;
 | 
				
			||||||
 | 
					import envoy.util.EnvoyLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides static utility methods for loading icons from the resource
 | 
					 * Provides static utility methods for loading icons from the resource
 | 
				
			||||||
 * folder.
 | 
					 * folder.
 | 
				
			||||||
@@ -21,37 +25,115 @@ public class IconUtil {
 | 
				
			|||||||
	private 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
 | 
						 * @param path the path to the icon inside the resource folder
 | 
				
			||||||
	 * @return the icon
 | 
						 * @return the loaded image
 | 
				
			||||||
	 * @since Envoy Client v0.1-beta
 | 
						 * @since Envoy Client v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); }
 | 
						public static Image load(String path) {
 | 
				
			||||||
 | 
							Image image = null;
 | 
				
			||||||
	/**
 | 
							try {
 | 
				
			||||||
	 * Loads an icon from the resource folder and scales it to a given size.
 | 
								image = new Image(IconUtil.class.getResource(path).toExternalForm());
 | 
				
			||||||
	 *
 | 
							} catch (final NullPointerException e) {
 | 
				
			||||||
	 * @param path the path to the icon inside the resource folder
 | 
								EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
 | 
				
			||||||
	 * @param size the size to scale the icon to
 | 
							}
 | 
				
			||||||
	 * @return the scaled icon
 | 
							return image;
 | 
				
			||||||
	 * @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);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
 | 
						 * 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
 | 
						 * 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
 | 
						 * 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 enumClass the class of the enum
 | 
				
			||||||
	 * @param size      the size to scale the icons to
 | 
						 * @param size      the size to scale the images to
 | 
				
			||||||
	 * @return a map containing the loaded icons with the corresponding enum
 | 
						 * @return a map containing the loaded images with the corresponding enum
 | 
				
			||||||
	 *         constants as keys
 | 
						 *         constants as keys
 | 
				
			||||||
	 * @since Envoy Client v0.1-beta
 | 
						 * @since Envoy Client v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
@@ -62,4 +144,17 @@ public class IconUtil {
 | 
				
			|||||||
			icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
 | 
								icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
 | 
				
			||||||
		return icons;
 | 
							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<>();
 | 
							groupMessageStatusCache	= new Cache<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		stage.setTitle("Envoy");
 | 
							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);
 | 
							final var sceneContext = new SceneContext(stage);
 | 
				
			||||||
		sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
							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);
 | 
								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
 | 
						@FXML
 | 
				
			||||||
	private MenuItem deleteContactMenuItem;
 | 
						private MenuItem deleteContactMenuItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						private ImageView attachmentView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private LocalDB			localDB;
 | 
						private LocalDB			localDB;
 | 
				
			||||||
	private Client			client;
 | 
						private Client			client;
 | 
				
			||||||
	private WriteProxy		writeProxy;
 | 
						private WriteProxy		writeProxy;
 | 
				
			||||||
@@ -103,7 +106,8 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
		messageList.setCellFactory(MessageListCellFactory::new);
 | 
							messageList.setCellFactory(MessageListCellFactory::new);
 | 
				
			||||||
		userList.setCellFactory(ContactListCellFactory::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
 | 
							// Listen to received messages
 | 
				
			||||||
		eventBus.register(MessageCreationEvent.class, e -> {
 | 
							eventBus.register(MessageCreationEvent.class, e -> {
 | 
				
			||||||
@@ -220,9 +224,9 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
			if (recorder.isRecording()) {
 | 
								if (recorder.isRecording()) {
 | 
				
			||||||
				recorder.cancel();
 | 
									recorder.cancel();
 | 
				
			||||||
				recording = false;
 | 
									recording = false;
 | 
				
			||||||
				voiceButton.setText("Record Voice Message");
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			pendingAttachment = null;
 | 
								pendingAttachment = null;
 | 
				
			||||||
 | 
								attachmentView.setVisible(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			remainingChars.setVisible(true);
 | 
								remainingChars.setVisible(true);
 | 
				
			||||||
			remainingChars
 | 
								remainingChars
 | 
				
			||||||
@@ -260,14 +264,23 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
			try {
 | 
								try {
 | 
				
			||||||
				if (!recording) {
 | 
									if (!recording) {
 | 
				
			||||||
					recording = true;
 | 
										recording = true;
 | 
				
			||||||
					Platform.runLater(() -> voiceButton.setText("Recording..."));
 | 
										Platform.runLater(() -> {
 | 
				
			||||||
 | 
											voiceButton.setText("Recording");
 | 
				
			||||||
 | 
											voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", 24)));
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
					recorder.start();
 | 
										recorder.start();
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					pendingAttachment	= new Attachment(recorder.finish(), AttachmentType.VOICE);
 | 
										pendingAttachment	= new Attachment(recorder.finish(), AttachmentType.VOICE);
 | 
				
			||||||
					recording			= false;
 | 
										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);
 | 
									logger.log(Level.SEVERE, "Could not record audio: ", e);
 | 
				
			||||||
				Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
 | 
									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) {
 | 
						private void checkPostConditions(boolean sendKeyPressed) {
 | 
				
			||||||
		if (!postingPermanentlyDisabled) {
 | 
							if (!postingPermanentlyDisabled) {
 | 
				
			||||||
			if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
								if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
				
			||||||
			postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
 | 
								postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			final var noMoreMessaging = "Go online to send messages";
 | 
								final var noMoreMessaging = "Go online to send messages";
 | 
				
			||||||
			if (!infoLabel.getText().equals(noMoreMessaging))
 | 
								if (!infoLabel.getText().equals(noMoreMessaging))
 | 
				
			||||||
@@ -367,6 +380,7 @@ public final class ChatScene implements Restorable {
 | 
				
			|||||||
      if (pendingAttachment != null) {
 | 
					      if (pendingAttachment != null) {
 | 
				
			||||||
				builder.setAttachment(pendingAttachment);
 | 
									builder.setAttachment(pendingAttachment);
 | 
				
			||||||
				pendingAttachment = null;
 | 
									pendingAttachment = null;
 | 
				
			||||||
 | 
									attachmentView.setVisible(false);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
      // Building the final message
 | 
					      // Building the final message
 | 
				
			||||||
			final var	message	= currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
 | 
								final var	message	= currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.button:pressed {
 | 
					.button:pressed {
 | 
				
			||||||
	-fx-background-color: darkgray;
 | 
						-fx-background-color: darkviolet;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.button:disabled {
 | 
					.button:disabled {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<?import javafx.geometry.Insets?>
 | 
					<?import javafx.geometry.Insets?>
 | 
				
			||||||
 | 
					<?import javafx.geometry.Rectangle2D?>
 | 
				
			||||||
<?import javafx.scene.control.Button?>
 | 
					<?import javafx.scene.control.Button?>
 | 
				
			||||||
<?import javafx.scene.control.ButtonBar?>
 | 
					<?import javafx.scene.control.ButtonBar?>
 | 
				
			||||||
<?import javafx.scene.control.ContextMenu?>
 | 
					<?import javafx.scene.control.ContextMenu?>
 | 
				
			||||||
@@ -9,6 +10,7 @@
 | 
				
			|||||||
<?import javafx.scene.control.MenuItem?>
 | 
					<?import javafx.scene.control.MenuItem?>
 | 
				
			||||||
<?import javafx.scene.control.TextArea?>
 | 
					<?import javafx.scene.control.TextArea?>
 | 
				
			||||||
<?import javafx.scene.control.Tooltip?>
 | 
					<?import javafx.scene.control.Tooltip?>
 | 
				
			||||||
 | 
					<?import javafx.scene.image.ImageView?>
 | 
				
			||||||
<?import javafx.scene.layout.ColumnConstraints?>
 | 
					<?import javafx.scene.layout.ColumnConstraints?>
 | 
				
			||||||
<?import javafx.scene.layout.GridPane?>
 | 
					<?import javafx.scene.layout.GridPane?>
 | 
				
			||||||
<?import javafx.scene.layout.RowConstraints?>
 | 
					<?import javafx.scene.layout.RowConstraints?>
 | 
				
			||||||
@@ -23,6 +25,9 @@
 | 
				
			|||||||
			prefWidth="160.0" />
 | 
								prefWidth="160.0" />
 | 
				
			||||||
		<ColumnConstraints hgrow="ALWAYS"
 | 
							<ColumnConstraints hgrow="ALWAYS"
 | 
				
			||||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
 | 
								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>
 | 
						</columnConstraints>
 | 
				
			||||||
	<rowConstraints>
 | 
						<rowConstraints>
 | 
				
			||||||
		<RowConstraints maxHeight="-Infinity"
 | 
							<RowConstraints maxHeight="-Infinity"
 | 
				
			||||||
@@ -43,7 +48,7 @@
 | 
				
			|||||||
			prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1"
 | 
								prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1"
 | 
				
			||||||
			GridPane.rowSpan="2147483647">
 | 
								GridPane.rowSpan="2147483647">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets bottom="10.0" left="10.0" />
 | 
									<Insets bottom="5.0" left="10.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
			<padding>
 | 
								<padding>
 | 
				
			||||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
									<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
@@ -68,8 +73,8 @@
 | 
				
			|||||||
		</Label>
 | 
							</Label>
 | 
				
			||||||
		<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
							<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
				
			||||||
			onAction="#settingsButtonClicked" text="_Settings"
 | 
								onAction="#settingsButtonClicked" text="_Settings"
 | 
				
			||||||
			GridPane.columnIndex="1" GridPane.halignment="RIGHT"
 | 
								GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
 | 
				
			||||||
			GridPane.valignment="CENTER">
 | 
								GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
									<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
@@ -105,17 +110,21 @@
 | 
				
			|||||||
			</contextMenu>
 | 
								</contextMenu>
 | 
				
			||||||
		</ListView>
 | 
							</ListView>
 | 
				
			||||||
		<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
 | 
							<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
 | 
				
			||||||
			GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
								GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
 | 
				
			||||||
			GridPane.valignment="BOTTOM">
 | 
								GridPane.rowIndex="5" GridPane.valignment="BOTTOM">
 | 
				
			||||||
			<GridPane.margin>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets right="10.0" top="5.0" />
 | 
									<Insets right="10.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
			<buttons>
 | 
								<buttons>
 | 
				
			||||||
				<Button fx:id="voiceButton" disable="true"
 | 
									<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"
 | 
									<Button fx:id="postButton" defaultButton="true"
 | 
				
			||||||
					disable="true" mnemonicParsing="true" onAction="#postMessage"
 | 
										disable="true" mnemonicParsing="true" onAction="#postMessage"
 | 
				
			||||||
					prefHeight="10.0" prefWidth="75.0" text="_Post">
 | 
										text="_Post">
 | 
				
			||||||
					<tooltip>
 | 
										<tooltip>
 | 
				
			||||||
						<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
											<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
				
			||||||
							maxWidth="350.0"
 | 
												maxWidth="350.0"
 | 
				
			||||||
@@ -140,7 +149,8 @@
 | 
				
			|||||||
			onInputMethodTextChanged="#messageTextUpdated"
 | 
								onInputMethodTextChanged="#messageTextUpdated"
 | 
				
			||||||
			onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
								onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
				
			||||||
			prefHeight="200.0" prefWidth="200.0" wrapText="true"
 | 
								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>
 | 
								<GridPane.margin>
 | 
				
			||||||
				<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
 | 
									<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
 | 
				
			||||||
			</GridPane.margin>
 | 
								</GridPane.margin>
 | 
				
			||||||
@@ -156,7 +166,7 @@
 | 
				
			|||||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
									<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
			</padding>
 | 
								</padding>
 | 
				
			||||||
			<GridPane.margin>
 | 
								<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>
 | 
								</GridPane.margin>
 | 
				
			||||||
		</Button>
 | 
							</Button>
 | 
				
			||||||
		<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
							<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
				
			||||||
@@ -189,5 +199,16 @@
 | 
				
			|||||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
									<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
			</padding>
 | 
								</padding>
 | 
				
			||||||
		</Label>
 | 
							</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>
 | 
						</children>
 | 
				
			||||||
</GridPane>
 | 
					</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  |