Merge branch 'develop' into f/new_ui

This commit is contained in:
DieGurke 2020-08-01 10:49:40 +02:00 committed by GitHub
commit 209262b4c9
51 changed files with 1157 additions and 239 deletions

View File

@ -1,11 +1,7 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: ''
labels: bug labels: bug
assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333
projects: Envoy
milestone: Envoy v0.2-beta
--- ---
**Describe the bug** **Describe the bug**

View File

@ -1,11 +1,7 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' labels: enhancement
labels: enhancement, feature
assignees: CyB3RC0nN0R, delvh, DieGurke
project: Envoy
milestones: Envoy v0.2-beta
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

View File

@ -24,9 +24,12 @@ jobs:
restore-keys: ${{ runner.os }}-m2 restore-keys: ${{ runner.os }}-m2
- name: Build with Maven - name: Build with Maven
run: mvn -B package run: mvn -B package
- name: Stage build artifacts
run: |
mkdir staging
cp server/target/envoy-server-jar-with-dependencies.jar staging/envoy-server.jar
cp client/target/envoy-client*shaded.jar staging/envoy.jar
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: envoy-${{ matrix.os }} name: envoy-${{ matrix.os }}
path: | path: staging
server/target/envoy-server-jar-with-dependencies.jar
client/target/envoy-client*shaded.jar

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# Envoy
---
<a href="https://github.com/informatik-ag-ngl/envoy"><img src="https://raw.githubusercontent.com/informatik-ag-ngl/envoy/develop/client/src/main/resources/icons/envoy_logo.png" align="right" width="150" height="150"></a>
<a href="https://github.com/informatik-ag-ngl/envoy/milestone/1"><img alt="GitHub milestone" src="https://img.shields.io/github/milestones/progress-percent/informatik-ag-ngl/envoy/1"></a>
<a href="https://github.com/informatik-ag-ngl/envoy/pulls"><img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr-raw/informatik-ag-ngl/envoy?style=flat"></a>
<a href="https://github.com/informatik-ag-ngl/envoy/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/informatik-ag-ngl/envoy?style=flat"></a>
<a href="https://github.com/informatik-ag-ngl/envoy/actions"><img src="https://github.com/informatik-ag-ngl/envoy/workflows/Java%20CI/badge.svg"></a>
**Envoy** is a messenger written in Java.<br>
It is split into three separate components: Envoy Client, Envoy Common and Envoy Server.
<br><br><br><br><br>
---
### Envoy Client:
This is the only part users are interested in. It contains everything to make this messenger work: the UI.
### Envoy Server:
Envoy offers the option to download and host your own server over which Envoy can run.<br>
This part will be especially appealing to institutions/organizations who want to self-host Envoy.
### Envoy Common:
This part contains elements that both the client and the server need. It will be automatically part of either one (Thanks, Maven!).
## Features
Envoy features a lot of things and many more are yet to come.
Currently existing features are:
#### 'Client' contains:
* typical Messenger features (sending and receiving of messages, groups, sending images and voice messages)
* typical Messenger feeling (displaying unread messages)
* Appealing user interface (UI)
* Programming
* API to change default configuration
* Advanced logging possibilities
* Tons of Events to interact with
* Detailed Javadoc to improve readability of code
#### 'Common' contains:
* the event system
* the logger
* Envoy-specific Exceptions
* some util classes
* the most basic datatypes
#### 'Server' contains:
* the database implementation of the data classes
* the connectivity classes
* processors to handle incoming events
* Utility classes to check client version compatability and Password validity

File diff suppressed because one or more lines are too long

View File

