From 63b1809c90a56d26c10614a1f932da8b2756fcd1 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 13 Nov 2019 05:59:51 +0100 Subject: [PATCH 01/22] Loading config from properties before command line args --- src/main/java/envoy/client/Config.java | 4 +-- src/main/java/envoy/client/ui/Startup.java | 34 ++++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/envoy/client/Config.java b/src/main/java/envoy/client/Config.java index c31c972..545df2f 100644 --- a/src/main/java/envoy/client/Config.java +++ b/src/main/java/envoy/client/Config.java @@ -32,7 +32,7 @@ public class Config { * * @param properties a {@link Properties} object containing information about * the server and port, as well as the path to the local - * database + * database and the synchronization timeout * @since Envoy v0.1-alpha */ public void load(Properties properties) { @@ -64,8 +64,6 @@ public class Config { case "-db": localDB = new File(args[++i]); } - if (localDB == null) localDB = new File(".\\localDB"); - if (syncTimeout == 0) syncTimeout = 1000; } /** diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index c3a3baa..5a2d92f 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -26,21 +26,24 @@ public class Startup { public static void main(String[] args) { Config config = Config.getInstance(); - if (args.length > 0) { - config.load(args); - } else { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - try { - Properties configProperties = new Properties(); - configProperties.load(loader.getResourceAsStream("client.properties")); - config.load(configProperties); - } catch (IOException e) { - e.printStackTrace(); - } + + // Load the configuration from client.properties first + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Properties configProperties = new Properties(); + configProperties.load(loader.getResourceAsStream("client.properties")); + config.load(configProperties); + } catch (IOException e) { + e.printStackTrace(); } + // Override configuration values with command line arguments + if (args.length > 0) + config.load(args); + if (!config.isInitialized()) { - System.err.println("Server or port are not defined. Exiting..."); + JOptionPane.showMessageDialog(null, "Error loading configuration values.", "Configuration error", + JOptionPane.ERROR_MESSAGE); System.exit(1); } @@ -49,16 +52,15 @@ public class Startup { System.err.println("User name is not set or empty. Exiting..."); System.exit(1); } - Client client = new Client(config, userName); - LocalDB localDB = new LocalDB(client.getSender()); + Client client = new Client(config, userName); + LocalDB localDB = new LocalDB(client.getSender()); try { localDB.initializeDBFile(config.getLocalDB()); } catch (EnvoyException e) { e.printStackTrace(); JOptionPane.showMessageDialog(null, "Error while loading local database: " + e.toString() + "\nChats will not be stored locally.", - "Local DB error", - JOptionPane.WARNING_MESSAGE); + "Local DB error", JOptionPane.WARNING_MESSAGE); } EventQueue.invokeLater(() -> { From c93f9fe2306c966d53f8526e22d98fc257968fb0 Mon Sep 17 00:00:00 2001 From: derharry333 Date: Wed, 27 Nov 2019 17:07:25 +0100 Subject: [PATCH 02/22] Replaced print statements with logger statements. --- src/main/java/envoy/client/Client.java | 10 ++++++--- src/main/java/envoy/client/LocalDB.java | 21 +++++++++++-------- src/main/java/envoy/client/ui/ChatWindow.java | 5 ++++- src/main/java/envoy/client/ui/Startup.java | 10 +++++++-- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/main/java/envoy/client/Client.java b/src/main/java/envoy/client/Client.java index 522d44f..5b9a2b8 100644 --- a/src/main/java/envoy/client/Client.java +++ b/src/main/java/envoy/client/Client.java @@ -1,5 +1,7 @@ package envoy.client; +import java.util.logging.Logger; + import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; @@ -28,16 +30,18 @@ public class Client { private Config config; private User sender, recipient; + private static final Logger logger = Logger.getLogger(Client.class.getSimpleName()); + public Client(Config config, String username) { this.config = config; sender = getUser(username); - System.out.println("ID: " + sender.getID()); + + logger.info("ID: " + sender.getID()); } private R post(String uri, T body, Class responseBodyClass) { javax.ws.rs.client.Client client = ClientBuilder.newClient(); WebTarget target = client.target(uri); - Response response = target.request().post(Entity.entity(body, "application/xml")); R responseBody = response.readEntity(responseBodyClass); response.close(); @@ -92,7 +96,7 @@ public class Client { if (returnSenderSync.getUsers().size() == 1) { returnSender = returnSenderSync.getUsers().get(0); } else { - System.out.println("ERROR exiting..."); + logger.warning("ERROR exiting..."); } return returnSender; diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index 142b687..8af3094 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -9,6 +9,7 @@ import java.io.ObjectOutputStream; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; @@ -36,6 +37,8 @@ public class LocalDB { private ObjectFactory objectFactory = new ObjectFactory(); private DatatypeFactory datatypeFactory; + private static final Logger logger = Logger.getLogger(LocalDB.class.getSimpleName()); + /** * Constructs an empty local database. * @@ -80,13 +83,13 @@ public class LocalDB { localDB.createNewFile(); } catch (IOException e) { e.printStackTrace(); - System.err.println("unable to save the messages"); + logger.warning("unable to save the messages"); } try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(localDB))) { out.writeObject(chats); } catch (IOException ex) { ex.printStackTrace(); - System.err.println("unable to save the messages"); + logger.warning("unable to save the messages"); } } @@ -144,7 +147,7 @@ public class LocalDB { } readMessages.getMessages().clear(); - System.out.println(sync.getMessages().size()); + logger.info(String.format("Filled sync with %d messages.", sync.getMessages().size())); return sync; } @@ -207,20 +210,20 @@ public class LocalDB { if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.READ) { // Update local Messages to state READ - System.out.println("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + logger.info("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + "was initialized to be set to READ in localDB."); for (int j = 0; j < getChats().size(); j++) { if (getChats().get(j) .getRecipient() .getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - System.out.println("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); + logger.info("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync .getMessages() .get(i) .getMetadata() .getMessageId()) { - System.out.println("Message with ID: " + logger.info("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + "was selected."); getChats().get(j) @@ -228,7 +231,7 @@ public class LocalDB { .get(k) .getMetadata() .setState(returnSync.getMessages().get(i).getMetadata().getState()); - System.out.println("Message State is now: " + logger.info("Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); } } @@ -244,7 +247,7 @@ public class LocalDB { if (getChats().get(k).getRecipient().getID() == returnSync.getUsers().get(j).getID()) { getChats().get(k).getRecipient().setStatus(returnSync.getUsers().get(j).getStatus()); - System.out.println(getChats().get(k).getRecipient().getStatus().toString()); + logger.info(getChats().get(k).getRecipient().getStatus().toString()); } } } @@ -342,7 +345,7 @@ public class LocalDB { for (int j = 0; j < getChats().get(i).getModel().getSize(); j++) { if (getChats().get(i).getModel().get(j).getMetadata().getState() == MessageState.WAITING) { // addMessageToSync(localDB.getChats().get(i).getModel().get(j)); - System.out.println("Got Waiting Message"); + logger.info("Got Waiting Message"); sync.getMessages().add(0, getChats().get(i).getModel().get(j)); } } diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index 2a7ae11..c6fce27 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -10,6 +10,7 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.logging.Logger; import javax.swing.DefaultListModel; import javax.swing.JButton; @@ -56,6 +57,8 @@ public class ChatWindow extends JFrame { private Chat currentChat; private JTextArea messageEnterTextArea; + + private static final Logger logger = Logger.getLogger(ChatWindow.class.getSimpleName()); public ChatWindow(Client client, LocalDB localDB) { this.client = client; @@ -186,7 +189,7 @@ public class ChatWindow extends JFrame { SettingsScreen.open(localDB.getUser().getName()); } catch (Exception e) { SettingsScreen.open(); - System.err.println("An error occured while opening the settings screen: " + e); + logger.warning("An error occured while opening the settings screen: " + e); e.printStackTrace(); } }); diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index c3a3baa..0fdd76f 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -3,6 +3,8 @@ package envoy.client.ui; import java.awt.EventQueue; import java.io.IOException; import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JOptionPane; @@ -24,7 +26,11 @@ import envoy.exception.EnvoyException; */ public class Startup { + private static final Logger logger = Logger.getLogger(Client.class.getSimpleName()); + public static void main(String[] args) { + logger.setLevel(Level.ALL); + Config config = Config.getInstance(); if (args.length > 0) { config.load(args); @@ -40,13 +46,13 @@ public class Startup { } if (!config.isInitialized()) { - System.err.println("Server or port are not defined. Exiting..."); + logger.warning("Server or port are not defined. Exiting..."); System.exit(1); } String userName = JOptionPane.showInputDialog("Please enter your username"); if (userName == null || userName.isEmpty()) { - System.err.println("User name is not set or empty. Exiting..."); + logger.warning("User name is not set or empty. Exiting..."); System.exit(1); } Client client = new Client(config, userName); From cb0e7db444b1efb45cf73f016be04fe8526bf615 Mon Sep 17 00:00:00 2001 From: delvh Date: Fri, 29 Nov 2019 20:48:21 +0100 Subject: [PATCH 03/22] Fixed minor Javadoc errors --- src/main/java/envoy/client/LocalDB.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index a07ba8b..44f0f19 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -47,7 +47,7 @@ public class LocalDB { /** * Constructs an empty local database. * - * @param client the user that is logged in with this client + * @param sender the user that is logged in with this client * @since Envoy v0.1-alpha */ @@ -117,7 +117,9 @@ public class LocalDB { * Creates a {@link Message} object serializable to XML. * * @param textContent The content (text) of the message + * @param recipient The recipient of the message * @return prepared {@link Message} object + * @since Envoy v0.1-alpha */ public Message createMessage(String textContent, User recipient) { Message.Metadata metaData = objectFactory.createMessageMetadata(); From 8e5d166d75237d8afa54ddd33a7fbf5bc8b69318 Mon Sep 17 00:00:00 2001 From: delvh Date: Fri, 29 Nov 2019 20:54:33 +0100 Subject: [PATCH 04/22] Deleted unnecessary blank line --- src/main/java/envoy/client/LocalDB.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index 44f0f19..b1668ad 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -50,7 +50,6 @@ public class LocalDB { * @param sender the user that is logged in with this client * @since Envoy v0.1-alpha */ - public LocalDB(User sender) { this.sender = sender; try { From 06bd12743255b4c9450b35c042cf5c63e92557eb Mon Sep 17 00:00:00 2001 From: kske Date: Mon, 2 Dec 2019 21:44:18 +0100 Subject: [PATCH 05/22] Added a test call to the system tray displaying a message --- src/main/java/envoy/client/ui/ChatWindow.java | 24 ++++++++++++++---- src/main/resources/Envoy Logo.png | Bin 0 -> 8152 bytes 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/Envoy Logo.png diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index 32b024b..0eeccc0 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -1,11 +1,17 @@ package envoy.client.ui; +import java.awt.AWTException; import java.awt.Color; import java.awt.ComponentOrientation; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.Image; import java.awt.Insets; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.TrayIcon.MessageType; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; @@ -37,7 +43,7 @@ import envoy.schema.User; * Project: envoy-client
* File: ChatWindow.java
* Created: 28 Sep 2019
- * + * * @author Kai S. K. Engelbart * @author Maximilian Käfer * @author Leon Hofmeister @@ -122,13 +128,11 @@ public class ChatWindow extends JFrame { @Override public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER && ((SettingsScreen.enterToSend && e.getModifiersEx() == 0) || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) { postMessage(messageList); } - } }); // Checks for changed Message @@ -287,7 +291,7 @@ public class ChatWindow extends JFrame { /** * Initializes the elements of the user list by downloading them from the * server. - * + * * @since Envoy v0.1-alpha */ private void loadUsersAndChats() { @@ -308,7 +312,7 @@ public class ChatWindow extends JFrame { /** * Updates the data model and the UI repeatedly after a certain amount of * time. - * + * * @param timeout the amount of time that passes between two requests sent to * the server * @since Envoy v0.1-alpha @@ -346,4 +350,14 @@ public class ChatWindow extends JFrame { * Marks messages in the current chat as {@code READ}. */ private void readCurrentChat() { if (currentChat != null) { localDB.setMessagesToRead(currentChat); } } + + private void displayNotification(String message) throws AWTException { + SystemTray tray = SystemTray.getSystemTray(); + Image image = Toolkit.getDefaultToolkit().createImage(getClass().getResource("Envoy Logo.png")); + TrayIcon trayIcon = new TrayIcon(image, "Envoy Client"); + trayIcon.setImageAutoSize(true); + trayIcon.setToolTip("You are notified if you have unread messages."); + tray.add(trayIcon); + trayIcon.displayMessage("Envoy Client", message, MessageType.INFO); + } } diff --git a/src/main/resources/Envoy Logo.png b/src/main/resources/Envoy Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..35ef7d9fd78cc3b79454de104ae59fbb65c0b050 GIT binary patch literal 8152 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 Date: Tue, 3 Dec 2019 21:48:16 +0100 Subject: [PATCH 06/22] Moved system tray logic to StatusTrayIcon class --- src/main/java/envoy/client/ui/ChatWindow.java | 37 +++-------------- .../java/envoy/client/ui/StatusTrayIcon.java | 39 ++++++++++++++++++ .../{Envoy Logo.png => envoy_logo.png} | Bin 3 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 src/main/java/envoy/client/ui/StatusTrayIcon.java rename src/main/resources/{Envoy Logo.png => envoy_logo.png} (100%) diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index 0eeccc0..7a5c67c 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -1,17 +1,11 @@ package envoy.client.ui; -import java.awt.AWTException; import java.awt.Color; import java.awt.ComponentOrientation; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.awt.Image; import java.awt.Insets; -import java.awt.SystemTray; -import java.awt.Toolkit; -import java.awt.TrayIcon; -import java.awt.TrayIcon.MessageType; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; @@ -128,8 +122,8 @@ public class ChatWindow extends JFrame { @Override public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER && ((SettingsScreen.enterToSend && e.getModifiersEx() == 0) - || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) { + if (e.getKeyCode() == KeyEvent.VK_ENTER + && ((SettingsScreen.enterToSend && e.getModifiersEx() == 0) || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) { postMessage(messageList); } @@ -220,11 +214,7 @@ public class ChatWindow extends JFrame { final User user = selectedUserList.getSelectedValue(); client.setRecipient(user); - currentChat = localDB.getChats() - .stream() - .filter(chat -> chat.getRecipient().getID() == user.getID()) - .findFirst() - .get(); + currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getID() == user.getID()).findFirst().get(); // Set all unread messages in the chat to read readCurrentChat(); @@ -263,10 +253,7 @@ public class ChatWindow extends JFrame { private void postMessage(JList messageList) { if (!client.hasRecipient()) { - JOptionPane.showMessageDialog(this, - "Please select a recipient!", - "Cannot send message", - JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); } if (!messageEnterTextArea.getText().isEmpty()) try { @@ -322,8 +309,7 @@ public class ChatWindow extends JFrame { new Thread(() -> { // Synchronize - localDB.applySync( - client.sendSync(client.getSender().getID(), localDB.fillSync(client.getSender().getID()))); + localDB.applySync(client.sendSync(client.getSender().getID(), localDB.fillSync(client.getSender().getID()))); // Process unread messages localDB.addUnreadMessagesToLocalDB(); @@ -333,8 +319,7 @@ public class ChatWindow extends JFrame { readCurrentChat(); // Update UI - SwingUtilities - .invokeLater(() -> { updateUserStates(); contentPane.revalidate(); contentPane.repaint(); }); + SwingUtilities.invokeLater(() -> { updateUserStates(); contentPane.revalidate(); contentPane.repaint(); }); }).start(); }).start(); } @@ -350,14 +335,4 @@ public class ChatWindow extends JFrame { * Marks messages in the current chat as {@code READ}. */ private void readCurrentChat() { if (currentChat != null) { localDB.setMessagesToRead(currentChat); } } - - private void displayNotification(String message) throws AWTException { - SystemTray tray = SystemTray.getSystemTray(); - Image image = Toolkit.getDefaultToolkit().createImage(getClass().getResource("Envoy Logo.png")); - TrayIcon trayIcon = new TrayIcon(image, "Envoy Client"); - trayIcon.setImageAutoSize(true); - trayIcon.setToolTip("You are notified if you have unread messages."); - tray.add(trayIcon); - trayIcon.displayMessage("Envoy Client", message, MessageType.INFO); - } } diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java new file mode 100644 index 0000000..54bc3a3 --- /dev/null +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -0,0 +1,39 @@ +package envoy.client.ui; + +import java.awt.AWTException; +import java.awt.Image; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; + +import envoy.exception.EnvoyException; + +/** + * Project: envoy-client
+ * File: StatusTrayIcon.java
+ * Created: 3 Dec 2019
+ * + * @author Kai S. K. Engelbart + * @since Envoy v0.2-alpha + */ +public class StatusTrayIcon { + + private TrayIcon trayIcon; + + public StatusTrayIcon() throws EnvoyException { + if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); + + Image img = Toolkit.getDefaultToolkit().createImage(getClass().getResource("envoy_logo.png")); + TrayIcon trayIcon = new TrayIcon(img, "Envoy Client"); + trayIcon.setImageAutoSize(true); + trayIcon.setToolTip("You are notified if you have unread messages."); + try { + SystemTray.getSystemTray().add(trayIcon); + } catch (AWTException e) { + throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e); + } + } + + // TODO: Add event listener + // trayIcon.displayMessage("Envoy Client", message, MessageType.INFO); +} diff --git a/src/main/resources/Envoy Logo.png b/src/main/resources/envoy_logo.png similarity index 100% rename from src/main/resources/Envoy Logo.png rename to src/main/resources/envoy_logo.png From 378a83638a3ff2fc498a6cfd2fba5e7b0305b368 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 4 Dec 2019 07:50:59 +0100 Subject: [PATCH 07/22] Added a small popup menu to StatusTrayIcon and loading it in Startup --- src/main/java/envoy/client/ui/Startup.java | 1 + .../java/envoy/client/ui/StatusTrayIcon.java | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index c3a3baa..7b35158 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -64,6 +64,7 @@ public class Startup { EventQueue.invokeLater(() -> { try { ChatWindow frame = new ChatWindow(client, localDB); + new StatusTrayIcon().show(); frame.setVisible(true); } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 54bc3a3..7ef6877 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -2,6 +2,8 @@ package envoy.client.ui; import java.awt.AWTException; import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; @@ -24,16 +26,31 @@ public class StatusTrayIcon { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); Image img = Toolkit.getDefaultToolkit().createImage(getClass().getResource("envoy_logo.png")); - TrayIcon trayIcon = new TrayIcon(img, "Envoy Client"); + trayIcon = new TrayIcon(img, "Envoy Client"); trayIcon.setImageAutoSize(true); trayIcon.setToolTip("You are notified if you have unread messages."); + + PopupMenu popup = new PopupMenu(); + + MenuItem exitMenuItem = new MenuItem("Exit"); + exitMenuItem.addActionListener((evt) -> System.exit(0)); + popup.add(exitMenuItem); + + trayIcon.setPopupMenu(popup); + } + + /** + * Makes this {@link StatusTrayIcon} appear in the system tray. + * @throws EnvoyException + */ + public void show() throws EnvoyException { try { SystemTray.getSystemTray().add(trayIcon); } catch (AWTException e) { throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e); } } - + // TODO: Add event listener // trayIcon.displayMessage("Envoy Client", message, MessageType.INFO); } From 3c7f95f86973a9ff22dc91e506e33b812cbdb563 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 4 Dec 2019 18:50:06 +0100 Subject: [PATCH 08/22] Added event system + Event interface for defining event objects + EventHandler interface for defining event handlers + EventBus singleton class for managing event handlers and dispatching events --- src/main/java/envoy/client/event/Event.java | 16 ++++++++++ .../java/envoy/client/event/EventBus.java | 32 +++++++++++++++++++ .../java/envoy/client/event/EventHandler.java | 25 +++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/main/java/envoy/client/event/Event.java create mode 100644 src/main/java/envoy/client/event/EventBus.java create mode 100644 src/main/java/envoy/client/event/EventHandler.java diff --git a/src/main/java/envoy/client/event/Event.java b/src/main/java/envoy/client/event/Event.java new file mode 100644 index 0000000..5d2e8a3 --- /dev/null +++ b/src/main/java/envoy/client/event/Event.java @@ -0,0 +1,16 @@ +package envoy.client.event; + +/** + * Project: envoy-clientChess
+ * File: Event.javaEvent.java
+ * Created: 04.12.2019
+ * + * @author Kai S. K. Engelbart + */ +public interface Event { + + /** + * @return the data associated with this event + */ + T get(); +} diff --git a/src/main/java/envoy/client/event/EventBus.java b/src/main/java/envoy/client/event/EventBus.java new file mode 100644 index 0000000..c97844b --- /dev/null +++ b/src/main/java/envoy/client/event/EventBus.java @@ -0,0 +1,32 @@ +package envoy.client.event; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Project: envoy-clientChess
+ * File: EventBus.javaEvent.java
+ * Created: 04.12.2019
+ * + * @author Kai S. K. Engelbart + */ +public class EventBus { + + private static EventBus eventBus; + + private List handlers = new ArrayList<>(); + + private EventBus() {} + + public EventBus getInstance() { + if (eventBus == null) eventBus = new EventBus(); + return eventBus; + } + + public void register(EventHandler... handlers) { this.handlers.addAll(Arrays.asList(handlers)); } + + public void dispatch(Event event) { handlers.stream().filter(h -> h.supports().contains(event.getClass())).forEach(h -> h.handle(event)); } + + public List getHandlers() { return handlers; } +} diff --git a/src/main/java/envoy/client/event/EventHandler.java b/src/main/java/envoy/client/event/EventHandler.java new file mode 100644 index 0000000..a6e5b81 --- /dev/null +++ b/src/main/java/envoy/client/event/EventHandler.java @@ -0,0 +1,25 @@ +package envoy.client.event; + +import java.util.Set; + +/** + * Project: envoy-clientChess
+ * File: EventHandler.javaEvent.java
+ * Created: 04.12.2019
+ * + * @author Kai S. K. Engelbart + */ +public interface EventHandler { + + /** + * Consumes an event dispatched by the event bus. + * + * @param event The event dispatched by the event bus, only of supported type + */ + void handle(Event event); + + /** + * @return A set of classes this class is supposed to handle in events + */ + Set>> supports(); +} From b5badae77308a14860e200aa856c2e688e60b353 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 4 Dec 2019 18:52:48 +0100 Subject: [PATCH 09/22] Added StatusTrayIcon#displayMessageNotification method Using this method, a message object can be displayed as a OS-specific notification, which can be useful in the future to alert the user about an incoming message while the application is not in focus. --- .../java/envoy/client/ui/StatusTrayIcon.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 7ef6877..869e678 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -7,8 +7,10 @@ import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; +import java.awt.TrayIcon.MessageType; import envoy.exception.EnvoyException; +import envoy.schema.Message; /** * Project: envoy-client
@@ -25,22 +27,23 @@ public class StatusTrayIcon { public StatusTrayIcon() throws EnvoyException { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); - Image img = Toolkit.getDefaultToolkit().createImage(getClass().getResource("envoy_logo.png")); - trayIcon = new TrayIcon(img, "Envoy Client"); + Image img = Toolkit.getDefaultToolkit().createImage(getClass().getResource("envoy_logo.png")); + trayIcon = new TrayIcon(img, "Envoy Client"); trayIcon.setImageAutoSize(true); trayIcon.setToolTip("You are notified if you have unread messages."); - + PopupMenu popup = new PopupMenu(); - + MenuItem exitMenuItem = new MenuItem("Exit"); exitMenuItem.addActionListener((evt) -> System.exit(0)); popup.add(exitMenuItem); - + trayIcon.setPopupMenu(popup); } /** * Makes this {@link StatusTrayIcon} appear in the system tray. + * * @throws EnvoyException */ public void show() throws EnvoyException { @@ -50,7 +53,14 @@ public class StatusTrayIcon { throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e); } } - - // TODO: Add event listener - // trayIcon.displayMessage("Envoy Client", message, MessageType.INFO); + + /** + * Notifies the user of a message by displaying a popup. + * + * @param message the {@link Message} object to display in the popup + */ + public void displayMessageNotification(Message message) { + // TODO: Add event listener + trayIcon.displayMessage("New message received", message.getContent().get(0).getText(), MessageType.INFO); + } } From e7439ac1372eaed4890e4c717607cf4c6cf647c9 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Wed, 4 Dec 2019 22:01:05 +0100 Subject: [PATCH 10/22] Removed unnecessary project natures in .project --- .project | 2 -- 1 file changed, 2 deletions(-) diff --git a/.project b/.project index 08d7599..49a90bb 100644 --- a/.project +++ b/.project @@ -34,7 +34,5 @@ org.eclipse.jdt.core.javanature org.eclipse.m2e.core.maven2Nature - org.jboss.tools.jst.web.kb.kbnature - org.jboss.tools.cdi.core.cdinature From af7408142c55560945685c8ce0cbcf55fec89b2c Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 4 Dec 2019 22:26:24 +0100 Subject: [PATCH 11/22] Added message events, triggering message creation event + Abstract MessageEvent class with MessageCreationEvent and MessageModificationEvent subclasses + Made StatusTrayIcon an event handler - Fixed EventBus#getInstance not being static --- src/main/java/envoy/client/LocalDB.java | 15 ++++++---- .../java/envoy/client/event/EventBus.java | 4 +-- .../client/event/MessageCreationEvent.java | 18 ++++++++++++ .../java/envoy/client/event/MessageEvent.java | 20 +++++++++++++ .../event/MessageModificationEvent.java | 18 ++++++++++++ .../java/envoy/client/ui/StatusTrayIcon.java | 29 +++++++++++++------ 6 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 src/main/java/envoy/client/event/MessageCreationEvent.java create mode 100644 src/main/java/envoy/client/event/MessageEvent.java create mode 100644 src/main/java/envoy/client/event/MessageModificationEvent.java diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index 3cc96a2..bf6f01a 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -13,6 +13,8 @@ import java.util.List; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; +import envoy.client.event.EventBus; +import envoy.client.event.MessageCreationEvent; import envoy.exception.EnvoyException; import envoy.schema.Message; import envoy.schema.Message.Metadata.MessageState; @@ -112,7 +114,7 @@ public class LocalDB { /** * Creates a {@link Message} object serializable to XML. - * + * * @param textContent The content (text) of the message * @return prepared {@link Message} object */ @@ -161,6 +163,9 @@ public class LocalDB { && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.RECEIVED) { // these are the unread Messages from the server unreadMessagesSync.getMessages().add(returnSync.getMessages().get(i)); + + // Create and dispatch message creation event + EventBus.getInstance().dispatch(new MessageCreationEvent(returnSync.getMessages().get(i))); } if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 && returnSync.getMessages().get(i).getMetadata().getSender() == 0 @@ -225,7 +230,7 @@ public class LocalDB { /** * Adds the unread messages returned from the server in the latest sync to the * right chats in the LocalDB. - * + * * @param localDB * @since Envoy v0.1-alpha */ @@ -243,7 +248,7 @@ public class LocalDB { * {@code READ}. *
* Adds these messages to the {@code readMessages} {@link Sync} object. - * + * * @param currentChat * @since Envoy v0.1-alpha */ @@ -259,7 +264,7 @@ public class LocalDB { /** * Adds all messages with state {@code WAITING} from the {@link LocalDB} to the * {@link Sync} object. - * + * * @since Envoy v0.1-alpha */ private void addWaitingMessagesToSync() { @@ -273,7 +278,7 @@ public class LocalDB { /** * Clears the {@code unreadMessagesSync} {@link Sync} object. - * + * * @since Envoy v0.1-alpha */ public void clearUnreadMessagesSync() { unreadMessagesSync.getMessages().clear(); } diff --git a/src/main/java/envoy/client/event/EventBus.java b/src/main/java/envoy/client/event/EventBus.java index c97844b..f1c40fc 100644 --- a/src/main/java/envoy/client/event/EventBus.java +++ b/src/main/java/envoy/client/event/EventBus.java @@ -8,7 +8,7 @@ import java.util.List; * Project: envoy-clientChess
* File: EventBus.javaEvent.java
* Created: 04.12.2019
- * + * * @author Kai S. K. Engelbart */ public class EventBus { @@ -19,7 +19,7 @@ public class EventBus { private EventBus() {} - public EventBus getInstance() { + public static EventBus getInstance() { if (eventBus == null) eventBus = new EventBus(); return eventBus; } diff --git a/src/main/java/envoy/client/event/MessageCreationEvent.java b/src/main/java/envoy/client/event/MessageCreationEvent.java new file mode 100644 index 0000000..28f1b58 --- /dev/null +++ b/src/main/java/envoy/client/event/MessageCreationEvent.java @@ -0,0 +1,18 @@ +package envoy.client.event; + +import envoy.schema.Message; + +/** + * Project: envoy-client
+ * File: MessageCreationEvent.java
+ * Created: 4 Dec 2019
+ * + * @author Kai S. K. Engelbart + */ +public class MessageCreationEvent extends MessageEvent { + + /** + * @param message the {@link Message} that has been created + */ + public MessageCreationEvent(Message message) { super(message); } +} diff --git a/src/main/java/envoy/client/event/MessageEvent.java b/src/main/java/envoy/client/event/MessageEvent.java new file mode 100644 index 0000000..014a7bb --- /dev/null +++ b/src/main/java/envoy/client/event/MessageEvent.java @@ -0,0 +1,20 @@ +package envoy.client.event; + +import envoy.schema.Message; + +/** + * Project: envoy-client
+ * File: MessageCreationEvent.java
+ * Created: 4 Dec 2019
+ * + * @author Kai S. K. Engelbart + */ +public class MessageEvent implements Event { + + protected final Message message; + + public MessageEvent(Message message) { this.message = message; } + + @Override + public Message get() { return message; } +} diff --git a/src/main/java/envoy/client/event/MessageModificationEvent.java b/src/main/java/envoy/client/event/MessageModificationEvent.java new file mode 100644 index 0000000..0b83ef0 --- /dev/null +++ b/src/main/java/envoy/client/event/MessageModificationEvent.java @@ -0,0 +1,18 @@ +package envoy.client.event; + +import envoy.schema.Message; + +/** + * Project: envoy-client
+ * File: MessageModificationEvent.java
+ * Created: 4 Dec 2019
+ * + * @author Kai S. K. Engelbart + */ +public class MessageModificationEvent extends MessageEvent { + + /** + * @param message the {@link Message} that has been modified + */ + public MessageModificationEvent(Message message) { super(message); } +} diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 869e678..ab668e1 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -8,9 +8,14 @@ import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.TrayIcon.MessageType; +import java.util.HashSet; +import java.util.Set; +import envoy.client.event.Event; +import envoy.client.event.EventBus; +import envoy.client.event.EventHandler; +import envoy.client.event.MessageCreationEvent; import envoy.exception.EnvoyException; -import envoy.schema.Message; /** * Project: envoy-client
@@ -20,7 +25,7 @@ import envoy.schema.Message; * @author Kai S. K. Engelbart * @since Envoy v0.2-alpha */ -public class StatusTrayIcon { +public class StatusTrayIcon implements EventHandler { private TrayIcon trayIcon; @@ -39,11 +44,12 @@ public class StatusTrayIcon { popup.add(exitMenuItem); trayIcon.setPopupMenu(popup); + EventBus.getInstance().register(this); } /** * Makes this {@link StatusTrayIcon} appear in the system tray. - * + * * @throws EnvoyException */ public void show() throws EnvoyException { @@ -55,12 +61,17 @@ public class StatusTrayIcon { } /** - * Notifies the user of a message by displaying a popup. - * - * @param message the {@link Message} object to display in the popup + * Notifies the user of a message by displaying a pop-up. */ - public void displayMessageNotification(Message message) { - // TODO: Add event listener - trayIcon.displayMessage("New message received", message.getContent().get(0).getText(), MessageType.INFO); + @Override + public void handle(Event event) { + trayIcon.displayMessage("New message received", ((MessageCreationEvent) event).get().getContent().get(0).getText(), MessageType.INFO); + } + + @Override + public Set>> supports() { + Set>> supportedEvents = new HashSet<>(); + supportedEvents.add(MessageCreationEvent.class); + return supportedEvents; } } From dca65df9bdd8b995e404a1f0c8de9c867ac76e81 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 4 Dec 2019 23:27:17 +0100 Subject: [PATCH 12/22] Added Javadoc to event related classes and StatusTrayIcon --- src/main/java/envoy/client/LocalDB.java | 1 - src/main/java/envoy/client/event/Event.java | 1 + .../java/envoy/client/event/EventBus.java | 57 ++++++++++++++++--- .../java/envoy/client/ui/StatusTrayIcon.java | 28 ++++++++- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index bf6f01a..c080541 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -49,7 +49,6 @@ public class LocalDB { * @param client the user that is logged in with this client * @since Envoy v0.1-alpha */ - public LocalDB(User sender) { this.sender = sender; try { diff --git a/src/main/java/envoy/client/event/Event.java b/src/main/java/envoy/client/event/Event.java index 5d2e8a3..9db2477 100644 --- a/src/main/java/envoy/client/event/Event.java +++ b/src/main/java/envoy/client/event/Event.java @@ -6,6 +6,7 @@ package envoy.client.event; * Created: 04.12.2019
* * @author Kai S. K. Engelbart + * @since Envoy v0.2-alpha */ public interface Event { diff --git a/src/main/java/envoy/client/event/EventBus.java b/src/main/java/envoy/client/event/EventBus.java index f1c40fc..f6da3f5 100644 --- a/src/main/java/envoy/client/event/EventBus.java +++ b/src/main/java/envoy/client/event/EventBus.java @@ -5,28 +5,67 @@ import java.util.Arrays; import java.util.List; /** - * Project: envoy-clientChess
- * File: EventBus.javaEvent.java
+ * This class handles events by allowing {@link EventHandler} object to register + * themselves and then be notified about certain events dispatched by the event + * bus.
+ *
+ * The event bus is a singleton and can be used across the entire application to + * guarantee the propagation of events. + * Project: envoy-client
+ * File: EventBus.java
* Created: 04.12.2019
- * + * * @author Kai S. K. Engelbart + * @since Envoy v0.2-alpha */ public class EventBus { - private static EventBus eventBus; - + /** + * Contains all {@link EventHandler} instances registered at this + * {@link EventBus}. + */ private List handlers = new ArrayList<>(); + /** + * The singleton instance of this {@link EventBus} that is used across the + * entire application. + */ + private static EventBus eventBus = new EventBus(); + + /** + * This constructor is not accessible from outside this class because a + * singleton instance of it is provided by the {@link EventBus#getInstance()} + * method. + */ private EventBus() {} - public static EventBus getInstance() { - if (eventBus == null) eventBus = new EventBus(); - return eventBus; - } + /** + * @return the singleton instance of the {@link EventBus} + * @since Envoy v0.2-alpha + */ + public static EventBus getInstance() { return eventBus; } + /** + * Registers a list of {@link EventHandler} objects to be notified when a + * {@link Event} is dispatched that they are subscribed to. + * + * @param handlers the {@link EventHandler} objects to register + * @since Envoy v0.2-alpha + */ public void register(EventHandler... handlers) { this.handlers.addAll(Arrays.asList(handlers)); } + /** + * Dispatches a {@link Event} to every {@link EventHandler} subscribed to it. + * + * @param event the {@link Event} to dispatch + * @since Envoy v0.2-alpha + */ public void dispatch(Event event) { handlers.stream().filter(h -> h.supports().contains(event.getClass())).forEach(h -> h.handle(event)); } + /** + * @return a list of all {@link EventHandler} instances currently registered at + * this {@link EventBus} + * @since Envoy v0.2-alpha + */ public List getHandlers() { return handlers; } } diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index ab668e1..951fbd4 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -27,8 +27,20 @@ import envoy.exception.EnvoyException; */ public class StatusTrayIcon implements EventHandler { + /** + * The {@link TrayIcon} provided by the System Tray API for controlling the + * system tray. This includes displaying the icon, but also creating + * notifications when new messages are received. + */ private TrayIcon trayIcon; + /** + * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up + * menu. + * + * @throws EnvoyException if the currently used OS does not support the System + * Tray API + */ public StatusTrayIcon() throws EnvoyException { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); @@ -50,7 +62,9 @@ public class StatusTrayIcon implements EventHandler { /** * Makes this {@link StatusTrayIcon} appear in the system tray. * - * @throws EnvoyException + * @throws EnvoyException if the status icon could not be attaches to the system + * tray for system-internal reasons + * @since Envoy v0.2-alpha */ public void show() throws EnvoyException { try { @@ -61,13 +75,23 @@ public class StatusTrayIcon implements EventHandler { } /** - * Notifies the user of a message by displaying a pop-up. + * Notifies the user of a message by displaying a pop-up every time a new + * message is received. + * + * @since Envoy v0.2-alpha */ @Override public void handle(Event event) { trayIcon.displayMessage("New message received", ((MessageCreationEvent) event).get().getContent().get(0).getText(), MessageType.INFO); } + /** + * The {@link StatusTrayIcon} only reacts to {@link MessageCreationEvent} + * instances which signify newly received messages. + * + * @return A set with the single element {@code MessageCreationEvent.class} + * @since Envoy v0.2-alpha + */ @Override public Set>> supports() { Set>> supportedEvents = new HashSet<>(); From 50945a45a2f616274eae56901eaebd96cd0ee17e Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 5 Dec 2019 15:05:05 +0100 Subject: [PATCH 13/22] Simplified and optimized sync related code in LocalDB --- src/main/java/envoy/client/LocalDB.java | 141 ++++++++++++------------ 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index c080541..d15e62a 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -147,80 +147,83 @@ public class LocalDB { public void applySync(Sync returnSync) { for (int i = 0; i < returnSync.getMessages().size(); i++) { - if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 - && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.SENT) { - // Update Local Messages with State WAITING (add Message ID and change State to - // SENT) - for (int j = 0; j < sync.getMessages().size(); j++) { - if (j == i) { - sync.getMessages().get(j).getMetadata().setMessageId(returnSync.getMessages().get(j).getMetadata().getMessageId()); - sync.getMessages().get(j).getMetadata().setState(returnSync.getMessages().get(j).getMetadata().getState()); - } - } - } - if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 && returnSync.getMessages().get(i).getMetadata().getSender() != 0 - && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.RECEIVED) { - // these are the unread Messages from the server - unreadMessagesSync.getMessages().add(returnSync.getMessages().get(i)); - // Create and dispatch message creation event - EventBus.getInstance().dispatch(new MessageCreationEvent(returnSync.getMessages().get(i))); - } + // The message has an ID + if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0) { - if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 && returnSync.getMessages().get(i).getMetadata().getSender() == 0 - && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.RECEIVED) { - // Update Messages in localDB to state RECEIVED - for (int j = 0; j < getChats().size(); j++) { - if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { - if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() - .get(i) - .getMetadata() - .getMessageId()) { - // Update Message in LocalDB - getChats().get(j).getModel().get(k).getMetadata().setState(returnSync.getMessages().get(j).getMetadata().getState()); + // Messages are processes differently corresponding to their state + switch (returnSync.getMessages().get(i).getMetadata().getState()) { + case SENT: + // Update previously waiting and now sent messages that were assigned an ID by + // the server + sync.getMessages().get(i).getMetadata().setMessageId(returnSync.getMessages().get(i).getMetadata().getMessageId()); + sync.getMessages().get(i).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); + break; + case RECEIVED: + if (returnSync.getMessages().get(i).getMetadata().getSender() != 0) { + // these are the unread Messages from the server + unreadMessagesSync.getMessages().add(returnSync.getMessages().get(i)); + + // Create and dispatch message creation event + EventBus.getInstance().dispatch(new MessageCreationEvent(returnSync.getMessages().get(i))); + } else { + // Update Messages in localDB to state RECEIVED + for (int j = 0; j < getChats().size(); j++) { + if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { + for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { + if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() + .get(i) + .getMetadata() + .getMessageId()) { + // Update Message in LocalDB + getChats().get(j) + .getModel() + .get(k) + .getMetadata() + .setState(returnSync.getMessages().get(j).getMetadata().getState()); + } + } + } } } - } - } - - } - - if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 - && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.READ) { - // Update local Messages to state READ - System.out.println("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() - + "was initialized to be set to READ in localDB."); - for (int j = 0; j < getChats().size(); j++) { - if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - System.out.println("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); - for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { - if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() - .get(i) - .getMetadata() - .getMessageId()) { - System.out.println( - "Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + "was selected."); - getChats().get(j).getModel().get(k).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); - System.out - .println("Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); + break; + case READ: + // Update local Messages to state READ + System.out.println("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + + "was initialized to be set to READ in localDB."); + for (int j = 0; j < getChats().size(); j++) { + if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { + System.out.println("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); + for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { + if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() + .get(i) + .getMetadata() + .getMessageId()) { + System.out.println("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + + "was selected."); + getChats().get(j) + .getModel() + .get(k) + .getMetadata() + .setState(returnSync.getMessages().get(i).getMetadata().getState()); + System.out.println( + "Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); + } + } } } - } + break; } } } // Updating UserStatus of all users in LocalDB - for (int j = 0; j < returnSync.getUsers().size(); j++) { - for (int k = 0; k < getChats().size(); k++) { - if (getChats().get(k).getRecipient().getID() == returnSync.getUsers().get(j).getID()) { - - getChats().get(k).getRecipient().setStatus(returnSync.getUsers().get(j).getStatus()); - System.out.println(getChats().get(k).getRecipient().getStatus().toString()); + for (User user : returnSync.getUsers()) + for (Chat chat : getChats()) + if (user.getID() == chat.getRecipient().getID()) { + chat.getRecipient().setStatus(user.getStatus()); + System.out.println(chat.getRecipient().getStatus()); } - } - } sync.getMessages().clear(); sync.getUsers().clear(); @@ -234,11 +237,11 @@ public class LocalDB { * @since Envoy v0.1-alpha */ public void addUnreadMessagesToLocalDB() { - Sync unreadMessages = unreadMessagesSync; - for (int i = 0; i < unreadMessages.getMessages().size(); i++) - for (int j = 0; j < getChats().size(); j++) - if (getChats().get(j).getRecipient().getID() == unreadMessages.getMessages().get(i).getMetadata().getSender()) { - getChats().get(j).appendMessage(unreadMessages.getMessages().get(i)); + for(Message message : unreadMessagesSync.getMessages()) + for(Chat chat : getChats()) + if(message.getMetadata().getSender() == chat.getRecipient().getID()) { + chat.appendMessage(message); + break; } } @@ -283,8 +286,8 @@ public class LocalDB { public void clearUnreadMessagesSync() { unreadMessagesSync.getMessages().clear(); } /** - * @return all saves {@link Chat} objects that list the client user as the - * client + * @return all saved {@link Chat} objects that list the client user as the + * sender * @since Envoy v0.1-alpha **/ public List getChats() { return chats; } From 6dad4eda08b0a9cb08bd2418d2bd18917b66f019 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 5 Dec 2019 15:13:19 +0100 Subject: [PATCH 14/22] Fixed Envoy logo loading for StatusTrayIcon --- src/main/java/envoy/client/ui/StatusTrayIcon.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 951fbd4..94d30d0 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -44,7 +44,8 @@ public class StatusTrayIcon implements EventHandler { public StatusTrayIcon() throws EnvoyException { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); - Image img = Toolkit.getDefaultToolkit().createImage(getClass().getResource("envoy_logo.png")); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png")); trayIcon = new TrayIcon(img, "Envoy Client"); trayIcon.setImageAutoSize(true); trayIcon.setToolTip("You are notified if you have unread messages."); From 2831b9a7a362c34125419bc0a09329ed49a73451 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 5 Dec 2019 15:42:20 +0100 Subject: [PATCH 15/22] Creating message notifications only if ChatWindow has lost focus StatusTray injects a WindowFocusListener into ChatWindow in its constructor and does only react to received messages if ChatWindow has currently lost focus. --- src/main/java/envoy/client/ui/Startup.java | 6 ++-- .../java/envoy/client/ui/StatusTrayIcon.java | 35 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index 7b35158..6333a60 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -63,9 +63,9 @@ public class Startup { EventQueue.invokeLater(() -> { try { - ChatWindow frame = new ChatWindow(client, localDB); - new StatusTrayIcon().show(); - frame.setVisible(true); + ChatWindow chatWindow = new ChatWindow(client, localDB); + new StatusTrayIcon(chatWindow).show(); + chatWindow.setVisible(true); } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 94d30d0..827296d 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -8,6 +8,8 @@ import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.TrayIcon.MessageType; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.util.HashSet; import java.util.Set; @@ -16,6 +18,7 @@ import envoy.client.event.EventBus; import envoy.client.event.EventHandler; import envoy.client.event.MessageCreationEvent; import envoy.exception.EnvoyException; +import envoy.schema.Message; /** * Project: envoy-client
@@ -34,6 +37,12 @@ public class StatusTrayIcon implements EventHandler { */ private TrayIcon trayIcon; + /** + * A received {@link Message} is only displayed as a system tray notification if + * this variable is set to {@code true}. + */ + private boolean displayMessages = false; + /** * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up * menu. @@ -41,11 +50,11 @@ public class StatusTrayIcon implements EventHandler { * @throws EnvoyException if the currently used OS does not support the System * Tray API */ - public StatusTrayIcon() throws EnvoyException { + public StatusTrayIcon(ChatWindow chatWindow) throws EnvoyException { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png")); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png")); trayIcon = new TrayIcon(img, "Envoy Client"); trayIcon.setImageAutoSize(true); trayIcon.setToolTip("You are notified if you have unread messages."); @@ -57,6 +66,22 @@ public class StatusTrayIcon implements EventHandler { popup.add(exitMenuItem); trayIcon.setPopupMenu(popup); + + // Only display messages if the chat window is not focused + chatWindow.addWindowFocusListener(new WindowAdapter() { + + @Override + public void windowGainedFocus(WindowEvent e) { + displayMessages = false; + } + + @Override + public void windowLostFocus(WindowEvent e) { + displayMessages = true; + } + }); + + // Start processing message events EventBus.getInstance().register(this); } @@ -83,7 +108,9 @@ public class StatusTrayIcon implements EventHandler { */ @Override public void handle(Event event) { - trayIcon.displayMessage("New message received", ((MessageCreationEvent) event).get().getContent().get(0).getText(), MessageType.INFO); + System.out.println("Message received. Displaying message: " + displayMessages); + if (displayMessages) + trayIcon.displayMessage("New message received", ((MessageCreationEvent) event).get().getContent().get(0).getText(), MessageType.INFO); } /** From 6cf8c462b9da60516051cc4cedc7049a7867f5e7 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 5 Dec 2019 16:10:28 +0100 Subject: [PATCH 16/22] Re-added logging to LocalDB to resolve merge conflict --- src/main/java/envoy/client/LocalDB.java | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index d15e62a..d3ed430 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -9,6 +9,7 @@ import java.io.ObjectOutputStream; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; @@ -43,6 +44,8 @@ public class LocalDB { private Sync sync = objectFactory.createSync(); private Sync readMessages = objectFactory.createSync(); + private static final Logger logger = Logger.getLogger(LocalDB.class.getSimpleName()); + /** * Constructs an empty local database. * @@ -85,13 +88,13 @@ public class LocalDB { localDB.createNewFile(); } catch (IOException e) { e.printStackTrace(); - System.err.println("unable to save the messages"); + logger.warning("unable to save the messages"); } try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(localDB))) { out.writeObject(chats); } catch (IOException ex) { ex.printStackTrace(); - System.err.println("unable to save the messages"); + logger.warning("unable to save the messages"); } } @@ -141,7 +144,7 @@ public class LocalDB { sync.getMessages().addAll(readMessages.getMessages()); readMessages.getMessages().clear(); - System.out.println(sync.getMessages().size()); + logger.info(String.format("Filled sync with %d messages.", sync.getMessages().size())); return sync; } @@ -189,24 +192,24 @@ public class LocalDB { break; case READ: // Update local Messages to state READ - System.out.println("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + logger.info("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + "was initialized to be set to READ in localDB."); for (int j = 0; j < getChats().size(); j++) { if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - System.out.println("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); + logger.info("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() .get(i) .getMetadata() .getMessageId()) { - System.out.println("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + logger.info("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + "was selected."); getChats().get(j) .getModel() .get(k) .getMetadata() .setState(returnSync.getMessages().get(i).getMetadata().getState()); - System.out.println( + logger.info( "Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); } } @@ -222,7 +225,7 @@ public class LocalDB { for (Chat chat : getChats()) if (user.getID() == chat.getRecipient().getID()) { chat.getRecipient().setStatus(user.getStatus()); - System.out.println(chat.getRecipient().getStatus()); + logger.info(chat.getRecipient().getStatus().toString()); } sync.getMessages().clear(); @@ -273,7 +276,7 @@ public class LocalDB { for (Chat chat : getChats()) for (int i = 0; i < chat.getModel().size(); i++) if (chat.getModel().get(i).getMetadata().getState() == MessageState.WAITING) { - System.out.println("Got Waiting Message"); + logger.info("Got Waiting Message"); sync.getMessages().add(chat.getModel().get(i)); } } From 8247e18fce4517bce46ea2a6e91e6fb5dcf3ca84 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 5 Dec 2019 16:17:33 +0100 Subject: [PATCH 17/22] Javadoc fixes and using superclass when injecting WindowFocusListener --- src/main/java/envoy/client/LocalDB.java | 4 +++- src/main/java/envoy/client/ui/StatusTrayIcon.java | 14 +++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index d3ed430..8caea75 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -49,7 +49,7 @@ public class LocalDB { /** * Constructs an empty local database. * - * @param client the user that is logged in with this client + * @param sender the user that is logged in with this client * @since Envoy v0.1-alpha */ public LocalDB(User sender) { @@ -118,7 +118,9 @@ public class LocalDB { * Creates a {@link Message} object serializable to XML. * * @param textContent The content (text) of the message + * @param recipient The recipient of the message * @return prepared {@link Message} object + * @since Envoy v0.1-alpha */ public Message createMessage(String textContent, User recipient) { Message.Metadata metaData = objectFactory.createMessageMetadata(); diff --git a/src/main/java/envoy/client/ui/StatusTrayIcon.java b/src/main/java/envoy/client/ui/StatusTrayIcon.java index 827296d..01cc8d7 100644 --- a/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -8,6 +8,7 @@ import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.TrayIcon.MessageType; +import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.HashSet; @@ -46,11 +47,14 @@ public class StatusTrayIcon implements EventHandler { /** * 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 v0.2-alpha */ - public StatusTrayIcon(ChatWindow chatWindow) throws EnvoyException { + public StatusTrayIcon(Window focusTarget) throws EnvoyException { if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); ClassLoader loader = Thread.currentThread().getContextClassLoader(); @@ -68,7 +72,7 @@ public class StatusTrayIcon implements EventHandler { trayIcon.setPopupMenu(popup); // Only display messages if the chat window is not focused - chatWindow.addWindowFocusListener(new WindowAdapter() { + focusTarget.addWindowFocusListener(new WindowAdapter() { @Override public void windowGainedFocus(WindowEvent e) { @@ -103,7 +107,7 @@ public class StatusTrayIcon implements EventHandler { /** * Notifies the user of a message by displaying a pop-up every time a new * message is received. - * + * * @since Envoy v0.2-alpha */ @Override @@ -116,7 +120,7 @@ public class StatusTrayIcon implements EventHandler { /** * The {@link StatusTrayIcon} only reacts to {@link MessageCreationEvent} * instances which signify newly received messages. - * + * * @return A set with the single element {@code MessageCreationEvent.class} * @since Envoy v0.2-alpha */ From 8f7e115219f5713f2f277eab42739a76c849aaee Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 7 Dec 2019 09:53:55 +0100 Subject: [PATCH 18/22] Implemented changes requested by @delvh --- .settings/org.eclipse.jdt.core.prefs | 99 +++++++++++++++++++ src/main/java/envoy/client/LocalDB.java | 67 ++++++------- src/main/java/envoy/client/ui/ChatWindow.java | 1 + 3 files changed, 131 insertions(+), 36 deletions(-) diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index cb635b1..262bd4f 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,107 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index 1989f89..3929315 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -140,6 +140,13 @@ public class LocalDB { return message; } + /** + * Creates a {@link Sync} object filled with the changes that occurred to the + * local database since the last synchronization. + * + * @param userId the ID of the user that is synchronized by this client + * @return {@link Sync} object filled with the current changes + */ public Sync fillSync(long userId) { addWaitingMessagesToSync(); @@ -150,6 +157,11 @@ public class LocalDB { return sync; } + /** + * Applies the changes carried by a {@link Sync} object to the local database + * + * @param returnSync the {@link Sync} object to apply + */ public void applySync(Sync returnSync) { for (int i = 0; i < returnSync.getMessages().size(); i++) { @@ -173,50 +185,33 @@ public class LocalDB { EventBus.getInstance().dispatch(new MessageCreationEvent(returnSync.getMessages().get(i))); } else { // Update Messages in localDB to state RECEIVED - for (int j = 0; j < getChats().size(); j++) { - if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { - if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() - .get(i) - .getMetadata() - .getMessageId()) { - // Update Message in LocalDB - getChats().get(j) - .getModel() - .get(k) - .getMetadata() - .setState(returnSync.getMessages().get(j).getMetadata().getState()); - } - } - } - } + for (Chat chat : getChats()) + if (chat.getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) + for (int j = 0; j < chat.getModel().getSize(); j++) + if (chat.getModel().get(j).getMetadata().getMessageId() == returnSync.getMessages() + .get(i) + .getMetadata() + .getMessageId()) + chat.getModel().get(j).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); } break; case READ: // Update local Messages to state READ logger.info("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + "was initialized to be set to READ in localDB."); - for (int j = 0; j < getChats().size(); j++) { - if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - logger.info("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); - for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { - if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() + for (Chat chat : getChats()) + if (chat.getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { + logger.info("Chat with: " + chat.getRecipient().getID() + "was selected."); + for (int k = 0; k < chat.getModel().getSize(); k++) + if (chat.getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() .get(i) .getMetadata() .getMessageId()) { - logger.info("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() - + "was selected."); - getChats().get(j) - .getModel() - .get(k) - .getMetadata() - .setState(returnSync.getMessages().get(i).getMetadata().getState()); - logger.info( - "Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); + logger.info("Message with ID: " + chat.getModel().get(k).getMetadata().getMessageId() + "was selected."); + chat.getModel().get(k).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); + logger.info("Message State is now: " + chat.getModel().get(k).getMetadata().getState()); } - } } - } break; } } @@ -242,9 +237,9 @@ public class LocalDB { * @since Envoy v0.1-alpha */ public void addUnreadMessagesToLocalDB() { - for(Message message : unreadMessagesSync.getMessages()) - for(Chat chat : getChats()) - if(message.getMetadata().getSender() == chat.getRecipient().getID()) { + for (Message message : unreadMessagesSync.getMessages()) + for (Chat chat : getChats()) + if (message.getMetadata().getSender() == chat.getRecipient().getID()) { chat.appendMessage(message); break; } diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index cefe886..e6e6872 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -257,6 +257,7 @@ public class ChatWindow extends JFrame { private void postMessage(JList messageList) { if (!client.hasRecipient()) { JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); + return; } if (!messageEnterTextArea.getText().isEmpty()) try { From c79500acdecf5d35945129fb86c2f5f36f3cb263 Mon Sep 17 00:00:00 2001 From: delvh Date: Sat, 7 Dec 2019 10:44:25 +0100 Subject: [PATCH 19/22] Improved logging and code readability --- src/main/java/envoy/client/Client.java | 20 +- src/main/java/envoy/client/Config.java | 2 +- src/main/java/envoy/client/LocalDB.java | 45 +- src/main/java/envoy/client/ui/ChatWindow.java | 704 +++++++++--------- .../envoy/client/ui/MessageListRenderer.java | 101 ++- .../java/envoy/client/ui/SettingsScreen.java | 10 +- src/main/java/envoy/client/ui/Startup.java | 23 +- .../envoy/client/ui/UserListRenderer.java | 119 ++- 8 files changed, 497 insertions(+), 527 deletions(-) diff --git a/src/main/java/envoy/client/Client.java b/src/main/java/envoy/client/Client.java index 5b9a2b8..4c775e1 100644 --- a/src/main/java/envoy/client/Client.java +++ b/src/main/java/envoy/client/Client.java @@ -36,14 +36,14 @@ public class Client { this.config = config; sender = getUser(username); - logger.info("ID: " + sender.getID()); + logger.info("ID: " + sender.getID()); } private R post(String uri, T body, Class responseBodyClass) { - javax.ws.rs.client.Client client = ClientBuilder.newClient(); - WebTarget target = client.target(uri); - Response response = target.request().post(Entity.entity(body, "application/xml")); - R responseBody = response.readEntity(responseBodyClass); + javax.ws.rs.client.Client client = ClientBuilder.newClient(); + WebTarget target = client.target(uri); + Response response = target.request().post(Entity.entity(body, "application/xml")); + R responseBody = response.readEntity(responseBodyClass); response.close(); client.close(); @@ -133,7 +133,9 @@ public class Client { * Updating UserStatus of all users in LocalDB. (Server sends all users with * their updated UserStatus to the client.)
* - * @param userId + * @param userId the id of the {@link Client} who sends the {@link Sync} + * @param sync the {@link Sync} to send + * @return a sync * @since Envoy v0.1-alpha */ public Sync sendSync(long userId, Sync sync) { @@ -168,16 +170,16 @@ public class Client { public User getRecipient() { return recipient; } /** - * Sets the recipient. + * Sets the recipient. + * * @param recipient - the recipient to set * @since Envoy v0.1-alpha */ public void setRecipient(User recipient) { this.recipient = recipient; } - /** + /** * @return true, if a recipient is selected * @since Envoy v0.1-alpha */ public boolean hasRecipient() { return recipient != null; } } - diff --git a/src/main/java/envoy/client/Config.java b/src/main/java/envoy/client/Config.java index 545df2f..116a341 100644 --- a/src/main/java/envoy/client/Config.java +++ b/src/main/java/envoy/client/Config.java @@ -114,7 +114,7 @@ public class Config { * Changes the default local database. * Exclusively intended for development purposes. * - * @param the file containing the local database + * @param localDB the file containing the local database * @since Envoy v0.1-alpha **/ public void setLocalDB(File localDB) { this.localDB = localDB; } diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java index b1668ad..ba5e3a3 100644 --- a/src/main/java/envoy/client/LocalDB.java +++ b/src/main/java/envoy/client/LocalDB.java @@ -39,7 +39,7 @@ public class LocalDB { private DatatypeFactory datatypeFactory; private static final Logger logger = Logger.getLogger(LocalDB.class.getSimpleName()); - + private Sync unreadMessagesSync = objectFactory.createSync(); private Sync sync = objectFactory.createSync(); private Sync readMessages = objectFactory.createSync(); @@ -80,19 +80,13 @@ public class LocalDB { * @throws IOException if something went wrong during saving * @since Envoy v0.1-alpha */ - public void saveToLocalDB() { - try { - localDB.getParentFile().mkdirs(); - localDB.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - logger.warning("unable to save the messages"); - } + public void saveToLocalDB() throws IOException { + localDB.getParentFile().mkdirs(); + localDB.createNewFile(); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(localDB))) { out.writeObject(chats); } catch (IOException ex) { - ex.printStackTrace(); - logger.warning("unable to save the messages"); + throw ex; } } @@ -116,7 +110,7 @@ public class LocalDB { * Creates a {@link Message} object serializable to XML. * * @param textContent The content (text) of the message - * @param recipient The recipient of the message + * @param recipient The recipient of the message * @return prepared {@link Message} object * @since Envoy v0.1-alpha */ @@ -144,7 +138,7 @@ public class LocalDB { sync.getMessages().addAll(readMessages.getMessages()); readMessages.getMessages().clear(); - logger.info(String.format("Filled sync with %d messages.", sync.getMessages().size())); + logger.finest(String.format("Filled sync with %d messages.", sync.getMessages().size())); return sync; } @@ -189,28 +183,20 @@ public class LocalDB { if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0 && returnSync.getMessages().get(i).getMetadata().getState() == MessageState.READ) { // Update local Messages to state READ - logger.info("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + logger.finest("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + "was initialized to be set to READ in localDB."); for (int j = 0; j < getChats().size(); j++) { - if (getChats().get(j) - .getRecipient() - .getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { - logger.info("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); + if (getChats().get(j).getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { + logger.fine("Chat with: " + getChats().get(j).getRecipient().getID() + "was selected."); for (int k = 0; k < getChats().get(j).getModel().getSize(); k++) { if (getChats().get(j).getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() .get(i) .getMetadata() .getMessageId()) { - logger.info("Message with ID: " - + getChats().get(j).getModel().get(k).getMetadata().getMessageId() - + "was selected."); - getChats().get(j) - .getModel() - .get(k) - .getMetadata() - .setState(returnSync.getMessages().get(i).getMetadata().getState()); - logger.info("Message State is now: " - + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); + logger + .finest("Message with ID: " + getChats().get(j).getModel().get(k).getMetadata().getMessageId() + "was selected."); + getChats().get(j).getModel().get(k).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); + logger.finest("Message State is now: " + getChats().get(j).getModel().get(k).getMetadata().getState().toString()); } } } @@ -237,7 +223,6 @@ public class LocalDB { * Adds the unread messages returned from the server in the latest sync to the * right chats in the LocalDB. * - * @param localDB * @since Envoy v0.1-alpha */ public void addUnreadMessagesToLocalDB() { @@ -255,7 +240,7 @@ public class LocalDB { *
* Adds these messages to the {@code readMessages} {@link Sync} object. * - * @param currentChat + * @param currentChat the {@link Chat} that was just opened * @since Envoy v0.1-alpha */ public void setMessagesToRead(Chat currentChat) { diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index 39a907f..e36a3f0 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -1,352 +1,352 @@ -package envoy.client.ui; - -import java.awt.Color; -import java.awt.ComponentOrientation; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.logging.Logger; - -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextPane; -import javax.swing.ListSelectionModel; -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.border.EmptyBorder; - -import envoy.client.Chat; -import envoy.client.Client; -import envoy.client.Config; -import envoy.client.LocalDB; -import envoy.schema.Message; -import envoy.schema.Sync; -import envoy.schema.User; - -/** - * Project: envoy-client
- * File: ChatWindow.java
- * Created: 28 Sep 2019
- * - * @author Kai S. K. Engelbart - * @author Maximilian Käfer - * @author Leon Hofmeister - * @since Envoy v0.1-alpha - */ -public class ChatWindow extends JFrame { - - private static final long serialVersionUID = 6865098428255463649L; - - private JPanel contentPane = new JPanel(); - - private Client client; - private LocalDB localDB; - - private JList userList = new JList<>(); - private Chat currentChat; - - private JTextArea messageEnterTextArea; - - private static final Logger logger = Logger.getLogger(ChatWindow.class.getSimpleName()); - - public ChatWindow(Client client, LocalDB localDB) { - this.client = client; - this.localDB = localDB; - - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setBounds(100, 100, 600, 800); - setTitle("Envoy"); - setLocationRelativeTo(null); - - // Save chats when window closes - addWindowListener(new WindowAdapter() { - - @Override - public void windowClosing(WindowEvent e) { localDB.saveToLocalDB(); } - }); - - contentPane.setBackground(new Color(0, 0, 0)); - contentPane.setForeground(Color.white); - contentPane.setBorder(new EmptyBorder(0, 5, 0, 0)); - setContentPane(contentPane); - GridBagLayout gbl_contentPane = new GridBagLayout(); - gbl_contentPane.columnWidths = new int[] { 1, 1, 1 }; - gbl_contentPane.rowHeights = new int[] { 1, 1, 1 }; - gbl_contentPane.columnWeights = new double[] { 0.3, 1.0, 0.1 }; - gbl_contentPane.rowWeights = new double[] { 0.05, 1.0, 0.07 }; - contentPane.setLayout(gbl_contentPane); - - JList messageList = new JList<>(); - messageList.setCellRenderer(new MessageListRenderer()); - - messageList.setFocusTraversalKeysEnabled(false); - messageList.setSelectionForeground(new Color(255, 255, 255)); - messageList.setSelectionBackground(new Color(102, 0, 153)); - messageList.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); - messageList.setForeground(new Color(255, 255, 255)); - messageList.setBackground(new Color(51, 51, 51)); - - DefaultListModel messageListModel = new DefaultListModel<>(); - messageList.setModel(messageListModel); - messageList.setFont(new Font("Arial", Font.PLAIN, 17)); - messageList.setFixedCellHeight(60); - messageList.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JScrollPane scrollPane = new JScrollPane(); - scrollPane.setForeground(new Color(0, 0, 0)); - scrollPane.setBackground(new Color(51, 51, 51)); - scrollPane.setViewportView(messageList); - scrollPane.setBorder(null); - - GridBagConstraints gbc_scrollPane = new GridBagConstraints(); - gbc_scrollPane.fill = GridBagConstraints.BOTH; - gbc_scrollPane.gridwidth = 2; - gbc_scrollPane.gridx = 1; - gbc_scrollPane.gridy = 1; - - gbc_scrollPane.insets = new Insets(0, 10, 10, 10); - - contentPane.add(scrollPane, gbc_scrollPane); - - // Message enter field - messageEnterTextArea = new JTextArea(); - messageEnterTextArea.addKeyListener(new KeyAdapter() { - - @Override - public void keyReleased(KeyEvent e) { - - if (e.getKeyCode() == KeyEvent.VK_ENTER && ((SettingsScreen.enterToSend && e.getModifiersEx() == 0) - || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) { - - postMessage(messageList); - } - - } - }); - // Checks for changed Message - messageEnterTextArea.setWrapStyleWord(true); - messageEnterTextArea.setCaretColor(new Color(255, 255, 255)); - messageEnterTextArea.setForeground(new Color(255, 255, 255)); - messageEnterTextArea.setBackground(new Color(51, 51, 51)); - messageEnterTextArea.setLineWrap(true); - messageEnterTextArea.setBorder(null); - messageEnterTextArea.setFont(new Font("Arial", Font.PLAIN, 17)); - messageEnterTextArea.setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc_messageEnterTextfield = new GridBagConstraints(); - gbc_messageEnterTextfield.fill = GridBagConstraints.BOTH; - gbc_messageEnterTextfield.gridx = 1; - gbc_messageEnterTextfield.gridy = 2; - - gbc_messageEnterTextfield.insets = new Insets(10, 10, 10, 10); - - contentPane.add(messageEnterTextArea, gbc_messageEnterTextfield); - - // Post Button - JButton postButton = new JButton("Post"); - postButton.setForeground(new Color(255, 255, 255)); - postButton.setBackground(new Color(102, 51, 153)); - postButton.setBorderPainted(false); - - GridBagConstraints gbc_moveSelectionPostButton = new GridBagConstraints(); - - gbc_moveSelectionPostButton.fill = GridBagConstraints.BOTH; - gbc_moveSelectionPostButton.gridx = 2; - gbc_moveSelectionPostButton.gridy = 2; - - gbc_moveSelectionPostButton.insets = new Insets(10, 10, 10, 10); - - postButton.addActionListener((evt) -> { postMessage(messageList); }); - contentPane.add(postButton, gbc_moveSelectionPostButton); - - // Settings Button - JButton settingsButton = new JButton("Settings"); - settingsButton.setForeground(new Color(255, 255, 255)); - settingsButton.setBackground(new Color(102, 51, 153)); - settingsButton.setBorderPainted(false); - - GridBagConstraints gbc_moveSelectionSettingsButton = new GridBagConstraints(); - - gbc_moveSelectionSettingsButton.fill = GridBagConstraints.BOTH; - gbc_moveSelectionSettingsButton.gridx = 2; - gbc_moveSelectionSettingsButton.gridy = 0; - - gbc_moveSelectionSettingsButton.insets = new Insets(10, 10, 10, 10); - - settingsButton.addActionListener((evt) -> { - try { - SettingsScreen.open(localDB.getUser().getName()); - } catch (Exception e) { - SettingsScreen.open(); - logger.warning("An error occured while opening the settings screen: " + e); - e.printStackTrace(); - } - }); - contentPane.add(settingsButton, gbc_moveSelectionSettingsButton); - - // Partner name display - JTextPane textPane = new JTextPane(); - textPane.setBackground(new Color(0, 0, 0)); - textPane.setForeground(new Color(255, 255, 255)); - - textPane.setFont(new Font("Arial", Font.PLAIN, 20)); - - GridBagConstraints gbc_partnerName = new GridBagConstraints(); - gbc_partnerName.fill = GridBagConstraints.HORIZONTAL; - gbc_partnerName.gridx = 1; - gbc_partnerName.gridy = 0; - - gbc_partnerName.insets = new Insets(0, 10, 0, 10); - contentPane.add(textPane, gbc_partnerName); - - userList.setCellRenderer(new UserListRenderer()); - userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - userList.addListSelectionListener((listSelectionEvent) -> { - if (!listSelectionEvent.getValueIsAdjusting()) { - @SuppressWarnings("unchecked") - final JList selectedUserList = (JList) listSelectionEvent.getSource(); - final User user = selectedUserList.getSelectedValue(); - client.setRecipient(user); - - currentChat = localDB.getChats() - .stream() - .filter(chat -> chat.getRecipient().getID() == user.getID()) - .findFirst() - .get(); - - // Set all unread messages in the chat to read - readCurrentChat(); - - client.setRecipient(user); - - textPane.setText(currentChat.getRecipient().getName()); - - messageList.setModel(currentChat.getModel()); - contentPane.revalidate(); - } - }); - - userList.setSelectionForeground(new Color(255, 255, 255)); - userList.setSelectionBackground(new Color(102, 0, 153)); - userList.setForeground(new Color(255, 255, 255)); - userList.setBackground(new Color(51, 51, 51)); - userList.setFont(new Font("Arial", Font.PLAIN, 17)); - userList.setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc_userList = new GridBagConstraints(); - gbc_userList.fill = GridBagConstraints.VERTICAL; - gbc_userList.gridx = 0; - gbc_userList.gridy = 1; - gbc_userList.anchor = GridBagConstraints.PAGE_START; - gbc_userList.insets = new Insets(0, 0, 10, 0); - - contentPane.add(userList, gbc_userList); - contentPane.revalidate(); - - loadUsersAndChats(); - startSyncThread(Config.getInstance().getSyncTimeout()); - - contentPane.revalidate(); - } - - private void postMessage(JList messageList) { - if (!client.hasRecipient()) { - JOptionPane.showMessageDialog(this, - "Please select a recipient!", - "Cannot send message", - JOptionPane.INFORMATION_MESSAGE); - } - - if (!messageEnterTextArea.getText().isEmpty()) try { - - // Create and send message object - final Message message = localDB.createMessage(messageEnterTextArea.getText(), currentChat.getRecipient()); - currentChat.appendMessage(message); - messageList.setModel(currentChat.getModel()); - - // Clear text field - messageEnterTextArea.setText(""); - contentPane.revalidate(); - } catch (Exception e) { - JOptionPane.showMessageDialog(this, - "An exception occured while sending a message. See the log for more details.", - "Exception occured", - JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - } - } - - /** - * Initializes the elements of the user list by downloading them from the - * server. - * - * @since Envoy v0.1-alpha - */ - private void loadUsersAndChats() { - new Thread(() -> { - Sync users = client.getUsersListXml(); - DefaultListModel userListModel = new DefaultListModel<>(); - users.getUsers().forEach(user -> { - userListModel.addElement(user); - - // Check if user exists in local DB - if (localDB.getChats().stream().filter(c -> c.getRecipient().getID() == user.getID()).count() == 0) - localDB.getChats().add(new Chat(user)); - }); - SwingUtilities.invokeLater(() -> userList.setModel(userListModel)); - }).start(); - } - - /** - * Updates the data model and the UI repeatedly after a certain amount of - * time. - * - * @param timeout the amount of time that passes between two requests sent to - * the server - * @since Envoy v0.1-alpha - */ - private void startSyncThread(int timeout) { - new Timer(timeout, (evt) -> { - new Thread(() -> { - - // Synchronize - localDB.applySync( - client.sendSync(client.getSender().getID(), localDB.fillSync(client.getSender().getID()))); - - // Process unread messages - localDB.addUnreadMessagesToLocalDB(); - localDB.clearUnreadMessagesSync(); - - // Mark unread messages as read when they are in the current chat - readCurrentChat(); - - // Update UI - SwingUtilities - .invokeLater(() -> { updateUserStates(); contentPane.revalidate(); contentPane.repaint(); }); - }).start(); - }).start(); - } - - private void updateUserStates() { - for (int i = 0; i < userList.getModel().getSize(); i++) - for (int j = 0; j < localDB.getChats().size(); j++) - if (userList.getModel().getElementAt(i).getID() == localDB.getChats().get(j).getRecipient().getID()) - userList.getModel().getElementAt(i).setStatus(localDB.getChats().get(j).getRecipient().getStatus()); - } - - /** - * Marks messages in the current chat as {@code READ}. - */ - private void readCurrentChat() { if (currentChat != null) { localDB.setMessagesToRead(currentChat); } } -} +package envoy.client.ui; + +import java.awt.Color; +import java.awt.ComponentOrientation; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextPane; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.border.EmptyBorder; + +import envoy.client.Chat; +import envoy.client.Client; +import envoy.client.Config; +import envoy.client.LocalDB; +import envoy.schema.Message; +import envoy.schema.Sync; +import envoy.schema.User; + +/** + * Project: envoy-client
+ * File: ChatWindow.java
+ * Created: 28 Sep 2019
+ * + * @author Kai S. K. Engelbart + * @author Maximilian Käfer + * @author Leon Hofmeister + * @since Envoy v0.1-alpha + */ +public class ChatWindow extends JFrame { + + private static final long serialVersionUID = 6865098428255463649L; + + private JPanel contentPane = new JPanel(); + + private Client client; + private LocalDB localDB; + + private JList userList = new JList<>(); + private Chat currentChat; + + private JTextArea messageEnterTextArea; + + private static final Logger logger = Logger.getLogger(ChatWindow.class.getSimpleName()); + + public ChatWindow(Client client, LocalDB localDB) { + this.client = client; + this.localDB = localDB; + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setBounds(100, 100, 600, 800); + setTitle("Envoy"); + setLocationRelativeTo(null); + + // Save chats when window closes + addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent evt) { + try { + localDB.saveToLocalDB(); + } catch (IOException e1) { + e1.printStackTrace(); + logger.log(Level.WARNING, "Unable to save the messages", e1); + } + } + }); + + contentPane.setBackground(new Color(0, 0, 0)); + contentPane.setForeground(Color.white); + contentPane.setBorder(new EmptyBorder(0, 5, 0, 0)); + setContentPane(contentPane); + GridBagLayout gbl_contentPane = new GridBagLayout(); + gbl_contentPane.columnWidths = new int[] { 1, 1, 1 }; + gbl_contentPane.rowHeights = new int[] { 1, 1, 1 }; + gbl_contentPane.columnWeights = new double[] { 0.3, 1.0, 0.1 }; + gbl_contentPane.rowWeights = new double[] { 0.05, 1.0, 0.07 }; + contentPane.setLayout(gbl_contentPane); + + JList messageList = new JList<>(); + messageList.setCellRenderer(new MessageListRenderer()); + + messageList.setFocusTraversalKeysEnabled(false); + messageList.setSelectionForeground(new Color(255, 255, 255)); + messageList.setSelectionBackground(new Color(102, 0, 153)); + messageList.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + messageList.setForeground(new Color(255, 255, 255)); + messageList.setBackground(new Color(51, 51, 51)); + + DefaultListModel messageListModel = new DefaultListModel<>(); + messageList.setModel(messageListModel); + messageList.setFont(new Font("Arial", Font.PLAIN, 17)); + messageList.setFixedCellHeight(60); + messageList.setBorder(new EmptyBorder(5, 5, 5, 5)); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setForeground(new Color(0, 0, 0)); + scrollPane.setBackground(new Color(51, 51, 51)); + scrollPane.setViewportView(messageList); + scrollPane.setBorder(null); + + GridBagConstraints gbc_scrollPane = new GridBagConstraints(); + gbc_scrollPane.fill = GridBagConstraints.BOTH; + gbc_scrollPane.gridwidth = 2; + gbc_scrollPane.gridx = 1; + gbc_scrollPane.gridy = 1; + + gbc_scrollPane.insets = new Insets(0, 10, 10, 10); + + contentPane.add(scrollPane, gbc_scrollPane); + + // Message enter field + messageEnterTextArea = new JTextArea(); + messageEnterTextArea.addKeyListener(new KeyAdapter() { + + @Override + public void keyReleased(KeyEvent e) { + + if (e.getKeyCode() == KeyEvent.VK_ENTER + && ((SettingsScreen.enterToSend && e.getModifiersEx() == 0) || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) { + + postMessage(messageList); + } + + } + }); + // Checks for changed Message + messageEnterTextArea.setWrapStyleWord(true); + messageEnterTextArea.setCaretColor(new Color(255, 255, 255)); + messageEnterTextArea.setForeground(new Color(255, 255, 255)); + messageEnterTextArea.setBackground(new Color(51, 51, 51)); + messageEnterTextArea.setLineWrap(true); + messageEnterTextArea.setBorder(null); + messageEnterTextArea.setFont(new Font("Arial", Font.PLAIN, 17)); + messageEnterTextArea.setBorder(new EmptyBorder(5, 5, 5, 5)); + + GridBagConstraints gbc_messageEnterTextfield = new GridBagConstraints(); + gbc_messageEnterTextfield.fill = GridBagConstraints.BOTH; + gbc_messageEnterTextfield.gridx = 1; + gbc_messageEnterTextfield.gridy = 2; + + gbc_messageEnterTextfield.insets = new Insets(10, 10, 10, 10); + + contentPane.add(messageEnterTextArea, gbc_messageEnterTextfield); + + // Post Button + JButton postButton = new JButton("Post"); + postButton.setForeground(new Color(255, 255, 255)); + postButton.setBackground(new Color(102, 51, 153)); + postButton.setBorderPainted(false); + + GridBagConstraints gbc_moveSelectionPostButton = new GridBagConstraints(); + + gbc_moveSelectionPostButton.fill = GridBagConstraints.BOTH; + gbc_moveSelectionPostButton.gridx = 2; + gbc_moveSelectionPostButton.gridy = 2; + + gbc_moveSelectionPostButton.insets = new Insets(10, 10, 10, 10); + + postButton.addActionListener((evt) -> { postMessage(messageList); }); + contentPane.add(postButton, gbc_moveSelectionPostButton); + + // Settings Button + JButton settingsButton = new JButton("Settings"); + settingsButton.setForeground(new Color(255, 255, 255)); + settingsButton.setBackground(new Color(102, 51, 153)); + settingsButton.setBorderPainted(false); + + GridBagConstraints gbc_moveSelectionSettingsButton = new GridBagConstraints(); + + gbc_moveSelectionSettingsButton.fill = GridBagConstraints.BOTH; + gbc_moveSelectionSettingsButton.gridx = 2; + gbc_moveSelectionSettingsButton.gridy = 0; + + gbc_moveSelectionSettingsButton.insets = new Insets(10, 10, 10, 10); + + settingsButton.addActionListener((evt) -> { + try { + SettingsScreen.open(localDB.getUser().getName()); + } catch (Exception e) { + SettingsScreen.open(); + logger.log(Level.WARNING, "An error occured while opening the settings screen", e); + e.printStackTrace(); + } + }); + contentPane.add(settingsButton, gbc_moveSelectionSettingsButton); + + // Partner name display + JTextPane textPane = new JTextPane(); + textPane.setBackground(new Color(0, 0, 0)); + textPane.setForeground(new Color(255, 255, 255)); + + textPane.setFont(new Font("Arial", Font.PLAIN, 20)); + + GridBagConstraints gbc_partnerName = new GridBagConstraints(); + gbc_partnerName.fill = GridBagConstraints.HORIZONTAL; + gbc_partnerName.gridx = 1; + gbc_partnerName.gridy = 0; + + gbc_partnerName.insets = new Insets(0, 10, 0, 10); + contentPane.add(textPane, gbc_partnerName); + + userList.setCellRenderer(new UserListRenderer()); + userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + userList.addListSelectionListener((listSelectionEvent) -> { + if (!listSelectionEvent.getValueIsAdjusting()) { + @SuppressWarnings("unchecked") + final JList selectedUserList = (JList) listSelectionEvent.getSource(); + final User user = selectedUserList.getSelectedValue(); + client.setRecipient(user); + + currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getID() == user.getID()).findFirst().get(); + + // Set all unread messages in the chat to read + readCurrentChat(); + + client.setRecipient(user); + + textPane.setText(currentChat.getRecipient().getName()); + + messageList.setModel(currentChat.getModel()); + contentPane.revalidate(); + } + }); + + userList.setSelectionForeground(new Color(255, 255, 255)); + userList.setSelectionBackground(new Color(102, 0, 153)); + userList.setForeground(new Color(255, 255, 255)); + userList.setBackground(new Color(51, 51, 51)); + userList.setFont(new Font("Arial", Font.PLAIN, 17)); + userList.setBorder(new EmptyBorder(5, 5, 5, 5)); + + GridBagConstraints gbc_userList = new GridBagConstraints(); + gbc_userList.fill = GridBagConstraints.VERTICAL; + gbc_userList.gridx = 0; + gbc_userList.gridy = 1; + gbc_userList.anchor = GridBagConstraints.PAGE_START; + gbc_userList.insets = new Insets(0, 0, 10, 0); + + contentPane.add(userList, gbc_userList); + contentPane.revalidate(); + + loadUsersAndChats(); + startSyncThread(Config.getInstance().getSyncTimeout()); + + contentPane.revalidate(); + } + + private void postMessage(JList messageList) { + if (!client.hasRecipient()) { + JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); + } + + if (!messageEnterTextArea.getText().isEmpty()) try { + + // Create and send message object + final Message message = localDB.createMessage(messageEnterTextArea.getText(), currentChat.getRecipient()); + currentChat.appendMessage(message); + messageList.setModel(currentChat.getModel()); + + // Clear text field + messageEnterTextArea.setText(""); + contentPane.revalidate(); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + "An exception occured while sending a message. See the log for more details.", + "Exception occured", + JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + } + } + + /** + * Initializes the elements of the user list by downloading them from the + * server. + * + * @since Envoy v0.1-alpha + */ + private void loadUsersAndChats() { + new Thread(() -> { + Sync users = client.getUsersListXml(); + DefaultListModel userListModel = new DefaultListModel<>(); + users.getUsers().forEach(user -> { + userListModel.addElement(user); + + // Check if user exists in local DB + if (localDB.getChats().stream().filter(c -> c.getRecipient().getID() == user.getID()).count() == 0) + localDB.getChats().add(new Chat(user)); + }); + SwingUtilities.invokeLater(() -> userList.setModel(userListModel)); + }).start(); + } + + /** + * Updates the data model and the UI repeatedly after a certain amount of + * time. + * + * @param timeout the amount of time that passes between two requests sent to + * the server + * @since Envoy v0.1-alpha + */ + private void startSyncThread(int timeout) { + new Timer(timeout, (evt) -> { + new Thread(() -> { + + // Synchronize + localDB.applySync(client.sendSync(client.getSender().getID(), localDB.fillSync(client.getSender().getID()))); + + // Process unread messages + localDB.addUnreadMessagesToLocalDB(); + localDB.clearUnreadMessagesSync(); + + // Mark unread messages as read when they are in the current chat + readCurrentChat(); + + // Update UI + SwingUtilities.invokeLater(() -> { updateUserStates(); contentPane.revalidate(); contentPane.repaint(); }); + }).start(); + }).start(); + } + + private void updateUserStates() { + for (int i = 0; i < userList.getModel().getSize(); i++) + for (int j = 0; j < localDB.getChats().size(); j++) + if (userList.getModel().getElementAt(i).getID() == localDB.getChats().get(j).getRecipient().getID()) + userList.getModel().getElementAt(i).setStatus(localDB.getChats().get(j).getRecipient().getStatus()); + } + + /** + * Marks messages in the current chat as {@code READ}. + */ + private void readCurrentChat() { if (currentChat != null) { localDB.setMessagesToRead(currentChat); } } +} diff --git a/src/main/java/envoy/client/ui/MessageListRenderer.java b/src/main/java/envoy/client/ui/MessageListRenderer.java index 9053c6c..3e6eb23 100644 --- a/src/main/java/envoy/client/ui/MessageListRenderer.java +++ b/src/main/java/envoy/client/ui/MessageListRenderer.java @@ -1,54 +1,49 @@ -package envoy.client.ui; - -import java.awt.Component; -import java.text.SimpleDateFormat; - -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.ListCellRenderer; - -import envoy.schema.Message; - -/** - * Defines how a message is displayed.
- *
- * - * Project: envoy-client
- * File: UserListRenderer.java
- * Created: 19 Oct 2019
- * - * @author Kai S. K. Engelbart - * @author Maximilian Käfer - * @since Envoy v0.1-alpha - */ -public class MessageListRenderer extends JLabel implements ListCellRenderer { - - private static final long serialVersionUID = 5164417379767181198L; - - @Override - public Component getListCellRendererComponent(JList list, Message value, int index, - boolean isSelected, boolean cellHasFocus) { - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - } - - setOpaque(true); - - final String text = value.getContent().get(0).getText(); - final String state = value.getMetadata().getState().toString(); - final String date = value.getMetadata().getDate() == null ? "" - : new SimpleDateFormat("dd.MM.yyyy HH:mm ") - .format(value.getMetadata().getDate().toGregorianCalendar().getTime()); - - setText(String.format( - "

%s

%s :%s", - date, - text, - state)); - return this; - } +package envoy.client.ui; + +import java.awt.Component; +import java.text.SimpleDateFormat; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +import envoy.schema.Message; + +/** + * Defines how a message is displayed.
+ *
+ * + * Project: envoy-client
+ * File: UserListRenderer.java
+ * Created: 19 Oct 2019
+ * + * @author Kai S. K. Engelbart + * @author Maximilian Käfer + * @since Envoy v0.1-alpha + */ +public class MessageListRenderer extends JLabel implements ListCellRenderer { + + private static final long serialVersionUID = 5164417379767181198L; + + @Override + public Component getListCellRendererComponent(JList list, Message value, int index, boolean isSelected, boolean cellHasFocus) { + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + setOpaque(true); + + final String text = value.getContent().get(0).getText(); + final String state = value.getMetadata().getState().toString(); + final String date = value.getMetadata().getDate() == null ? "" + : new SimpleDateFormat("dd.MM.yyyy HH:mm ").format(value.getMetadata().getDate().toGregorianCalendar().getTime()); + + setText(String + .format("

%s

%s :%s", date, text, state)); + return this; + } } \ No newline at end of file diff --git a/src/main/java/envoy/client/ui/SettingsScreen.java b/src/main/java/envoy/client/ui/SettingsScreen.java index 7d5de28..08aa9cb 100644 --- a/src/main/java/envoy/client/ui/SettingsScreen.java +++ b/src/main/java/envoy/client/ui/SettingsScreen.java @@ -41,7 +41,6 @@ public class SettingsScreen extends JDialog { * It personalises the screen more. * * @param username The name of the User - * @param Email The Email that is associated with that Account * @since Envoy v0.1-alpha */ public static void open(String username) {// , String Email) {AUSKLAMMERN, WENN ANMELDUNG PER @@ -101,7 +100,6 @@ public class SettingsScreen extends JDialog { * It personalises the screen more. * * @param Username The name of the User - * @param Email The Email that is associated with that Account * @since Envoy v0.1-alpha */ public SettingsScreen(String Username) {// , String Email, String hashedPwd) {AUSKLAMMERN, WENN ANMELDUNG PER EMAIL @@ -145,10 +143,10 @@ public class SettingsScreen extends JDialog { public static boolean isEnterToSend() { return enterToSend; } /** - * @param enterToSend
- * toggles whether a message should be sent via - *
- * buttonpress "enter" or "ctrl"+"enter" + * @param enterForSend
+ * toggles whether a message should be sent via + *
+ * buttonpress "enter" or "ctrl"+"enter" * @since Envoy v0.1-alpha */ public static void setEnterToSend(boolean enterForSend) { enterToSend = enterForSend; } diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java index 4d6a198..5b8674f 100644 --- a/src/main/java/envoy/client/ui/Startup.java +++ b/src/main/java/envoy/client/ui/Startup.java @@ -26,11 +26,11 @@ import envoy.exception.EnvoyException; */ public class Startup { - private static final Logger logger = Logger.getLogger(Client.class.getSimpleName()); - + private static final Logger logger = Logger.getLogger(Startup.class.getSimpleName()); + public static void main(String[] args) { logger.setLevel(Level.ALL); - + Config config = Config.getInstance(); // Load the configuration from client.properties first @@ -44,30 +44,29 @@ public class Startup { } // Override configuration values with command line arguments - if (args.length > 0) - config.load(args); + if (args.length > 0) config.load(args); if (!config.isInitialized()) { - logger.warning("Server or port are not defined. Exiting..."); - JOptionPane.showMessageDialog(null, "Error loading configuration values.", "Configuration error", - JOptionPane.ERROR_MESSAGE); + logger.severe("Server or port are not defined. Exiting..."); + JOptionPane.showMessageDialog(null, "Error loading configuration values.", "Configuration error", JOptionPane.ERROR_MESSAGE); System.exit(1); } String userName = JOptionPane.showInputDialog("Please enter your username"); if (userName == null || userName.isEmpty()) { - logger.warning("User name is not set or empty. Exiting..."); + logger.severe("User name is not set or empty. Exiting..."); System.exit(1); } - Client client = new Client(config, userName); - LocalDB localDB = new LocalDB(client.getSender()); + Client client = new Client(config, userName); + LocalDB localDB = new LocalDB(client.getSender()); try { localDB.initializeDBFile(config.getLocalDB()); } catch (EnvoyException e) { e.printStackTrace(); JOptionPane.showMessageDialog(null, "Error while loading local database: " + e.toString() + "\nChats will not be stored locally.", - "Local DB error", JOptionPane.WARNING_MESSAGE); + "Local DB error", + JOptionPane.WARNING_MESSAGE); } EventQueue.invokeLater(() -> { diff --git a/src/main/java/envoy/client/ui/UserListRenderer.java b/src/main/java/envoy/client/ui/UserListRenderer.java index 40bb2ad..e794b1f 100644 --- a/src/main/java/envoy/client/ui/UserListRenderer.java +++ b/src/main/java/envoy/client/ui/UserListRenderer.java @@ -1,65 +1,56 @@ -package envoy.client.ui; - -import java.awt.Component; - -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.ListCellRenderer; - -import envoy.schema.User; -import envoy.schema.User.UserStatus; - -/** - * Defines how the {@code UserList} is displayed. - * - * Project: envoy-client
- * File: UserListRenderer.java
- * Created: 12 Oct 2019
- * - * @author Kai S. K. Engelbart - * @author Maximilian Käfer - * @since Envoy v0.1-alpha - */ -public class UserListRenderer extends JLabel implements ListCellRenderer { - - private static final long serialVersionUID = 5164417379767181198L; - - @Override - public Component getListCellRendererComponent(JList list, User value, int index, boolean isSelected, - boolean cellHasFocus) { - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - } - - // Enable background rendering - setOpaque(true); - - - final String name = value.getName(); - final UserStatus status = value.getStatus(); - - switch (status) { - case ONLINE: - setText(String.format( - "

%s

%s", - status, - name)); - break; - - case OFFLINE: - setText(String.format( - "

%s

%s", - status, - name)); - break; - } - - - - return this; - } +package envoy.client.ui; + +import java.awt.Component; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +import envoy.schema.User; +import envoy.schema.User.UserStatus; + +/** + * Defines how the {@code UserList} is displayed. + * + * Project: envoy-client
+ * File: UserListRenderer.java
+ * Created: 12 Oct 2019
+ * + * @author Kai S. K. Engelbart + * @author Maximilian Käfer + * @since Envoy v0.1-alpha + */ +public class UserListRenderer extends JLabel implements ListCellRenderer { + + private static final long serialVersionUID = 5164417379767181198L; + + @SuppressWarnings("incomplete-switch") + @Override + public Component getListCellRendererComponent(JList list, User value, int index, boolean isSelected, boolean cellHasFocus) { + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + // Enable background rendering + setOpaque(true); + + final String name = value.getName(); + final UserStatus status = value.getStatus(); + + switch (status) { + case ONLINE: + setText(String + .format("

%s

%s", status, name)); + break; + case OFFLINE: + setText(String + .format("

%s

%s", status, name)); + break; + } + return this; + } } \ No newline at end of file From b0b76984ea57cf44283cbdefe5be5b4e4997f774 Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 7 Dec 2019 13:02:38 +0100 Subject: [PATCH 20/22] Added new Envoy logo designed by @DieGurke --- src/main/java/envoy/client/ui/ChatWindow.java | 4 +++- src/main/resources/envoy_logo.png | Bin 8152 -> 30553 bytes src/main/resources/envoy_logo_alpha.png | Bin 0 -> 28525 bytes src/main/resources/envoy_logo_old.png | Bin 0 -> 8152 bytes 4 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/envoy_logo_alpha.png create mode 100644 src/main/resources/envoy_logo_old.png diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index b43bb65..05da3c3 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -6,6 +6,7 @@ import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; +import java.awt.Toolkit; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; @@ -59,7 +60,7 @@ public class ChatWindow extends JFrame { private Chat currentChat; private JTextArea messageEnterTextArea; - + private static final Logger logger = Logger.getLogger(ChatWindow.class.getSimpleName()); public ChatWindow(Client client, LocalDB localDB) { @@ -70,6 +71,7 @@ public class ChatWindow extends JFrame { setBounds(100, 100, 600, 800); setTitle("Envoy"); setLocationRelativeTo(null); + setIconImage(Toolkit.getDefaultToolkit().createImage(getClass().getClassLoader().getResource("envoy_logo.png"))); // Save chats when window closes addWindowListener(new WindowAdapter() { diff --git a/src/main/resources/envoy_logo.png b/src/main/resources/envoy_logo.png index 35ef7d9fd78cc3b79454de104ae59fbb65c0b050..1606bd58e0c3a75a3f85b439c283922293bb6f05 100644 GIT binary patch literal 30553 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 ZCHeJUhpIFDU%4v|?Kj$&vB&Q6{{p0b=l1{r literal 8152 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 NRM)?ddk+53e*k9UjZ6Rl literal 0 HcmV?d00001 diff --git a/src/main/resources/envoy_logo_old.png b/src/main/resources/envoy_logo_old.png new file mode 100644 index 0000000000000000000000000000000000000000..35ef7d9fd78cc3b79454de104ae59fbb65c0b050 GIT binary patch literal 8152 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 Date: Sat, 7 Dec 2019 13:25:39 +0100 Subject: [PATCH 21/22] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9d4ae77 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at kske@outlook.de. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From 6d297355342a948661dc32121ee10b7bf39a8c13 Mon Sep 17 00:00:00 2001 From: delvh Date: Sat, 7 Dec 2019 13:33:55 +0100 Subject: [PATCH 22/22] Create issue templates Now issues can be easily created using these templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2b978b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333 + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..fbdebf6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: CyB3RC0nN0R, delvh, DieGurke + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here.