Merge branch 'develop' into f/groupMessages
This commit is contained in:
		@@ -245,7 +245,7 @@ org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
 | 
			
		||||
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
 | 
			
		||||
@@ -439,7 +439,7 @@ org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
 | 
			
		||||
@@ -450,7 +450,7 @@ org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never
 | 
			
		||||
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty
 | 
			
		||||
org.eclipse.jdt.core.formatter.lineSplit=150
 | 
			
		||||
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
 | 
			
		||||
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								src/main/java/envoy/client/data/audio/AudioPlayer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/main/java/envoy/client/data/audio/AudioPlayer.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
 | 
			
		||||
import javax.sound.sampled.*;
 | 
			
		||||
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plays back audio from a byte array.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioPlayer.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioPlayer {
 | 
			
		||||
 | 
			
		||||
	private final AudioFormat	format;
 | 
			
		||||
	private final DataLine.Info	info;
 | 
			
		||||
 | 
			
		||||
	private Clip clip;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the player with the default audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the player with a given audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param format the audio format to use
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioPlayer(AudioFormat format) {
 | 
			
		||||
		this.format	= format;
 | 
			
		||||
		info		= new DataLine.Info(Clip.class, format);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if audio play back is supported
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isSupported() { return AudioSystem.isLineSupported(info); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Plays back an audio clip.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param data the data of the clip
 | 
			
		||||
	 * @throws EnvoyException if the play back failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void play(byte[] data) throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
			clip = (Clip) AudioSystem.getLine(info);
 | 
			
		||||
			clip.open(format, data, 0, data.length);
 | 
			
		||||
			clip.start();
 | 
			
		||||
		} catch (final LineUnavailableException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot play back audio", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								src/main/java/envoy/client/data/audio/AudioRecorder.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/main/java/envoy/client/data/audio/AudioRecorder.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
 | 
			
		||||
import javax.sound.sampled.*;
 | 
			
		||||
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Records audio and exports it as a byte array.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioRecorder.java</strong><br>
 | 
			
		||||
 * Created: <strong>02.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioRecorder {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The default audio format used for recording and play back.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
 | 
			
		||||
 | 
			
		||||
	private final AudioFormat	format;
 | 
			
		||||
	private final DataLine.Info	info;
 | 
			
		||||
 | 
			
		||||
	private TargetDataLine	line;
 | 
			
		||||
	private Path			tempFile;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the recorder with the default audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the recorder with a given audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param format the audio format to use
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioRecorder(AudioFormat format) {
 | 
			
		||||
		this.format	= format;
 | 
			
		||||
		info		= new DataLine.Info(TargetDataLine.class, format);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if audio recording is supported
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isSupported() { return AudioSystem.isLineSupported(info); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if the recorder is active
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isRecording() { return line != null && line.isActive(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the audio recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws EnvoyException if starting the recording failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void start() throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
 | 
			
		||||
			// Open the line
 | 
			
		||||
			line = (TargetDataLine) AudioSystem.getLine(info);
 | 
			
		||||
			line.open(format);
 | 
			
		||||
			line.start();
 | 
			
		||||
 | 
			
		||||
			// Prepare temp file
 | 
			
		||||
			tempFile = Files.createTempFile("recording", "wav");
 | 
			
		||||
 | 
			
		||||
			// Start the recording
 | 
			
		||||
			final var ais = new AudioInputStream(line);
 | 
			
		||||
			AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
 | 
			
		||||
		} catch (IOException | LineUnavailableException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot record voice", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stops the recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return the finished recording
 | 
			
		||||
	 * @throws EnvoyException if finishing the recording failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public byte[] finish() throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
			line.stop();
 | 
			
		||||
			line.close();
 | 
			
		||||
			final byte[] data = Files.readAllBytes(tempFile);
 | 
			
		||||
			Files.delete(tempFile);
 | 
			
		||||
			return data;
 | 
			
		||||
		} catch (final IOException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot save voice recording", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Cancels the active recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void cancel() {
 | 
			
		||||
		line.stop();
 | 
			
		||||
		line.close();
 | 
			
		||||
		try {
 | 
			
		||||
			Files.deleteIfExists(tempFile);
 | 
			
		||||
		} catch (IOException e) {}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/main/java/envoy/client/data/audio/package-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/main/java/envoy/client/data/audio/package-info.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Contains classes related to recording and playing back audio clips.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>package-info.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
@@ -51,16 +51,25 @@ public class Receiver extends Thread {
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
		while (true) {
 | 
			
		||||
			try {
 | 
			
		||||
				// Read object length
 | 
			
		||||
				final byte[] lenBytes = new byte[4];
 | 
			
		||||
				in.read(lenBytes);
 | 
			
		||||
				final int len = SerializationUtils.bytesToInt(lenBytes, 0);
 | 
			
		||||
				logger.log(Level.FINEST, "Expecting object of length " + len + ".");
 | 
			
		||||
 | 
			
		||||
				// Read object into byte array
 | 
			
		||||
				final byte[]	objBytes	= new byte[len];
 | 
			
		||||
				in.read(objBytes);
 | 
			
		||||
				final int		bytesRead	= in.read(objBytes);
 | 
			
		||||
				logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
 | 
			
		||||
 | 
			
		||||
				// Catch LV encoding errors
 | 
			
		||||
				if (len != bytesRead) {
 | 
			
		||||
					logger.log(Level.WARNING,
 | 
			
		||||
							String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
 | 
			
		||||
					final Object obj = oin.readObject();
 | 
			
		||||
@@ -75,13 +84,14 @@ public class Receiver extends Thread {
 | 
			
		||||
										obj.getClass()));
 | 
			
		||||
					else processor.accept(obj);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			} catch (final SocketException e) {
 | 
			
		||||
				// Connection probably closed by client.
 | 
			
		||||
				return;
 | 
			
		||||
			} catch (final Exception e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Error on receiver thread", e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds an object processor to this {@link Receiver}. It will be called once an
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								src/main/java/envoy/client/ui/AudioControl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/main/java/envoy/client/ui/AudioControl.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.control.Alert;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.layout.HBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.audio.AudioPlayer;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enables the play back of audio clips through a button.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioControl.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioControl extends HBox {
 | 
			
		||||
 | 
			
		||||
	private AudioPlayer player = new AudioPlayer();
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the audio control.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param audioData the audio data to play.
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioControl(byte[] audioData) {
 | 
			
		||||
		var button = new Button("Play");
 | 
			
		||||
		button.setOnAction(e -> {
 | 
			
		||||
			try {
 | 
			
		||||
				player.play(audioData);
 | 
			
		||||
			} catch (EnvoyException ex) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not play back audio: ", ex);
 | 
			
		||||
				new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		getChildren().add(button);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -18,6 +18,7 @@ import javafx.scene.input.KeyEvent;
 | 
			
		||||
import javafx.scene.paint.Color;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.data.audio.AudioRecorder;
 | 
			
		||||
import envoy.client.event.MessageCreationEvent;
 | 
			
		||||
import envoy.client.net.Client;
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
@@ -29,7 +30,9 @@ import envoy.client.ui.listcell.MessageControl;
 | 
			
		||||
import envoy.client.ui.listcell.MessageListCellFactory;
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.event.*;
 | 
			
		||||
import envoy.data.Attachment.AttachmentType;
 | 
			
		||||
import envoy.event.contact.ContactOperation;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -54,6 +57,9 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button postButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button voiceButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button settingsButton;
 | 
			
		||||
 | 
			
		||||
@@ -74,9 +80,11 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
	private WriteProxy		writeProxy;
 | 
			
		||||
	private SceneContext	sceneContext;
 | 
			
		||||
 | 
			
		||||
	private boolean postingPermanentlyDisabled = false;
 | 
			
		||||
 | 
			
		||||
	private Chat			currentChat;
 | 
			
		||||
	private AudioRecorder	recorder;
 | 
			
		||||
	private boolean			recording;
 | 
			
		||||
	private Attachment		pendingAttachment;
 | 
			
		||||
	private boolean			postingPermanentlyDisabled	= false;
 | 
			
		||||
 | 
			
		||||
	private static final Settings	settings			= Settings.getInstance();
 | 
			
		||||
	private static final EventBus	eventBus			= EventBus.getInstance();
 | 
			
		||||
@@ -173,6 +181,8 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
		contactLabel.setText(localDB.getUser().getName());
 | 
			
		||||
		MessageControl.setUser(localDB.getUser());
 | 
			
		||||
		if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
 | 
			
		||||
 | 
			
		||||
		recorder = new AudioRecorder();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
@@ -206,11 +216,20 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
				logger.log(Level.WARNING, "Could not read current chat.", e);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Discard the pending attachment
 | 
			
		||||
			if (recorder.isRecording()) {
 | 
			
		||||
				recorder.cancel();
 | 
			
		||||
				recording = false;
 | 
			
		||||
				voiceButton.setText("Record Voice Message");
 | 
			
		||||
			}
 | 
			
		||||
			pendingAttachment = null;
 | 
			
		||||
 | 
			
		||||
			remainingChars.setVisible(true);
 | 
			
		||||
			remainingChars
 | 
			
		||||
				.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
 | 
			
		||||
		}
 | 
			
		||||
		messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
 | 
			
		||||
		voiceButton.setDisable(!recorder.isSupported());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
@@ -235,6 +254,26 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
		sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void voiceButtonClicked() {
 | 
			
		||||
		new Thread(() -> {
 | 
			
		||||
			try {
 | 
			
		||||
				if (!recording) {
 | 
			
		||||
					recording = true;
 | 
			
		||||
					Platform.runLater(() -> voiceButton.setText("Recording..."));
 | 
			
		||||
					recorder.start();
 | 
			
		||||
				} else {
 | 
			
		||||
					pendingAttachment	= new Attachment(recorder.finish(), AttachmentType.VOICE);
 | 
			
		||||
					recording			= false;
 | 
			
		||||
					Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); });
 | 
			
		||||
				}
 | 
			
		||||
			} catch (EnvoyException e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not record audio: ", e);
 | 
			
		||||
				Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
 | 
			
		||||
			}
 | 
			
		||||
		}).start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks the text length of the {@code messageTextArea}, adjusts the
 | 
			
		||||
	 * {@code remainingChars} label and checks whether to send the message
 | 
			
		||||
@@ -257,11 +296,14 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void checkPostConditions(KeyEvent e) {
 | 
			
		||||
		checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
 | 
			
		||||
				|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void checkPostConditions(boolean sendKeyPressed) {
 | 
			
		||||
		if (!postingPermanentlyDisabled) {
 | 
			
		||||
			if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
 | 
			
		||||
					|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()))
 | 
			
		||||
				postMessage();
 | 
			
		||||
			postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
 | 
			
		||||
			if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
			
		||||
			postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
 | 
			
		||||
		} else {
 | 
			
		||||
			final var noMoreMessaging = "Go online to send messages";
 | 
			
		||||
			if (!infoLabel.getText().equals(noMoreMessaging))
 | 
			
		||||
@@ -317,11 +359,16 @@ public final class ChatScene implements Restorable {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		final var text = messageTextArea.getText().strip();
 | 
			
		||||
		if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent.");
 | 
			
		||||
		try {
 | 
			
		||||
			// Create and send message
 | 
			
		||||
			// 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;
 | 
			
		||||
			}
 | 
			
		||||
      // Building the final message
 | 
			
		||||
			final var	message	= currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
 | 
			
		||||
					: builder.build();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import javafx.scene.image.Image;
 | 
			
		||||
import javafx.scene.image.ImageView;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.AudioControl;
 | 
			
		||||
import envoy.client.ui.IconUtil;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
@@ -38,6 +39,20 @@ public class MessageControl extends VBox {
 | 
			
		||||
	public MessageControl(Message message) {
 | 
			
		||||
		// Creating the underlying VBox, the dateLabel and the textLabel
 | 
			
		||||
		super(new Label(dateFormat.format(message.getCreationDate())));
 | 
			
		||||
 | 
			
		||||
		// Handling message attachment display
 | 
			
		||||
		if (message.hasAttachment()) switch (message.getAttachment().getType()) {
 | 
			
		||||
			case PICTURE:
 | 
			
		||||
				break;
 | 
			
		||||
			case VIDEO:
 | 
			
		||||
				break;
 | 
			
		||||
			case VOICE:
 | 
			
		||||
				getChildren().add(new AudioControl(message.getAttachment().getData()));
 | 
			
		||||
				break;
 | 
			
		||||
			case DOCUMENT:
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		final var textLabel = new Label(message.getText());
 | 
			
		||||
		textLabel.setWrapText(true);
 | 
			
		||||
		getChildren().add(textLabel);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
.button, .list-cell {
 | 
			
		||||
.button, .list-cell, .progress-bar * {
 | 
			
		||||
	-fx-background-radius: 5.0em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +26,10 @@
 | 
			
		||||
	-fx-text-fill: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress-bar{
 | 
			
		||||
	-fx-progress-color: blue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.online {
 | 
			
		||||
	-fx-text-fill: limegreen;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
<?import javafx.geometry.Insets?>
 | 
			
		||||
<?import javafx.scene.control.Button?>
 | 
			
		||||
<?import javafx.scene.control.ButtonBar?>
 | 
			
		||||
<?import javafx.scene.control.ContextMenu?>
 | 
			
		||||
<?import javafx.scene.control.Label?>
 | 
			
		||||
<?import javafx.scene.control.ListView?>
 | 
			
		||||
@@ -18,29 +19,24 @@
 | 
			
		||||
	xmlns:fx="http://javafx.com/fxml/1"
 | 
			
		||||
	fx:controller="envoy.client.ui.controller.ChatScene">
 | 
			
		||||
	<columnConstraints>
 | 
			
		||||
		<ColumnConstraints hgrow="SOMETIMES"
 | 
			
		||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="25.0"
 | 
			
		||||
			prefWidth="161.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="SOMETIMES"
 | 
			
		||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="65.0"
 | 
			
		||||
			prefWidth="357.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="SOMETIMES" maxWidth="10.0"
 | 
			
		||||
			minWidth="10.0" percentWidth="10.0" prefWidth="10.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="NEVER" minWidth="60.0"
 | 
			
		||||
			prefWidth="160.0" />
 | 
			
		||||
		<ColumnConstraints hgrow="ALWAYS"
 | 
			
		||||
			maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
 | 
			
		||||
	</columnConstraints>
 | 
			
		||||
	<rowConstraints>
 | 
			
		||||
		<RowConstraints maxHeight="-Infinity"
 | 
			
		||||
			minHeight="-Infinity" prefHeight="50.0" vgrow="NEVER" />
 | 
			
		||||
		<RowConstraints maxHeight="-Infinity"
 | 
			
		||||
			minHeight="-Infinity" prefHeight="20.0" vgrow="NEVER" />
 | 
			
		||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
			
		||||
			minHeight="10.0" percentHeight="10.0" prefHeight="70.0"
 | 
			
		||||
			vgrow="SOMETIMES" />
 | 
			
		||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
			
		||||
			minHeight="10.0" percentHeight="7.0" vgrow="SOMETIMES" />
 | 
			
		||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
			
		||||
			minHeight="10.0" percentHeight="60.0" prefHeight="50.0"
 | 
			
		||||
			vgrow="SOMETIMES" />
 | 
			
		||||
		<RowConstraints maxHeight="50.0" minHeight="10.0"
 | 
			
		||||
			percentHeight="2.0" prefHeight="50.0" vgrow="SOMETIMES" />
 | 
			
		||||
		<RowConstraints maxHeight="1.7976931348623157E308"
 | 
			
		||||
			minHeight="10.0" percentHeight="21.0" prefHeight="100.0"
 | 
			
		||||
			vgrow="SOMETIMES" />
 | 
			
		||||
			minHeight="50.0" prefHeight="155.14286150251115" vgrow="ALWAYS" />
 | 
			
		||||
		<RowConstraints maxHeight="-Infinity"
 | 
			
		||||
			minHeight="-Infinity" prefHeight="20.0" vgrow="NEVER" />
 | 
			
		||||
		<RowConstraints maxHeight="120.0" minHeight="40.0"
 | 
			
		||||
			prefHeight="60.0" vgrow="NEVER" />
 | 
			
		||||
		<RowConstraints maxHeight="-Infinity"
 | 
			
		||||
			minHeight="-Infinity" prefHeight="40.0" vgrow="NEVER" />
 | 
			
		||||
	</rowConstraints>
 | 
			
		||||
	<children>
 | 
			
		||||
		<ListView fx:id="userList" onMouseClicked="#userListClicked"
 | 
			
		||||
@@ -61,8 +57,8 @@
 | 
			
		||||
				</ContextMenu>
 | 
			
		||||
			</contextMenu>
 | 
			
		||||
		</ListView>
 | 
			
		||||
		<Label fx:id="contactLabel" prefHeight="16.0" prefWidth="250.0"
 | 
			
		||||
			text="Select a contact to chat with" GridPane.columnSpan="2">
 | 
			
		||||
		<Label fx:id="contactLabel" prefHeight="27.0" prefWidth="134.0"
 | 
			
		||||
			GridPane.columnSpan="2">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets left="10.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
@@ -72,8 +68,8 @@
 | 
			
		||||
		</Label>
 | 
			
		||||
		<Button fx:id="settingsButton" mnemonicParsing="true"
 | 
			
		||||
			onAction="#settingsButtonClicked" text="_Settings"
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.columnSpan="2"
 | 
			
		||||
			GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.halignment="RIGHT"
 | 
			
		||||
			GridPane.valignment="CENTER">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" right="10.0" top="10.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
@@ -108,14 +104,18 @@
 | 
			
		||||
				</ContextMenu>
 | 
			
		||||
			</contextMenu>
 | 
			
		||||
		</ListView>
 | 
			
		||||
		<Button fx:id="postButton" defaultButton="true" disable="true"
 | 
			
		||||
			mnemonicParsing="true" onAction="#postMessage" prefHeight="10.0"
 | 
			
		||||
			prefWidth="75.0" text="_Post" GridPane.columnIndex="2"
 | 
			
		||||
			GridPane.halignment="CENTER" GridPane.rowIndex="4"
 | 
			
		||||
		<ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
 | 
			
		||||
			GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
			
		||||
			GridPane.valignment="BOTTOM">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" right="10.0" />
 | 
			
		||||
				<Insets right="10.0" top="5.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
			<buttons>
 | 
			
		||||
				<Button fx:id="voiceButton" disable="true"
 | 
			
		||||
					onAction="#voiceButtonClicked" text="_Record Voice Message" />
 | 
			
		||||
				<Button fx:id="postButton" defaultButton="true"
 | 
			
		||||
					disable="true" mnemonicParsing="true" onAction="#postMessage"
 | 
			
		||||
					prefHeight="10.0" prefWidth="75.0" text="_Post">
 | 
			
		||||
					<tooltip>
 | 
			
		||||
						<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
 | 
			
		||||
							maxWidth="350.0"
 | 
			
		||||
@@ -134,13 +134,15 @@
 | 
			
		||||
						<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
					</padding>
 | 
			
		||||
				</Button>
 | 
			
		||||
			</buttons>
 | 
			
		||||
		</ButtonBar>
 | 
			
		||||
		<TextArea fx:id="messageTextArea" disable="true"
 | 
			
		||||
			onInputMethodTextChanged="#messageTextUpdated"
 | 
			
		||||
			onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
 | 
			
		||||
			prefHeight="200.0" prefWidth="200.0" wrapText="true"
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.rowIndex="4">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="10.0" left="5.0" top="3.0" />
 | 
			
		||||
				<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
			<opaqueInsets>
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
@@ -148,13 +150,13 @@
 | 
			
		||||
		</TextArea>
 | 
			
		||||
		<Button mnemonicParsing="true"
 | 
			
		||||
			onAction="#addContactButtonClicked" text="_Add Contacts"
 | 
			
		||||
			GridPane.halignment="CENTER" GridPane.rowIndex="4"
 | 
			
		||||
			GridPane.halignment="CENTER" GridPane.rowIndex="5"
 | 
			
		||||
			GridPane.valignment="CENTER">
 | 
			
		||||
			<padding>
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
			</padding>
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="5.0" left="10.0" right="5.0" top="5.0" />
 | 
			
		||||
				<Insets bottom="10.0" left="10.0" right="5.0" top="5.0" />
 | 
			
		||||
			</GridPane.margin>
 | 
			
		||||
		</Button>
 | 
			
		||||
		<Label id="remainingCharsLabel" fx:id="remainingChars"
 | 
			
		||||
@@ -178,7 +180,7 @@
 | 
			
		||||
			</tooltip>
 | 
			
		||||
		</Label>
 | 
			
		||||
		<Label fx:id="infoLabel" text="Something happened"
 | 
			
		||||
			wrapText="true" textFill="#faa007" visible="false"
 | 
			
		||||
			textFill="#faa007" visible="false" wrapText="true"
 | 
			
		||||
			GridPane.columnIndex="1" GridPane.rowIndex="1">
 | 
			
		||||
			<GridPane.margin>
 | 
			
		||||
				<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user