@ -37,15 +37,6 @@
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
</resource> </resource>
</resources> </resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -7,8 +7,10 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.Contact;
import envoy.data.Message;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
/** /**
@ -31,6 +33,11 @@ public class Chat implements Serializable {
protected int unreadAmount; 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; private static final long serialVersionUID = 1L;
/** /**
@ -41,16 +48,14 @@ public class Chat implements Serializable {
* @param recipient the user who receives the messages * @param recipient the user who receives the messages
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public Chat(Contact recipient) { public Chat(Contact recipient) { this.recipient = recipient; }
this.recipient = recipient;
}
@Override @Override
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); } public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
/** /**
* Generates a hash code based on the recipient. * Generates a hash code based on the recipient.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
@ -58,14 +63,14 @@ public class Chat implements Serializable {
/** /**
* Tests equality to another object based on the recipient. * Tests equality to another object based on the recipient.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj) return true;
if (!(obj instanceof Chat)) return false; if (!(obj instanceof Chat)) return false;
Chat other = (Chat) obj; final Chat other = (Chat) obj;
return Objects.equals(recipient, other.recipient); 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. * Inserts a message at the correct place according to its creation date.
* *
* @param message the message to insert * @param message the message to insert
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -116,13 +121,13 @@ public class Chat implements Serializable {
/** /**
* Increments the amount of unread messages. * Increments the amount of unread messages.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void incrementUnreadAmount() { unreadAmount++; } 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 * @since Envoy Client v0.1-beta
*/ */
public int getUnreadAmount() { return unreadAmount; } public int getUnreadAmount() { return unreadAmount; }
@ -140,14 +145,16 @@ public class Chat implements Serializable {
public Contact getRecipient() { return recipient; } public Contact getRecipient() { return recipient; }
/** /**
* @return whether this {@link Chat} points at a {@link User} * @return the last known time a {@link envoy.event.IsTyping} event has been
* @since Envoy Client v0.1-beta * 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} * Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
* @since Envoy Client v0.1-beta *
* @since Envoy Client v0.2-beta
*/ */
public boolean isGroupChat() { return recipient instanceof Group; } public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
} }

View File

@ -75,8 +75,12 @@ public class Settings {
private void supplementDefaults() { private void supplementDefaults() {
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key.")); 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("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); } 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. * @return the current on close mode.
* @since Envoy Client v0.3-alpha * @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. * 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 * @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 * @return the items

View File

@ -27,6 +27,11 @@ public final class AudioRecorder {
*/ */
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false); 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 AudioFormat format;
private final DataLine.Info info; private final DataLine.Info info;
@ -78,7 +83,7 @@ public final class AudioRecorder {
line.start(); line.start();
// Prepare temp file // Prepare temp file
tempFile = Files.createTempFile("recording", "wav"); tempFile = Files.createTempFile("recording", FILE_FORMAT);
// Start the recording // Start the recording
final var ais = new AudioInputStream(line); final var ais = new AudioInputStream(line);
@ -117,6 +122,6 @@ public final class AudioRecorder {
line.close(); line.close();
try { try {
Files.deleteIfExists(tempFile); Files.deleteIfExists(tempFile);
} catch (IOException e) {} } catch (final IOException e) {}
} }
} }

View 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);
}

View File

@ -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 : "") + "]";
}
}

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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:"/&lt;command&gt;"
* <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;

View File

@ -155,6 +155,9 @@ public class Client implements Closeable {
// Process group size changes // Process group size changes
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); }); receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
// Process IsTyping events
receiver.registerProcessor(IsTyping.class, eventBus::dispatch);
// Send event // Send event
eventBus.register(SendEvent.class, evt -> { eventBus.register(SendEvent.class, evt -> {
try { try {

View File

@ -1,8 +1,6 @@
package envoy.client.net; package envoy.client.net;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.SocketException; import java.net.SocketException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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())); logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
else processor.accept(obj); else processor.accept(obj);
} }
} catch (final SocketException e) { } catch (final SocketException | EOFException e) {
// Connection probably closed by client. // Connection probably closed by client.
logger.log(Level.FINER, "Exiting receiver...");
return; return;
} catch (final Exception e) { } catch (final Exception e) {
logger.log(Level.SEVERE, "Error on receiver thread", e); logger.log(Level.SEVERE, "Error on receiver thread", e);

View File

@ -1,9 +1,13 @@
package envoy.client.ui; package envoy.client.ui;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.logging.Level; import java.util.logging.Level;
import javax.imageio.ImageIO;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import envoy.client.data.Settings; import envoy.client.data.Settings;
@ -145,6 +149,23 @@ public class IconUtil {
return icons; 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 * This method should be called if the display of an image depends upon the
* currently active theme.<br> * currently active theme.<br>
@ -154,7 +175,7 @@ public class IconUtil {
* @return the theme specific folder * @return the theme specific folder
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static String themeSpecificSubFolder() { private static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : ""; return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
} }
} }

View File

@ -2,15 +2,13 @@ package envoy.client.ui;
import java.awt.*; import java.awt.*;
import java.awt.TrayIcon.MessageType; import java.awt.TrayIcon.MessageType;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import javafx.application.Platform;
import java.util.logging.Level; import javafx.stage.Stage;
import envoy.client.event.MessageCreationEvent; import envoy.client.event.MessageCreationEvent;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.EventBus; import envoy.event.EventBus;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/** /**
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>
@ -35,66 +33,65 @@ public class StatusTrayIcon {
*/ */
private boolean displayMessages = false; 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 * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
* menu. * menu.
* *
* @param focusTarget the {@link Window} which focus determines if message * @param stage the stage whose focus determines if message
* notifications are displayed * notifications are displayed
* @throws EnvoyException if the currently used OS does not support the System * @since Envoy Client v0.2-beta
* Tray API
* @since Envoy Client v0.2-alpha
*/ */
public StatusTrayIcon(Window focusTarget) throws EnvoyException { public StatusTrayIcon(Stage stage) {
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy");
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
trayIcon = new TrayIcon(img, "Envoy Client");
trayIcon.setImageAutoSize(true); trayIcon.setImageAutoSize(true);
trayIcon.setToolTip("You are notified if you have unread messages."); trayIcon.setToolTip("You are notified if you have unread messages.");
final PopupMenu popup = new PopupMenu(); final PopupMenu popup = new PopupMenu();
final MenuItem exitMenuItem = new MenuItem("Exit"); final MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> System.exit(0)); exitMenuItem.addActionListener(evt -> { Platform.exit(); System.exit(0); });
popup.add(exitMenuItem); popup.add(exitMenuItem);
trayIcon.setPopupMenu(popup); trayIcon.setPopupMenu(popup);
// Only display messages if the chat window is not focused // Only display messages if the stage is not focused
focusTarget.addWindowFocusListener(new WindowAdapter() { stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
@Override
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
@Override
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
});
// Show the window if the user clicks on the icon // 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 // Start processing message events
// TODO: Handle other message types EventBus.getInstance().register(MessageCreationEvent.class, evt -> {
EventBus.getInstance() if (displayMessages) trayIcon
.register(MessageCreationEvent.class, .displayMessage(
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); }); 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 * @since Envoy Client v0.2-alpha
*/ */
public void show() throws EnvoyException { public void show() {
try { try {
SystemTray.getSystemTray().add(trayIcon); SystemTray.getSystemTray().add(trayIcon);
} catch (final AWTException e) { } catch (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);
}
} }
/**
* Removes the icon from the system tray.
*
* @since Envoy Client v0.2-beta
*/
public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
} }

