Merge branch 'develop' into f/new_ui
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -37,15 +37,6 @@
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -7,8 +7,10 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Contact;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.data.User;
|
||||
import envoy.event.MessageStatusChange;
|
||||
|
||||
/**
|
||||
@ -31,6 +33,11 @@ public class Chat implements Serializable {
|
||||
|
||||
protected int unreadAmount;
|
||||
|
||||
/**
|
||||
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
||||
*/
|
||||
protected transient long lastWritingEvent;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
@ -41,16 +48,14 @@ public class Chat implements Serializable {
|
||||
* @param recipient the user who receives the messages
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public Chat(Contact recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
public Chat(Contact recipient) { this.recipient = recipient; }
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
|
||||
|
||||
/**
|
||||
* Generates a hash code based on the recipient.
|
||||
*
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
@ -58,14 +63,14 @@ public class Chat implements Serializable {
|
||||
|
||||
/**
|
||||
* Tests equality to another object based on the recipient.
|
||||
*
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Chat)) return false;
|
||||
Chat other = (Chat) obj;
|
||||
final Chat other = (Chat) obj;
|
||||
return Objects.equals(recipient, other.recipient);
|
||||
}
|
||||
|
||||
@ -101,7 +106,7 @@ public class Chat implements Serializable {
|
||||
|
||||
/**
|
||||
* Inserts a message at the correct place according to its creation date.
|
||||
*
|
||||
*
|
||||
* @param message the message to insert
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -116,13 +121,13 @@ public class Chat implements Serializable {
|
||||
|
||||
/**
|
||||
* Increments the amount of unread messages.
|
||||
*
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void incrementUnreadAmount() { unreadAmount++; }
|
||||
|
||||
/**
|
||||
* @return the amount of unread mesages in this chat
|
||||
* @return the amount of unread messages in this chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public int getUnreadAmount() { return unreadAmount; }
|
||||
@ -140,14 +145,16 @@ public class Chat implements Serializable {
|
||||
public Contact getRecipient() { return recipient; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link User}
|
||||
* @since Envoy Client v0.1-beta
|
||||
* @return the last known time a {@link envoy.event.IsTyping} event has been
|
||||
* sent
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isUserChat() { return recipient instanceof User; }
|
||||
public long getLastWritingEvent() { return lastWritingEvent; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link Group}
|
||||
* @since Envoy Client v0.1-beta
|
||||
* Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isGroupChat() { return recipient instanceof Group; }
|
||||
public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
|
||||
}
|
||||
|
@ -75,8 +75,12 @@ public class Settings {
|
||||
|
||||
private void supplementDefaults() {
|
||||
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
|
||||
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
|
||||
items.putIfAbsent("hideOnClose", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
|
||||
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
|
||||
items.putIfAbsent("downloadLocation",
|
||||
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
|
||||
"The location where files will be saved to"));
|
||||
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?"));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,19 +124,50 @@ public class Settings {
|
||||
*/
|
||||
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
|
||||
|
||||
/**
|
||||
* @return whether Envoy will prompt a dialogue before saving an
|
||||
* {@link envoy.data.Attachment}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Boolean isDownloadSavedWithoutAsking() { return (Boolean) items.get("autoSaveDownloads").get(); }
|
||||
|
||||
/**
|
||||
* Sets whether Envoy will prompt a dialogue before saving an
|
||||
* {@link envoy.data.Attachment}.
|
||||
*
|
||||
* @param autosaveDownload whether a download should be saved without asking
|
||||
* before
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) { ((SettingsItem<Boolean>) items.get("autoSaveDownloads")).set(autosaveDownload); }
|
||||
|
||||
/**
|
||||
* @return the path where downloads should be saved
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public File getDownloadLocation() { return (File) items.get("downloadLocation").get(); }
|
||||
|
||||
/**
|
||||
* Sets the path where downloads should be saved.
|
||||
*
|
||||
* @param downloadLocation the path to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setDownloadLocation(File downloadLocation) { ((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation); }
|
||||
|
||||
/**
|
||||
* @return the current on close mode.
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
|
||||
public Boolean isHideOnClose() { return (Boolean) items.get("hideOnClose").get(); }
|
||||
|
||||
/**
|
||||
* Sets the current on close mode.
|
||||
*
|
||||
* @param currentOnCloseMode the on close mode that should be set.
|
||||
* @param hideOnClose whether the application should be minimized on close
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
|
||||
public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); }
|
||||
|
||||
/**
|
||||
* @return the items
|
||||
|
@ -27,6 +27,11 @@ public final class AudioRecorder {
|
||||
*/
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||
|
||||
/**
|
||||
* The format in which audio files will be saved.
|
||||
*/
|
||||
public static final String FILE_FORMAT = "wav";
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
@ -78,7 +83,7 @@ public final class AudioRecorder {
|
||||
line.start();
|
||||
|
||||
// Prepare temp file
|
||||
tempFile = Files.createTempFile("recording", "wav");
|
||||
tempFile = Files.createTempFile("recording", FILE_FORMAT);
|
||||
|
||||
// Start the recording
|
||||
final var ais = new AudioInputStream(line);
|
||||
@ -117,6 +122,6 @@ public final class AudioRecorder {
|
||||
line.close();
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {}
|
||||
} catch (final IOException e) {}
|
||||
}
|
||||
}
|
||||
|
34
client/src/main/java/envoy/client/data/commands/OnCall.java
Normal file
34
client/src/main/java/envoy/client/data/commands/OnCall.java
Normal file
@ -0,0 +1,34 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* This interface defines an action that should be performed when a system
|
||||
* command gets called.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>OnCall.java</strong><br>
|
||||
* Created: <strong>23.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public interface OnCall {
|
||||
|
||||
/**
|
||||
* Performs class specific actions when a {@link SystemCommand} has been called.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
void onCall();
|
||||
|
||||
/**
|
||||
* Performs actions that can only be performed by classes that are not
|
||||
* {@link SystemCommand}s when a SystemCommand has been called.
|
||||
*
|
||||
* @param consumer the action to perform when this {@link SystemCommand} has
|
||||
* been called
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
void onCall(Supplier<Void> consumer);
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* This class is the base class of all {@code SystemCommands} and contains an
|
||||
* action and a number of arguments that should be used as input for this
|
||||
* function.
|
||||
* No {@code SystemCommand} can return anything.
|
||||
* Every {@code SystemCommand} must have as argument type {@code List<String>} so
|
||||
* that the words following the indicator String can be used as input of the
|
||||
* function. This approach has one limitation:<br>
|
||||
* <b>Order matters!</b> Changing the order of arguments will likely result in
|
||||
* unexpected behavior.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SystemCommand.java</strong><br>
|
||||
* Created: <strong>16.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class SystemCommand implements OnCall {
|
||||
|
||||
protected int relevance;
|
||||
|
||||
/**
|
||||
* The argument count of the command.
|
||||
*/
|
||||
protected final int numberOfArguments;
|
||||
|
||||
/**
|
||||
* This function takes a {@code List<String>} as argument because automatically
|
||||
* {@code SystemCommand#numberOfArguments} words following the necessary command
|
||||
* will be put into this list.
|
||||
*
|
||||
* @see String#split(String)
|
||||
*/
|
||||
protected final Consumer<List<String>> action;
|
||||
|
||||
protected final String description;
|
||||
|
||||
protected final List<String> defaults;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code SystemCommand}.
|
||||
*
|
||||
* @param action the action performed by the command
|
||||
* @param numberOfArguments the argument count accepted by the action
|
||||
* @param defaults the default values for the corresponding arguments
|
||||
* @param description the description of this {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand(Consumer<List<String>> action, int numberOfArguments, List<String> defaults, String description) {
|
||||
this.numberOfArguments = numberOfArguments;
|
||||
this.action = action;
|
||||
this.defaults = defaults == null ? new ArrayList<>() : defaults;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the action that should be performed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Consumer<List<String>> getAction() { return action; }
|
||||
|
||||
/**
|
||||
* @return the argument count of the command
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public int getNumberOfArguments() { return numberOfArguments; }
|
||||
|
||||
/**
|
||||
* @return the description
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public String getDescription() { return description; }
|
||||
|
||||
/**
|
||||
* @return the relevance
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public int getRelevance() { return relevance; }
|
||||
|
||||
/**
|
||||
* @param relevance the relevance to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setRelevance(int relevance) { this.relevance = relevance; }
|
||||
|
||||
/**
|
||||
* Increments the relevance of this {@code SystemCommand}.
|
||||
*/
|
||||
@Override
|
||||
public void onCall() { relevance++; }
|
||||
|
||||
/**
|
||||
* Increments the relevance of this {@code SystemCommand} and executes the
|
||||
* supplier.
|
||||
*/
|
||||
@Override
|
||||
public void onCall(Supplier<Void> consumer) {
|
||||
onCall();
|
||||
consumer.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the defaults
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public List<String> getDefaults() { return defaults; }
|
||||
|
||||
@Override
|
||||
public int hashCode() { return Objects.hash(action); }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
final SystemCommand other = (SystemCommand) obj;
|
||||
return Objects.equals(action, other.action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
|
||||
+ (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "")
|
||||
+ (defaults != null ? "defaults=" + defaults : "") + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This class acts as a builder for {@link SystemCommand}s.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SystemCommandBuilder.java</strong><br>
|
||||
* Created: <strong>23.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class SystemCommandBuilder {
|
||||
|
||||
private int numberOfArguments;
|
||||
private Consumer<List<String>> action;
|
||||
private List<String> defaults;
|
||||
private String description;
|
||||
private int relevance;
|
||||
|
||||
/**
|
||||
* @param numberOfArguments the numberOfArguments to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setNumberOfArguments(int numberOfArguments) {
|
||||
this.numberOfArguments = numberOfArguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param action the action to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setAction(Consumer<List<String>> action) {
|
||||
this.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param description the description to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param relevance the relevance to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setRelevance(int relevance) {
|
||||
this.relevance = relevance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param defaults the defaults to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setDefaults(String... defaults) {
|
||||
this.defaults = List.of(defaults);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all values stored.
|
||||
*
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder reset() {
|
||||
numberOfArguments = 0;
|
||||
action = null;
|
||||
defaults = new ArrayList<>();
|
||||
description = "";
|
||||
relevance = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build() { return build(true); }
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
|
||||
* previous value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildNoArg() {
|
||||
numberOfArguments = 0;
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
|
||||
* string as argument, regardless of the previous value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildRemainingArg() {
|
||||
numberOfArguments = -1;
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
|
||||
* not be.
|
||||
*
|
||||
* @param reset whether this {@code SystemCommandBuilder} should be reset
|
||||
* afterwards.<br>
|
||||
* This can be useful if another command wants to execute something
|
||||
* similar
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build(boolean reset) {
|
||||
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
|
||||
sc.setRelevance(relevance);
|
||||
if (reset) reset();
|
||||
return sc;
|
||||
}
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* This class stores all {@link SystemCommand}s used.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SystemCommandsMap.java</strong><br>
|
||||
* Created: <strong>17.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class SystemCommandsMap {
|
||||
|
||||
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
|
||||
|
||||
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(SystemCommandsMap.class);
|
||||
|
||||
/**
|
||||
* Adds a new command to the map if the command name is valid.
|
||||
*
|
||||
* @param command the input string to execute the
|
||||
* given action
|
||||
* @param systemCommand the command to add - can be built using
|
||||
* {@link SystemCommandBuilder}
|
||||
* @see SystemCommandsMap#isValidKey(String)
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void add(String command, SystemCommand systemCommand) { if (isValidKey(command)) systemCommands.put(command, systemCommand); }
|
||||
|
||||
/**
|
||||
* This method checks if the input String is a key in the map and returns the
|
||||
* wrapped System command if present.
|
||||
* It will return an empty optional if the value after the slash is not a key in
|
||||
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
|
||||
* map).
|
||||
* <p>
|
||||
* Usage example:<br>
|
||||
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br>
|
||||
* {@code Button button = new Button();}
|
||||
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
|
||||
* {@code ....}<br>
|
||||
* user input: {@code "/example xyz ..."}<br>
|
||||
* {@code systemCommands.get("example xyz ...")} or
|
||||
* {@code systemCommands.get("/example xyz ...")}
|
||||
* result: {@code Optional<SystemCommand>}
|
||||
*
|
||||
* @param input the input string given by the user
|
||||
* @return the wrapped system command, if present
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input))); }
|
||||
|
||||
/**
|
||||
* This method ensures that the "/" of a {@link SystemCommand} is stripped.<br>
|
||||
* It returns the command as (most likely) entered as key in the map for the
|
||||
* first word of the text.<br>
|
||||
* It should only be called on strings that contain a "/" at position 0/-1.
|
||||
*
|
||||
* @param raw the input
|
||||
* @return the command as entered in the map
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote this method will (most likely) not return anything useful if
|
||||
* whatever is entered after the slash is not a system command. Only
|
||||
* exception: for recommendation purposes.
|
||||
*/
|
||||
public String getCommand(String raw) {
|
||||
final var trimmed = raw.stripLeading();
|
||||
final var index = trimmed.indexOf(' ');
|
||||
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines whether a key can be put in the map and logs it with
|
||||
* {@code Level.WARNING} if that key violates API constrictions.<br>
|
||||
* (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
|
||||
* <p>
|
||||
* The approach to not throw an exception was taken so that an ugly try-catch
|
||||
* block for every addition to the system commands map could be avoided, an
|
||||
* error that should only occur during implementation and not in production.
|
||||
*
|
||||
* @param command the key to examine
|
||||
* @return whether this key can be used in the map
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isValidKey(String command) {
|
||||
final boolean valid = commandPattern.matcher(command).matches();
|
||||
if (!valid) logger.log(Level.WARNING,
|
||||
"The command \"" + command
|
||||
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
|
||||
+ commandPattern + "are allowed");
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a 'raw' string (the whole input) and checks if "/" is the first visible
|
||||
* character and then checks if a command is present after that "/". If that is
|
||||
* the case, it will be executed.
|
||||
* <p>
|
||||
*
|
||||
* @param raw the raw input string
|
||||
* @return whether a command could be found
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean executeIfAnyPresent(String raw) {
|
||||
// possibly a command was detected and could be executed
|
||||
final var raw2 = raw.stripLeading();
|
||||
final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false;
|
||||
// the command was executed successfully - no further checking needed
|
||||
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
|
||||
return commandFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if the input String is a key in the map and executes the
|
||||
* wrapped System command if present.
|
||||
* Its intended usage is after a "/" has been detected in the input String.
|
||||
* It will do nothing if the value after the slash is not a key in
|
||||
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
|
||||
* map).
|
||||
* <p>
|
||||
* Usage example:<br>
|
||||
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br>
|
||||
* {@code Button button = new Button();}<br>
|
||||
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
|
||||
* {@code ....}<br>
|
||||
* user input: {@code "/example xyz ..."}<br>
|
||||
* {@code systemCommands.executeIfPresent("example xyz ...")}
|
||||
* result: {@code button.getText()=="xyz"}
|
||||
*
|
||||
* @param input the input string given by the user
|
||||
* @return whether a command could be found
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean executeIfPresent(String input) {
|
||||
final var command = getCommand(input);
|
||||
final var value = get(command);
|
||||
value.ifPresent(systemCommand -> {
|
||||
// Splitting the String so that the leading command including the first " " is
|
||||
// removed and only as many following words as allowed by the system command
|
||||
// persist
|
||||
final var arguments = extractArguments(input, systemCommand);
|
||||
// Executing the function
|
||||
try {
|
||||
systemCommand.getAction().accept(arguments);
|
||||
systemCommand.onCall();
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.WARNING, "The system command " + command + " threw an exception: ", e);
|
||||
}
|
||||
});
|
||||
return value.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies missing values with default values.
|
||||
*
|
||||
* @param input the input String
|
||||
* @param systemCommand the command that is expected
|
||||
* @return the list of arguments that can be used to parse the systemCommand
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private List<String> extractArguments(String input, SystemCommand systemCommand) {
|
||||
// no more arguments follow after the command (e.g. text = "/DABR")
|
||||
final var indexOfSpace = input.indexOf(" ");
|
||||
if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand);
|
||||
// the arguments behind a system command
|
||||
final var remainingString = input.substring(indexOfSpace + 1);
|
||||
final var numberOfArguments = systemCommand.getNumberOfArguments();
|
||||
// splitting those arguments and supplying default values
|
||||
final var textArguments = remainingString.split(" ", -1);
|
||||
final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments;
|
||||
final var arguments = supplementDefaults(originalArguments, systemCommand);
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recommendations based on the current input entered.<br>
|
||||
* The first word is used for the recommendations and
|
||||
* it does not matter if the "/" is at its beginning or not.<br>
|
||||
* If none are present, nothing will be done.<br>
|
||||
* Otherwise the given function will be executed on the recommendations.<br>
|
||||
*
|
||||
* @param input the input string
|
||||
* @param action the action that should be taken for the recommendations, if any
|
||||
* are present
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void requestRecommendations(String input, Function<Set<String>, Void> action) {
|
||||
final var partialCommand = getCommand(input);
|
||||
// Get the expected commands
|
||||
final var recommendations = recommendCommands(partialCommand);
|
||||
if (recommendations.isEmpty()) return;
|
||||
// Execute the given action
|
||||
else action.apply(recommendations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommends commands based upon the currently entered input.<br>
|
||||
* In the current implementation, all we check is whether a key contains this
|
||||
* input. This might be updated later on.
|
||||
*
|
||||
* @param partialCommand the partially entered command
|
||||
* @return a set of all commands that match this input
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private Set<String> recommendCommands(String partialCommand) {
|
||||
// current implementation only looks if input is contained within a command,
|
||||
// might be updated
|
||||
return systemCommands.keySet()
|
||||
.stream()
|
||||
.filter(command -> command.contains(partialCommand))
|
||||
.sorted((command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Supplies the default values for arguments if none are present in the text for
|
||||
* any argument. <br>
|
||||
* Will only work for {@code SystemCommand}s whose argument counter is bigger
|
||||
* than 1.
|
||||
*
|
||||
* @param textArguments the arguments that were parsed from the text
|
||||
* @param toEvaluate the system command whose default values should be used
|
||||
* @return the final argument list
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote this method will insert an empty String if the size of the list
|
||||
* given to the {@code SystemCommand} is smaller than its argument
|
||||
* counter and no more text arguments could be found.
|
||||
*/
|
||||
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
|
||||
final var defaults = toEvaluate.getDefaults();
|
||||
final var numberOfArguments = toEvaluate.getNumberOfArguments();
|
||||
final List<String> result = new ArrayList<>();
|
||||
|
||||
if (toEvaluate.getNumberOfArguments() > 0) for (int index = 0; index < numberOfArguments; index++) {
|
||||
String textArg = null;
|
||||
if (index < textArguments.length) textArg = textArguments[index];
|
||||
// Set the argument at position index to the current argument of the text, if it
|
||||
// is present. Otherwise the default for that argument will be taken if present.
|
||||
// In the worst case, an empty String will be used.
|
||||
result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all {@link SystemCommand}s used with the underlying command as key
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This package contains all classes that can be used as system commands.<br>
|
||||
* Every system command can be called using a specific syntax:"/<command>"
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>16.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
package envoy.client.data.commands;
|
@ -155,6 +155,9 @@ public class Client implements Closeable {
|
||||
// Process group size changes
|
||||
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
|
||||
|
||||
// Process IsTyping events
|
||||
receiver.registerProcessor(IsTyping.class, eventBus::dispatch);
|
||||
|
||||
// Send event
|
||||
eventBus.register(SendEvent.class, evt -> {
|
||||
try {
|
||||
|
@ -1,8 +1,6 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.*;
|
||||
import java.net.SocketException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -82,8 +80,9 @@ public class Receiver extends Thread {
|
||||
logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
|
||||
else processor.accept(obj);
|
||||
}
|
||||
} catch (final SocketException e) {
|
||||
} catch (final SocketException | EOFException e) {
|
||||
// Connection probably closed by client.
|
||||
logger.log(Level.FINER, "Exiting receiver...");
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||
|
@ -1,9 +1,13 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
@ -145,6 +149,23 @@ public class IconUtil {
|
||||
return icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a buffered image from the resource folder which is compatible with AWT.
|
||||
*
|
||||
* @param path the path to the icon inside the resource folder
|
||||
* @return the loaded image
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static BufferedImage loadAWTCompatible(String path) {
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = ImageIO.read(IconUtil.class.getResource(path));
|
||||
} catch (IOException e) {
|
||||
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be called if the display of an image depends upon the
|
||||
* currently active theme.<br>
|
||||
@ -154,7 +175,7 @@ public class IconUtil {
|
||||
* @return the theme specific folder
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static String themeSpecificSubFolder() {
|
||||
private static String themeSpecificSubFolder() {
|
||||
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package envoy.client.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.data.Message;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
@ -35,66 +33,65 @@ public class StatusTrayIcon {
|
||||
*/
|
||||
private boolean displayMessages = false;
|
||||
|
||||
/**
|
||||
* @return {@code true} if the status tray icon is supported on this platform
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static boolean isSupported() { return SystemTray.isSupported(); }
|
||||
|
||||
/**
|
||||
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
|
||||
* menu.
|
||||
*
|
||||
* @param focusTarget the {@link Window} which focus determines if message
|
||||
* notifications are displayed
|
||||
* @throws EnvoyException if the currently used OS does not support the System
|
||||
* Tray API
|
||||
* @since Envoy Client v0.2-alpha
|
||||
* @param stage the stage whose focus determines if message
|
||||
* notifications are displayed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public StatusTrayIcon(Window focusTarget) throws EnvoyException {
|
||||
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
|
||||
|
||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
|
||||
trayIcon = new TrayIcon(img, "Envoy Client");
|
||||
public StatusTrayIcon(Stage stage) {
|
||||
trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy");
|
||||
trayIcon.setImageAutoSize(true);
|
||||
trayIcon.setToolTip("You are notified if you have unread messages.");
|
||||
|
||||
final PopupMenu popup = new PopupMenu();
|
||||
|
||||
final MenuItem exitMenuItem = new MenuItem("Exit");
|
||||
exitMenuItem.addActionListener(evt -> System.exit(0));
|
||||
exitMenuItem.addActionListener(evt -> { Platform.exit(); System.exit(0); });
|
||||
popup.add(exitMenuItem);
|
||||
|
||||
trayIcon.setPopupMenu(popup);
|
||||
|
||||
// Only display messages if the chat window is not focused
|
||||
focusTarget.addWindowFocusListener(new WindowAdapter() {
|
||||
|
||||
@Override
|
||||
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
|
||||
|
||||
@Override
|
||||
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
|
||||
});
|
||||
// Only display messages if the stage is not focused
|
||||
stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
|
||||
|
||||
// Show the window if the user clicks on the icon
|
||||
trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); });
|
||||
trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); }));
|
||||
|
||||
// Start processing message events
|
||||
// TODO: Handle other message types
|
||||
EventBus.getInstance()
|
||||
.register(MessageCreationEvent.class,
|
||||
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
|
||||
EventBus.getInstance().register(MessageCreationEvent.class, evt -> {
|
||||
if (displayMessages) trayIcon
|
||||
.displayMessage(
|
||||
evt.get().hasAttachment() ? "New " + evt.get().getAttachment().getType().toString().toLowerCase() + " message received"
|
||||
: "New message received",
|
||||
evt.get().getText(),
|
||||
MessageType.INFO);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this {@link StatusTrayIcon} appear in the system tray.
|
||||
* Makes the icon appear in the system tray.
|
||||
*
|
||||
* @throws EnvoyException if the status icon could not be attaches to the system
|
||||
* tray for system-internal reasons
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void show() throws EnvoyException {
|
||||
public void show() {
|
||||
try {
|
||||
SystemTray.getSystemTray().add(trayIcon);
|
||||
} catch (final AWTException e) {
|
||||
EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e);
|
||||
throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
|
||||
}
|
||||
} catch (AWTException e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the icon from the system tray.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
@ -33,7 +35,10 @@ import javafx.util.Duration;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.data.audio.AudioRecorder;
|
||||
import envoy.client.data.commands.SystemCommandBuilder;
|
||||
import envoy.client.data.commands.SystemCommandsMap;
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.client.ui.*;
|
||||
@ -126,6 +131,8 @@ public final class ChatScene implements Restorable {
|
||||
private Attachment pendingAttachment;
|
||||
private boolean postingPermanentlyDisabled;
|
||||
|
||||
private final SystemCommandsMap messageTextAreaCommands = new SystemCommandsMap();
|
||||
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
@ -221,7 +228,7 @@ public final class ChatScene implements Restorable {
|
||||
switch (e.getOperationType()) {
|
||||
case ADD:
|
||||
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
|
||||
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
||||
final Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
||||
Platform.runLater(() -> chatList.getItems().add(chat));
|
||||
break;
|
||||
case REMOVE:
|
||||
@ -231,6 +238,22 @@ public final class ChatScene implements Restorable {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all {@code SystemCommands} used in {@code ChatScene}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private void initializeSystemCommandsMap() {
|
||||
final var builder = new SystemCommandBuilder();
|
||||
// Do A Barrel roll initialization
|
||||
final var random = new Random();
|
||||
builder.setAction(text -> doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1))))
|
||||
.setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1))
|
||||
.setDescription("See for yourself :)")
|
||||
.setNumberOfArguments(2);
|
||||
messageTextAreaCommands.add("DABR", builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all necessary data via dependency injection-
|
||||
*
|
||||
@ -251,9 +274,12 @@ public final class ChatScene implements Restorable {
|
||||
chatList.setItems(chats);
|
||||
contactLabel.setText(localDB.getUser().getName());
|
||||
MessageControl.setLocalDB(localDB);
|
||||
MessageControl.setSceneContext(sceneContext);
|
||||
|
||||
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
|
||||
|
||||
recorder = new AudioRecorder();
|
||||
initializeSystemCommandsMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -362,7 +388,9 @@ public final class ChatScene implements Restorable {
|
||||
});
|
||||
recorder.start();
|
||||
} else {
|
||||
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
|
||||
pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
|
||||
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()) + "." + AudioRecorder.FILE_FORMAT,
|
||||
AttachmentType.VOICE);
|
||||
recording = false;
|
||||
Platform.runLater(() -> {
|
||||
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
|
||||
@ -413,7 +441,7 @@ public final class ChatScene implements Restorable {
|
||||
// Create the pending attachment
|
||||
try {
|
||||
final var fileBytes = Files.readAllBytes(file.toPath());
|
||||
pendingAttachment = new Attachment(fileBytes, type);
|
||||
pendingAttachment = new Attachment(fileBytes, file.getName(), type);
|
||||
checkPostConditions(false);
|
||||
// Setting the preview image as image of the attachmentView
|
||||
if (type == AttachmentType.PICTURE)
|
||||
@ -426,20 +454,34 @@ public final class ChatScene implements Restorable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates every element in our application by 360° in at most 2.75s.
|
||||
* Rotates every element in our application by (at most 4 *) 360° in at most
|
||||
* 2.75s.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void doABarrelRoll() {
|
||||
final var random = new Random();
|
||||
doABarrelRoll(random.nextInt(3) + 1, random.nextDouble() * 3 + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates every element in our application by {@code rotations}*360° in
|
||||
* {@code an}.
|
||||
*
|
||||
* @param rotations the amount of times the scene is rotated by 360°
|
||||
* @param animationTime the time in seconds that this animation lasts
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
private void doABarrelRoll(int rotations, double animationTime) {
|
||||
// contains all Node objects in ChatScene in alphabetical order
|
||||
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
|
||||
postButton, remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton };
|
||||
final var random = new Random();
|
||||
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea, postButton,
|
||||
remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton };
|
||||
for (final var node : rotatableNodes) {
|
||||
// Defines at most four whole rotation in at most 4s
|
||||
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
|
||||
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
|
||||
// Sets the animation duration to {animationTime}
|
||||
final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node);
|
||||
// rotates every element {rotations} times
|
||||
rotateTransition.setByAngle(rotations * 360);
|
||||
rotateTransition.play();
|
||||
// This is needed as for some strange reason objects could stop before being
|
||||
// rotated back to 0°
|
||||
@ -459,10 +501,28 @@ public final class ChatScene implements Restorable {
|
||||
private void checkKeyCombination(KeyEvent e) {
|
||||
// Checks whether the text is too long
|
||||
messageTextUpdated();
|
||||
// Sending an IsTyping event if none has been sent for
|
||||
// IsTyping#millisecondsActive
|
||||
if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
|
||||
eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID())));
|
||||
currentChat.lastWritingEventWasNow();
|
||||
}
|
||||
// Automatic sending of messages via (ctrl +) enter
|
||||
checkPostConditions(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id that should be used to send things to the server:
|
||||
* the id of 'our' {@link User} if the recipient of that object is another User,
|
||||
* else the id of the {@link Group} 'our' user is sending to.
|
||||
*
|
||||
* @return an id that can be sent to the server
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private long getChatID() {
|
||||
return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param e the keys that have been pressed
|
||||
* @since Envoy Client v0.1-beta
|
||||
@ -531,7 +591,7 @@ public final class ChatScene implements Restorable {
|
||||
return;
|
||||
}
|
||||
final var text = messageTextArea.getText().strip();
|
||||
try {
|
||||
if (!messageTextAreaCommands.executeIfAnyPresent(text)) try {
|
||||
// Creating the message and its metadata
|
||||
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
||||
.setText(text);
|
||||
@ -619,10 +679,15 @@ public final class ChatScene implements Restorable {
|
||||
private void copyAndPostMessage() {
|
||||
final var messageText = messageTextArea.getText();
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
|
||||
final var image = attachmentView.getImage();
|
||||
final var messageAttachment = pendingAttachment;
|
||||
postMessage();
|
||||
messageTextArea.setText(messageText);
|
||||
updateRemainingCharsLabel();
|
||||
postButton.setDisable(messageText.isBlank());
|
||||
attachmentView.setImage(image);
|
||||
if (attachmentView.getImage() != null) attachmentView.setVisible(true);
|
||||
pendingAttachment = messageAttachment;
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
@ -1,5 +1,6 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@ -53,8 +54,20 @@ public class ContactSearchScene {
|
||||
|
||||
private LocalDB localDB;
|
||||
|
||||
private static EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
private Alert alert = new Alert(AlertType.CONFIRMATION);
|
||||
|
||||
private User currentlySelectedUser;
|
||||
|
||||
private final Consumer<ContactOperation> handler = e -> {
|
||||
final var contact = e.get();
|
||||
if (e.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> {
|
||||
userList.getItems().remove(contact);
|
||||
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close();
|
||||
});
|
||||
};
|
||||
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
@ -72,6 +85,7 @@ public class ContactSearchScene {
|
||||
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); userList.getItems().clear(); });
|
||||
eventBus.register(UserSearchResult.class,
|
||||
response -> Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(response.get()); }));
|
||||
eventBus.register(ContactOperation.class, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,20 +122,21 @@ public class ContactSearchScene {
|
||||
private void userListClicked() {
|
||||
final var user = userList.getSelectionModel().getSelectedItem();
|
||||
if (user != null) {
|
||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle("Add Contact to Contact List");
|
||||
alert.setHeaderText("Add the user " + user.getName() + " to your contact list?");
|
||||
currentlySelectedUser = user;
|
||||
alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle("Add User to Contact List");
|
||||
alert.setHeaderText("Add the user " + currentlySelectedUser.getName() + " to your contact list?");
|
||||
// Normally, this would be total BS (we are already on the FX Thread), however
|
||||
// it could be proven that the creation of this dialog wrapped in
|
||||
// Platform.runLater is less error-prone than without it
|
||||
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
|
||||
final var event = new ContactOperation(user, ElementOperation.ADD);
|
||||
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
|
||||
// Sends the event to the server
|
||||
eventBus.dispatch(new SendEvent(event));
|
||||
// Removes the chosen user and updates the UI
|
||||
userList.getItems().remove(user);
|
||||
userList.getItems().remove(currentlySelectedUser);
|
||||
eventBus.dispatch(event);
|
||||
logger.log(Level.INFO, "Added user " + user);
|
||||
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,7 @@ import javafx.scene.image.ImageView;
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.client.ui.*;
|
||||
import envoy.data.LoginCredentials;
|
||||
import envoy.data.User;
|
||||
import envoy.data.User.UserStatus;
|
||||
@ -77,6 +75,7 @@ public final class LoginScene {
|
||||
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final ClientConfig config = ClientConfig.getInstance();
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
@ -239,5 +238,23 @@ public final class LoginScene {
|
||||
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
|
||||
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
|
||||
sceneContext.getStage().centerOnScreen();
|
||||
|
||||
if (StatusTrayIcon.isSupported()) {
|
||||
|
||||
// Configure hide on close
|
||||
sceneContext.getStage().setOnCloseRequest(e -> {
|
||||
if (settings.isHideOnClose()) {
|
||||
sceneContext.getStage().setIconified(true);
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize status tray icon
|
||||
final var trayIcon = new StatusTrayIcon(sceneContext.getStage());
|
||||
settings.getItems().get("hideOnClose").setChangeHandler(c -> {
|
||||
if ((Boolean) c) trayIcon.show();
|
||||
else trayIcon.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.settings.DownloadSettingsPane;
|
||||
import envoy.client.ui.settings.GeneralSettingsPane;
|
||||
import envoy.client.ui.settings.SettingsPane;
|
||||
|
||||
@ -29,7 +30,10 @@ public class SettingsScene {
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
|
||||
public void initializeData(SceneContext sceneContext) {
|
||||
this.sceneContext = sceneContext;
|
||||
settingsList.getItems().add(new DownloadSettingsPane(sceneContext));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
|
@ -53,12 +53,12 @@ public class ChatControl extends HBox {
|
||||
getChildren().add(new ContactControl(chat.getRecipient()));
|
||||
// Unread messages
|
||||
if (chat.getUnreadAmount() != 0) {
|
||||
Region spacing = new Region();
|
||||
final var spacing = new Region();
|
||||
setHgrow(spacing, Priority.ALWAYS);
|
||||
getChildren().add(spacing);
|
||||
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
|
||||
unreadMessagesLabel.setMinSize(15, 15);
|
||||
var vBox2 = new VBox();
|
||||
final var vBox2 = new VBox();
|
||||
vBox2.setAlignment(Pos.CENTER_RIGHT);
|
||||
unreadMessagesLabel.setAlignment(Pos.CENTER);
|
||||
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
|
||||
|
@ -2,7 +2,7 @@ package envoy.client.ui.listcell;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.*;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
@ -17,10 +17,14 @@ import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.ui.AudioControl;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.client.ui.SceneContext;
|
||||
|
||||
import envoy.data.GroupMessage;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
@ -44,9 +48,12 @@ public class MessageControl extends Label {
|
||||
|
||||
private static LocalDB localDB;
|
||||
|
||||
private static SceneContext sceneContext;
|
||||
|
||||
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
|
||||
|
||||
/**
|
||||
@ -149,7 +156,26 @@ public class MessageControl extends Label {
|
||||
|
||||
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
|
||||
|
||||
private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
|
||||
private void saveAttachment(Message message) {
|
||||
File file;
|
||||
final var fileName = message.getAttachment().getName();
|
||||
final var downloadLocation = settings.getDownloadLocation();
|
||||
// Show save file dialog, if the user did not opt-out
|
||||
if (!settings.isDownloadSavedWithoutAsking()) {
|
||||
final var fileChooser = new FileChooser();
|
||||
fileChooser.setInitialFileName(fileName);
|
||||
fileChooser.setInitialDirectory(downloadLocation);
|
||||
file = fileChooser.showSaveDialog(sceneContext.getStage());
|
||||
} else file = new File(downloadLocation, fileName);
|
||||
|
||||
// A file was selected
|
||||
if (file != null) try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||
fos.write(message.getAttachment().getData());
|
||||
logger.log(Level.FINE, "Attachment of message was saved at " + file.getAbsolutePath());
|
||||
} catch (final IOException e) {
|
||||
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param localDB the localDB
|
||||
@ -163,4 +189,10 @@ public class MessageControl extends Label {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isOwnMessage() { return ownMessage; }
|
||||
|
||||
/**
|
||||
* @param sceneContext the scene context storing the stage used in Envoy
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static void setSceneContext(SceneContext sceneContext) { MessageControl.sceneContext = sceneContext; }
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
package envoy.client.ui.settings;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import envoy.client.ui.SceneContext;
|
||||
|
||||
/**
|
||||
* Displays options for downloading {@link envoy.data.Attachment}s.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>DownloadSettingsPane.java</strong><br>
|
||||
* Created: <strong>27.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class DownloadSettingsPane extends SettingsPane {
|
||||
|
||||
/**
|
||||
* Constructs a new {@code DownloadSettingsPane}.
|
||||
*
|
||||
* @param sceneContext the {@code SceneContext} used to block input to the
|
||||
* {@link javafx.stage.Stage} used in Envoy
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public DownloadSettingsPane(SceneContext sceneContext) {
|
||||
super("Download");
|
||||
final var vbox = new VBox(15);
|
||||
vbox.setPadding(new Insets(15));
|
||||
// checkbox to disable asking
|
||||
final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
|
||||
checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
|
||||
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
|
||||
vbox.getChildren().add(checkBox);
|
||||
|
||||
// Displaying the default path to save to
|
||||
vbox.getChildren().add(new Label(settings.getItems().get("downloadLocation").getDescription() + ":"));
|
||||
final var hbox = new HBox(20);
|
||||
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
|
||||
hbox.getChildren().add(currentPath);
|
||||
|
||||
// Setting the default path
|
||||
final var button = new Button("Select");
|
||||
button.setOnAction(e -> {
|
||||
final var directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Select the directory where attachments should be saved to");
|
||||
directoryChooser.setInitialDirectory(settings.getDownloadLocation());
|
||||
final var selectedDirectory = directoryChooser.showDialog(sceneContext.getStage());
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
currentPath.setText(selectedDirectory.getAbsolutePath());
|
||||
settings.setDownloadLocation(selectedDirectory);
|
||||
}
|
||||
});
|
||||
hbox.getChildren().add(button);
|
||||
vbox.getChildren().add(hbox);
|
||||
getChildren().add(vbox);
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import java.util.List;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.data.SettingsItem;
|
||||
import envoy.client.event.ThemeChangeEvent;
|
||||
import envoy.data.User.UserStatus;
|
||||
@ -21,8 +20,6 @@ import envoy.event.EventBus;
|
||||
*/
|
||||
public class GeneralSettingsPane extends SettingsPane {
|
||||
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
|
||||
/**
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -31,7 +28,7 @@ public class GeneralSettingsPane extends SettingsPane {
|
||||
final var vbox = new VBox();
|
||||
|
||||
// TODO: Support other value types
|
||||
List.of("onCloseMode", "enterToSend")
|
||||
List.of("hideOnClose", "enterToSend")
|
||||
.stream()
|
||||
.map(settings.getItems()::get)
|
||||
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
|
||||
|
@ -2,11 +2,13 @@ package envoy.client.ui.settings;
|
||||
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SettingsPane.java</strong><br>
|
||||
* Created: <strong>18.04.2020</strong><br>
|
||||
*
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -14,6 +16,8 @@ public abstract class SettingsPane extends Pane {
|
||||
|
||||
protected String title;
|
||||
|
||||
protected static final Settings settings = Settings.getInstance();
|
||||
|
||||
protected SettingsPane(String title) { this.title = title; }
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user