View File

@ -8,6 +8,8 @@ import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random; import java.util.Random;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -33,7 +35,10 @@ import javafx.util.Duration;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; 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.MessageCreationEvent;
import envoy.client.event.SendEvent;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.client.ui.*; import envoy.client.ui.*;
@ -126,6 +131,8 @@ public final class ChatScene implements Restorable {
private Attachment pendingAttachment; private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled; private boolean postingPermanentlyDisabled;
private final SystemCommandsMap messageTextAreaCommands = new SystemCommandsMap();
private static final Settings settings = Settings.getInstance(); private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class); private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
@ -221,7 +228,7 @@ public final class ChatScene implements Restorable {
switch (e.getOperationType()) { switch (e.getOperationType()) {
case ADD: case ADD:
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact); 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)); Platform.runLater(() -> chatList.getItems().add(chat));
break; break;
case REMOVE: 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- * Initializes all necessary data via dependency injection-
* *
@ -251,9 +274,12 @@ public final class ChatScene implements Restorable {
chatList.setItems(chats); chatList.setItems(chats);
contactLabel.setText(localDB.getUser().getName()); contactLabel.setText(localDB.getUser().getName());
MessageControl.setLocalDB(localDB); MessageControl.setLocalDB(localDB);
MessageControl.setSceneContext(sceneContext);
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
recorder = new AudioRecorder(); recorder = new AudioRecorder();
initializeSystemCommandsMap();
} }
@Override @Override
@ -362,7 +388,9 @@ public final class ChatScene implements Restorable {
}); });
recorder.start(); recorder.start();
} else { } 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; recording = false;
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
@ -413,7 +441,7 @@ public final class ChatScene implements Restorable {
// Create the pending attachment // Create the pending attachment
try { try {
final var fileBytes = Files.readAllBytes(file.toPath()); final var fileBytes = Files.readAllBytes(file.toPath());
pendingAttachment = new Attachment(fileBytes, type); pendingAttachment = new Attachment(fileBytes, file.getName(), type);
checkPostConditions(false); checkPostConditions(false);
// Setting the preview image as image of the attachmentView // Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE) 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 * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void doABarrelRoll() { 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 // contains all Node objects in ChatScene in alphabetical order
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea, final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea, postButton,
postButton, remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton }; remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton };
final var random = new Random();
for (final var node : rotatableNodes) { for (final var node : rotatableNodes) {
// Defines at most four whole rotation in at most 4s // Sets the animation duration to {animationTime}
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node); final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node);
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360); // rotates every element {rotations} times
rotateTransition.setByAngle(rotations * 360);
rotateTransition.play(); rotateTransition.play();
// This is needed as for some strange reason objects could stop before being // This is needed as for some strange reason objects could stop before being
// rotated back to 0° // rotated back to 0°
@ -459,10 +501,28 @@ public final class ChatScene implements Restorable {
private void checkKeyCombination(KeyEvent e) { private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long // Checks whether the text is too long
messageTextUpdated(); 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 // Automatic sending of messages via (ctrl +) enter
checkPostConditions(e); 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 * @param e the keys that have been pressed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -531,7 +591,7 @@ public final class ChatScene implements Restorable {
return; return;
} }
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
try { if (!messageTextAreaCommands.executeIfAnyPresent(text)) try {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text); .setText(text);
@ -619,10 +679,15 @@ public final class ChatScene implements Restorable {
private void copyAndPostMessage() { private void copyAndPostMessage() {
final var messageText = messageTextArea.getText(); final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
final var image = attachmentView.getImage();
final var messageAttachment = pendingAttachment;
postMessage(); postMessage();
messageTextArea.setText(messageText); messageTextArea.setText(messageText);
updateRemainingCharsLabel(); updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank()); postButton.setDisable(messageText.isBlank());
attachmentView.setImage(image);
if (attachmentView.getImage() != null) attachmentView.setVisible(true);
pendingAttachment = messageAttachment;
} }
@FXML @FXML

View File

@ -1,5 +1,6 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -53,8 +54,20 @@ public class ContactSearchScene {
private LocalDB localDB; private LocalDB localDB;
private static EventBus eventBus = EventBus.getInstance(); private Alert alert = new Alert(AlertType.CONFIRMATION);
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
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 * @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(); }); searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); userList.getItems().clear(); });
eventBus.register(UserSearchResult.class, eventBus.register(UserSearchResult.class,
response -> Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(response.get()); })); 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() { private void userListClicked() {
final var user = userList.getSelectionModel().getSelectedItem(); final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) { if (user != null) {
final var alert = new Alert(AlertType.CONFIRMATION); currentlySelectedUser = user;
alert.setTitle("Add Contact to Contact List"); alert = new Alert(AlertType.CONFIRMATION);
alert.setHeaderText("Add the user " + user.getName() + " to your contact list?"); 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 // 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 // it could be proven that the creation of this dialog wrapped in
// Platform.runLater is less error-prone than without it // Platform.runLater is less error-prone than without it
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> { 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 // Sends the event to the server
eventBus.dispatch(new SendEvent(event)); eventBus.dispatch(new SendEvent(event));
// Removes the chosen user and updates the UI // Removes the chosen user and updates the UI
userList.getItems().remove(user); userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event); eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + user); logger.log(Level.INFO, "Added user " + currentlySelectedUser);
})); }));
} }
} }

View File

@ -17,9 +17,7 @@ import javafx.scene.image.ImageView;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.client.ui.IconUtil; import envoy.client.ui.*;
import envoy.client.ui.SceneContext;
import envoy.client.ui.Startup;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.data.User; import envoy.data.User;
import envoy.data.User.UserStatus; 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 Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final ClientConfig config = ClientConfig.getInstance(); private static final ClientConfig config = ClientConfig.getInstance();
private static final Settings settings = Settings.getInstance();
@FXML @FXML
private void initialize() { private void initialize() {
@ -239,5 +238,23 @@ public final class LoginScene {
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE); sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy); sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
sceneContext.getStage().centerOnScreen(); 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();
});
}
} }
} }

View File

@ -4,6 +4,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import envoy.client.ui.SceneContext; import envoy.client.ui.SceneContext;
import envoy.client.ui.settings.DownloadSettingsPane;
import envoy.client.ui.settings.GeneralSettingsPane; import envoy.client.ui.settings.GeneralSettingsPane;
import envoy.client.ui.settings.SettingsPane; import envoy.client.ui.settings.SettingsPane;
@ -29,7 +30,10 @@ public class SettingsScene {
* @param sceneContext enables the user to return to the chat scene * @param sceneContext enables the user to return to the chat scene
* @since Envoy Client v0.1-beta * @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 @FXML
private void initialize() { private void initialize() {

View File

@ -53,12 +53,12 @@ public class ChatControl extends HBox {
getChildren().add(new ContactControl(chat.getRecipient())); getChildren().add(new ContactControl(chat.getRecipient()));
// Unread messages // Unread messages
if (chat.getUnreadAmount() != 0) { if (chat.getUnreadAmount() != 0) {
Region spacing = new Region(); final var spacing = new Region();
setHgrow(spacing, Priority.ALWAYS); setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing); getChildren().add(spacing);
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount())); final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15); unreadMessagesLabel.setMinSize(15, 15);
var vBox2 = new VBox(); final var vBox2 = new VBox();
vBox2.setAlignment(Pos.CENTER_RIGHT); vBox2.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.setAlignment(Pos.CENTER); unreadMessagesLabel.setAlignment(Pos.CENTER);
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount"); unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");

View File

@ -2,7 +2,7 @@ package envoy.client.ui.listcell;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream; import java.io.*;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map; import java.util.Map;
@ -17,10 +17,14 @@ import javafx.scene.control.MenuItem;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import envoy.client.data.LocalDB; import envoy.client.data.LocalDB;
import envoy.client.data.Settings;
import envoy.client.ui.AudioControl; import envoy.client.ui.AudioControl;
import envoy.client.ui.IconUtil; import envoy.client.ui.IconUtil;
import envoy.client.ui.SceneContext;
import envoy.data.GroupMessage; import envoy.data.GroupMessage;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
@ -44,9 +48,12 @@ public class MessageControl extends Label {
private static LocalDB localDB; private static LocalDB localDB;
private static SceneContext sceneContext;
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault()); .withZone(ZoneId.systemDefault());
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16); 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); 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 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 * @param localDB the localDB
@ -163,4 +189,10 @@ public class MessageControl extends Label {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public boolean isOwnMessage() { return ownMessage; } 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; }
} }

View File

@ -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);
}
}

View File

@ -5,7 +5,6 @@ import java.util.List;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import envoy.client.data.Settings;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
@ -21,8 +20,6 @@ import envoy.event.EventBus;
*/ */
public class GeneralSettingsPane extends SettingsPane { public class GeneralSettingsPane extends SettingsPane {
private static final Settings settings = Settings.getInstance();
/** /**
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -31,7 +28,7 @@ public class GeneralSettingsPane extends SettingsPane {
final var vbox = new VBox(); final var vbox = new VBox();
// TODO: Support other value types // TODO: Support other value types
List.of("onCloseMode", "enterToSend") List.of("hideOnClose", "enterToSend")
.stream() .stream()
.map(settings.getItems()::get) .map(settings.getItems()::get)
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i)) .map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))

View File

@ -2,11 +2,13 @@ package envoy.client.ui.settings;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import envoy.client.data.Settings;
/** /**
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsPane.java</strong><br> * File: <strong>SettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br> * Created: <strong>18.04.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -14,6 +16,8 @@ public abstract class SettingsPane extends Pane {
protected String title; protected String title;
protected static final Settings settings = Settings.getInstance();
protected SettingsPane(String title) { this.title = title; } protected SettingsPane(String title) { this.title = title; }
/** /**

File diff suppressed because one or more lines are too long

View File

@ -24,46 +24,5 @@
<!-- Disable resource folder --> <!-- Disable resource folder -->
<resources /> <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>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<doclint>none</doclint>
</configuration>
</plugin>
</plugins>
</build> </build>
</project> </project>

View File

@ -18,29 +18,28 @@ public class Attachment implements Serializable {
/** /**
* Defines the type of the attachment. * Defines the type of the attachment.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public enum AttachmentType { public enum AttachmentType {
/** /**
* This attachment type denotes a picture. * This attachment type denotes a picture.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
PICTURE, PICTURE,
/** /**
* This attachment type denotes a video. * This attachment type denotes a video.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
VIDEO, VIDEO,
/** /**
* This attachment type denotes a voice message. * This attachment type denotes a voice message.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
VOICE, VOICE,
@ -55,18 +54,21 @@ public class Attachment implements Serializable {
private final byte[] data; private final byte[] data;
private final AttachmentType type; private final AttachmentType type;
private final String name;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 2L;
/** /**
* Constructs an attachment. * Constructs an attachment.
* *
* @param data the data of the attachment * @param data the data of the attachment
* @param name the name of the attachment
* @param type the type of the attachment * @param type the type of the attachment
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Attachment(byte[] data, AttachmentType type) { public Attachment(byte[] data, String name, AttachmentType type) {
this.data = data; this.data = data;
this.name = name;
this.type = type; this.type = type;
} }
@ -81,4 +83,10 @@ public class Attachment implements Serializable {
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public AttachmentType getType() { return type; } public AttachmentType getType() { return type; }
/**
* @return the name
* @since Envoy Common v0.2-beta
*/
public String getName() { return name; }
} }

View File

@ -0,0 +1,45 @@
package envoy.event;
/**
* This event should be sent when a user is currently typing something in a
* chat.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>IsTyping.java</strong><br>
* Created: <strong>24.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public class IsTyping extends Event<Long> {
private final long destinationID;
private static final long serialVersionUID = 1L;
/**
* The number of milliseconds that this event will be active.<br>
* Currently set to 3.5 seconds.
*
* @since Envoy Common v0.2-beta
*/
public static final int millisecondsActive = 3500;
/**
* Creates a new {@code IsTyping} event with originator and recipient.
*
* @param sourceID the id of the originator
* @param destinationID the id of the contact the user wrote to
* @since Envoy Common v0.2-beta
*/
public IsTyping(Long sourceID, long destinationID) {
super(sourceID);
this.destinationID = destinationID;
}
/**
* @return the id of the contact in whose chat the user typed something
* @since Envoy Common v0.2-beta
*/
public long getDestinationID() { return destinationID; }
}

12
pom.xml
View File

@ -18,6 +18,18 @@
<maven.compiler.target>11</maven.compiler.target> <maven.compiler.target>11</maven.compiler.target>
</properties> </properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<modules> <modules>
<module>common</module> <module>common</module>
<module>client</module> <module>client</module>

File diff suppressed because one or more lines are too long

View File

@ -49,15 +49,6 @@
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
</resource> </resource>
</resources> </resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -69,7 +69,8 @@ public class Startup {
new UserStatusChangeProcessor(), new UserStatusChangeProcessor(),
new IDGeneratorRequestProcessor(), new IDGeneratorRequestProcessor(),
new UserSearchProcessor(), new UserSearchProcessor(),
new ContactOperationProcessor()))); new ContactOperationProcessor(),
new IsTypingProcessor())));
// Initialize the current message ID // Initialize the current message ID
final PersistenceManager persistenceManager = PersistenceManager.getInstance(); final PersistenceManager persistenceManager = PersistenceManager.getInstance();

View File

@ -47,7 +47,7 @@ public class Message {
/** /**
* Named query retrieving pending messages for a user (parameter {@code :user}) * Named query retrieving pending messages for a user (parameter {@code :user})
* which was last seen after a specific date (parameter {@code :lastSeen}). * which was last seen after a specific date (parameter {@code :lastSeen}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public static final String getPending = "Message.getPending"; public static final String getPending = "Message.getPending";
@ -76,6 +76,7 @@ public class Message {
protected envoy.data.Message.MessageStatus status; protected envoy.data.Message.MessageStatus status;
protected AttachmentType attachmentType; protected AttachmentType attachmentType;
protected byte[] attachment; protected byte[] attachment;
protected String attachmentName;
protected boolean forwarded; protected boolean forwarded;
/** /**
@ -93,7 +94,7 @@ public class Message {
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public Message(envoy.data.Message message) { public Message(envoy.data.Message message) {
PersistenceManager persistenceManager = PersistenceManager.getInstance(); final var persistenceManager = PersistenceManager.getInstance();
id = message.getID(); id = message.getID();
status = message.getStatus(); status = message.getStatus();
text = message.getText(); text = message.getText();
@ -104,8 +105,10 @@ public class Message {
recipient = persistenceManager.getContactByID(message.getRecipientID()); recipient = persistenceManager.getContactByID(message.getRecipientID());
forwarded = message.isForwarded(); forwarded = message.isForwarded();
if (message.hasAttachment()) { if (message.hasAttachment()) {
attachment = message.getAttachment().getData(); final var messageAttachment = message.getAttachment();
attachmentType = message.getAttachment().getType(); attachment = messageAttachment.getData();
attachmentName = messageAttachment.getName();
attachmentType = messageAttachment.getType();
} }
} }
@ -123,20 +126,20 @@ public class Message {
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
MessageBuilder prepareBuilder() { MessageBuilder prepareBuilder() {
var builder = new MessageBuilder(sender.getID(), recipient.getID(), id).setText(text) final var builder = new MessageBuilder(sender.getID(), recipient.getID(), id).setText(text)
.setCreationDate(creationDate) .setCreationDate(creationDate)
.setReceivedDate(receivedDate) .setReceivedDate(receivedDate)
.setReadDate(readDate) .setReadDate(readDate)
.setStatus(status) .setStatus(status)
.setForwarded(forwarded); .setForwarded(forwarded);
if (attachment != null) builder.setAttachment(new Attachment(attachment, attachmentType)); if (attachment != null) builder.setAttachment(new Attachment(attachment, attachmentName, attachmentType));
return builder; return builder;
} }
/** /**
* Sets the message status to {@link MessageStatus#RECEIVED} and sets the * Sets the message status to {@link MessageStatus#RECEIVED} and sets the
* current time stamp as the received date. * current time stamp as the received date.
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void received() { public void received() {
@ -147,7 +150,7 @@ public class Message {
/** /**
* Sets the message status to {@link MessageStatus#READ} and sets the * Sets the message status to {@link MessageStatus#READ} and sets the
* current time stamp as the read date. * current time stamp as the read date.
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void read() { public void read() {
@ -282,6 +285,18 @@ public class Message {
*/ */
public void setAttachmentType(AttachmentType attachmentType) { this.attachmentType = attachmentType; } public void setAttachmentType(AttachmentType attachmentType) { this.attachmentType = attachmentType; }
/**
* @return the attachmentName
* @since Envoy Server v0.2-beta
*/
public String getAttachmentName() { return attachmentName; }
/**
* @param attachmentName the attachmentName to set
* @since Envoy Server v0.2-beta
*/
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
/** /**
* @return whether this message is a forwarded message * @return whether this message is a forwarded message
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha

View File

@ -3,6 +3,7 @@ package envoy.server.net;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.lang.reflect.ParameterizedType;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -50,14 +51,19 @@ public class ObjectMessageProcessor implements IMessageProcessor {
logger.fine("Received " + obj); logger.fine("Received " + obj);
// Process object // Get processor and input class and process object
processors.stream().filter(p -> p.getInputClass().equals(obj.getClass())).forEach((@SuppressWarnings("rawtypes") ObjectProcessor p) -> { for (@SuppressWarnings("rawtypes")
try { ObjectProcessor p : processors) {
p.process(p.getInputClass().cast(obj), message.socketId, new ObjectWriteProxy(writeProxy)); Class<?> c = (Class<?>) ((ParameterizedType) p.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
} catch (IOException e) { if (c.equals(obj.getClass())) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e); try {
p.process(c.cast(obj), message.socketId, new ObjectWriteProxy(writeProxy));
break;
} catch (IOException e) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e);
}
} }
}); }
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@ -41,7 +41,4 @@ public class ContactOperationProcessor implements ObjectProcessor<ContactOperati
logger.warning(String.format("Received %s with an unsupported operation.", evt)); logger.warning(String.format("Received %s with an unsupported operation.", evt));
} }
} }
@Override
public Class<ContactOperation> getInputClass() { return ContactOperation.class; }
} }

View File

@ -40,7 +40,4 @@ public class GroupCreationProcessor implements ObjectProcessor<GroupCreation> {
.map(connectionManager::getSocketID) .map(connectionManager::getSocketID)
.forEach(memberSocketID -> writeProxy.write(memberSocketID, new ContactOperation(group.toCommon(), ElementOperation.ADD))); .forEach(memberSocketID -> writeProxy.write(memberSocketID, new ContactOperation(group.toCommon(), ElementOperation.ADD)));
} }
@Override
public Class<GroupCreation> getInputClass() { return GroupCreation.class; }
} }

View File

@ -61,7 +61,4 @@ public class GroupMessageProcessor implements ObjectProcessor<GroupMessage> {
logger.warning("Received a groupMessage with an ID that already exists"); logger.warning("Received a groupMessage with an ID that already exists");
} }
} }
@Override
public Class<GroupMessage> getInputClass() { return GroupMessage.class; }
} }

View File

@ -63,7 +63,4 @@ public class GroupMessageStatusChangeProcessor implements ObjectProcessor<GroupM
} }
persistenceManager.updateMessage(gmsg); persistenceManager.updateMessage(gmsg);
} }
@Override
public Class<GroupMessageStatusChange> getInputClass() { return GroupMessageStatusChange.class; }
} }

View File

@ -47,7 +47,4 @@ public class GroupResizeProcessor implements ObjectProcessor<GroupResize> {
.map(connectionManager::getSocketID) .map(connectionManager::getSocketID)
.forEach(memberSocketID -> writeProxy.write(memberSocketID, commonGroup)); .forEach(memberSocketID -> writeProxy.write(memberSocketID, commonGroup));
} }
@Override
public Class<GroupResize> getInputClass() { return GroupResize.class; }
} }

View File

@ -21,9 +21,6 @@ public class IDGeneratorRequestProcessor implements ObjectProcessor<IDGeneratorR
private static final long ID_RANGE = 200; private static final long ID_RANGE = 200;
@Override
public Class<IDGeneratorRequest> getInputClass() { return IDGeneratorRequest.class; }
@Override @Override
public void process(IDGeneratorRequest input, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(IDGeneratorRequest input, long socketID, ObjectWriteProxy writeProxy) throws IOException {
writeProxy.write(socketID, createIDGenerator()); writeProxy.write(socketID, createIDGenerator());

View File

@ -0,0 +1,34 @@
package envoy.server.processors;
import java.io.IOException;
import envoy.event.IsTyping;
import envoy.server.data.PersistenceManager;
import envoy.server.data.User;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
/**
* This processor handles incoming {@link IsTyping} events.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>IsTypingProcessor.java</strong><br>
* Created: <strong>24.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Server v0.2-beta
*/
public class IsTypingProcessor implements ObjectProcessor<IsTyping> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override
public void process(IsTyping event, long socketID, ObjectWriteProxy writeProxy) throws IOException {
final var contact = persistenceManager.getContactByID(event.get());
if (contact instanceof User) {
final var destinationID = event.getDestinationID();
if (connectionManager.isOnline(destinationID)) writeProxy.write(connectionManager.getSocketID(destinationID), event);
} else writeProxy.writeToOnlineContacts(contact.getContacts(), event);
}
}

View File

@ -191,7 +191,4 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Complete the handshake // Complete the handshake
writeProxy.write(socketID, user.toCommon()); writeProxy.write(socketID, user.toCommon());
} }
@Override
public Class<LoginCredentials> getInputClass() { return LoginCredentials.class; }
} }

View File

@ -58,7 +58,4 @@ public class MessageProcessor implements ObjectProcessor<Message> {
logger.log(Level.WARNING, "Received " + message + " with an ID that already exists!"); logger.log(Level.WARNING, "Received " + message + " with an ID that already exists!");
} }
} }
@Override
public Class<Message> getInputClass() { return Message.class; }
} }

View File

@ -42,7 +42,4 @@ public class MessageStatusChangeProcessor implements ObjectProcessor<MessageStat
final long senderID = msg.getSender().getID(); final long senderID = msg.getSender().getID();
if (connectionManager.isOnline(senderID)) writeProxy.write(connectionManager.getSocketID(senderID), statusChange); if (connectionManager.isOnline(senderID)) writeProxy.write(connectionManager.getSocketID(senderID), statusChange);
} }
@Override
public Class<MessageStatusChange> getInputClass() { return MessageStatusChange.class; }
} }

View File

@ -28,7 +28,4 @@ public class NameChangeProcessor implements ObjectProcessor<NameChange> {
// Notify online contacts of the name change // Notify online contacts of the name change
writeProxy.writeToOnlineContacts(toUpdate.getContacts(), nameChange); writeProxy.writeToOnlineContacts(toUpdate.getContacts(), nameChange);
} }
@Override
public Class<NameChange> getInputClass() { return NameChange.class; }
} }

View File

@ -18,12 +18,6 @@ import envoy.server.net.ObjectWriteProxy;
*/ */
public interface ObjectProcessor<T> { public interface ObjectProcessor<T> {
/**
* @return the class of the request object
* @since Envoy Server Standalone v0.1-alpha
*/
Class<T> getInputClass();
/** /**
* @param input the request object * @param input the request object
* @param socketID the ID of the socket from which the object was received * @param socketID the ID of the socket from which the object was received

View File

@ -38,7 +38,4 @@ public class UserSearchProcessor implements ObjectProcessor<UserSearchRequest> {
.map(User::toCommon) .map(User::toCommon)
.collect(Collectors.toList()))); .collect(Collectors.toList())));
} }
@Override
public Class<UserSearchRequest> getInputClass() { return UserSearchRequest.class; }
} }

View File

@ -26,9 +26,6 @@ public class UserStatusChangeProcessor implements ObjectProcessor<UserStatusChan
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserStatusChangeProcessor.class); private static final Logger logger = EnvoyLog.getLogger(UserStatusChangeProcessor.class);
@Override
public Class<UserStatusChange> getInputClass() { return UserStatusChange.class; }
@Override @Override
public void process(UserStatusChange input, long socketID, ObjectWriteProxy writeProxy) { public void process(UserStatusChange input, long socketID, ObjectWriteProxy writeProxy) {
// new status should not equal old status // new status should not equal old status