Display Current User Status and Unread Message Amount in Status Tray Icon #103
@ -3,16 +3,17 @@ package envoy.client.data;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import javafx.beans.property.*;
|
||||||
import javafx.collections.*;
|
import javafx.collections.*;
|
||||||
|
|
||||||
import envoy.client.net.WriteProxy;
|
|
||||||
import envoy.data.*;
|
import envoy.data.*;
|
||||||
import envoy.data.Message.MessageStatus;
|
import envoy.data.Message.MessageStatus;
|
||||||
import envoy.event.MessageStatusChange;
|
import envoy.event.MessageStatusChange;
|
||||||
|
|
||||||
|
import envoy.client.net.WriteProxy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a chat between two {@link User}s
|
* Represents a chat between two {@link User}s as a list of {@link Message} objects.
|
||||||
* as a list of {@link Message} objects.
|
|
||||||
*
|
*
|
||||||
* @author Maximilian Käfer
|
* @author Maximilian Käfer
|
||||||
* @author Leon Hofmeister
|
* @author Leon Hofmeister
|
||||||
@ -25,13 +26,14 @@ public class Chat implements Serializable {
|
|||||||
|
|
||||||
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
|
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
protected int unreadAmount;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
||||||
*/
|
*/
|
||||||
protected transient long lastWritingEvent;
|
protected transient long lastWritingEvent;
|
||||||
|
|
||||||
|
protected int unreadAmount;
|
||||||
|
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(0);
|
||||||
|
|
||||||
private static final long serialVersionUID = 2L;
|
private static final long serialVersionUID = 2L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,11 +44,14 @@ public class Chat implements Serializable {
|
|||||||
* @param recipient the user who receives the messages
|
* @param recipient the user who receives the messages
|
||||||
* @since Envoy Client v0.1-alpha
|
* @since Envoy Client v0.1-alpha
|
||||||
*/
|
*/
|
||||||
public Chat(Contact recipient) { this.recipient = recipient; }
|
public Chat(Contact recipient) {
|
||||||
|
this.recipient = recipient;
|
||||||
|
}
|
||||||
|
|
||||||
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
|
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
|
||||||
stream.defaultReadObject();
|
stream.defaultReadObject();
|
||||||
messages = FXCollections.observableList((List<Message>) stream.readObject());
|
messages = FXCollections.observableList((List<Message>) stream.readObject());
|
||||||
|
totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeObject(ObjectOutputStream stream) throws IOException {
|
private void writeObject(ObjectOutputStream stream) throws IOException {
|
||||||
@ -55,7 +60,10 @@ public class Chat implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); }
|
public String toString() {
|
||||||
|
return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient,
|
||||||
|
messages.size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a hash code based on the recipient.
|
* Generates a hash code based on the recipient.
|
||||||
@ -63,7 +71,9 @@ public class Chat implements Serializable {
|
|||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() { return Objects.hash(recipient); }
|
public int hashCode() {
|
||||||
|
return Objects.hash(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests equality to another object based on the recipient.
|
* Tests equality to another object based on the recipient.
|
||||||
@ -72,39 +82,46 @@ public class Chat implements Serializable {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (this == obj) return true;
|
if (this == obj)
|
||||||
if (!(obj instanceof Chat)) return false;
|
return true;
|
||||||
|
if (!(obj instanceof Chat))
|
||||||
|
return false;
|
||||||
final var other = (Chat) obj;
|
final var other = (Chat) obj;
|
||||||
return Objects.equals(recipient, other.recipient);
|
return Objects.equals(recipient, other.recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the status of all chat messages received from the recipient to
|
* Sets the status of all chat messages received from the recipient to {@code READ} starting
|
||||||
* {@code READ} starting from the bottom and stopping once a read message is
|
* from the bottom and stopping once a read message is found.
|
||||||
* found.
|
|
||||||
*
|
*
|
||||||
* @param writeProxy the write proxy instance used to notify the server about
|
* @param writeProxy the write proxy instance used to notify the server about the message status
|
||||||
* the message status changes
|
* changes
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void read(WriteProxy writeProxy) {
|
public void read(WriteProxy writeProxy) {
|
||||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||||
final var m = messages.get(i);
|
final var m = messages.get(i);
|
||||||
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
if (m.getSenderID() == recipient.getID())
|
||||||
|
if (m.getStatus() == MessageStatus.READ)
|
||||||
|
break;
|
||||||
else {
|
else {
|
||||||
m.setStatus(MessageStatus.READ);
|
m.setStatus(MessageStatus.READ);
|
||||||
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount);
|
||||||
unreadAmount = 0;
|
unreadAmount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {@code true} if the newest message received in the chat doesn't have
|
* @return {@code true} if the newest message received in the chat doesn't have the status
|
||||||
* the status {@code READ}
|
* {@code READ}
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
|
public boolean isUnread() {
|
||||||
|
return !messages.isEmpty()
|
||||||
|
&& messages.get(messages.size() - 1).getStatus() != MessageStatus.READ;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a message at the correct place according to its creation date.
|
* Inserts a message at the correct place according to its creation date.
|
||||||
@ -128,14 +145,25 @@ public class Chat implements Serializable {
|
|||||||
* @return whether the message has been found and removed
|
* @return whether the message has been found and removed
|
||||||
* @since Envoy Client v0.3-beta
|
* @since Envoy Client v0.3-beta
|
||||||
*/
|
*/
|
||||||
public boolean remove(long messageID) { return messages.removeIf(m -> m.getID() == messageID); }
|
public boolean remove(long messageID) {
|
||||||
|
return messages.removeIf(m -> m.getID() == messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer property storing the total amount of unread messages
|
||||||
|
* @since Envoy Client v0.3-beta
|
||||||
|
*/
|
||||||
|
public static IntegerProperty getTotalUnreadAmount() { return totalUnreadAmount; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments the amount of unread messages.
|
* Increments the amount of unread messages.
|
||||||
*
|
*
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public void incrementUnreadAmount() { ++unreadAmount; }
|
public void incrementUnreadAmount() {
|
||||||
|
++unreadAmount;
|
||||||
|
totalUnreadAmount.set(totalUnreadAmount.get() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the amount of unread messages in this chat
|
* @return the amount of unread messages in this chat
|
||||||
@ -156,8 +184,7 @@ public class Chat implements Serializable {
|
|||||||
public Contact getRecipient() { return recipient; }
|
public Contact getRecipient() { return recipient; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the last known time a {@link envoy.event.IsTyping} event has been
|
* @return the last known time a {@link envoy.event.IsTyping} event has been sent
|
||||||
* sent
|
|
||||||
* @since Envoy Client v0.2-beta
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public long getLastWritingEvent() { return lastWritingEvent; }
|
public long getLastWritingEvent() { return lastWritingEvent; }
|
||||||
@ -167,5 +194,7 @@ public class Chat implements Serializable {
|
|||||||
*
|
*
|
||||||
* @since Envoy Client v0.2-beta
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
|
public void lastWritingEventWasNow() {
|
||||||
|
lastWritingEvent = System.currentTimeMillis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package envoy.client.ui;
|
package envoy.client.ui;
|
||||||
|
|
||||||
|
import static java.awt.Image.SCALE_SMOOTH;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.TrayIcon.MessageType;
|
import java.awt.TrayIcon.MessageType;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@ -13,13 +15,13 @@ import dev.kske.eventbus.Event;
|
|||||||
import envoy.data.Message;
|
import envoy.data.Message;
|
||||||
import envoy.data.User.UserStatus;
|
import envoy.data.User.UserStatus;
|
||||||
|
|
||||||
import envoy.client.data.Context;
|
import envoy.client.data.*;
|
||||||
import envoy.client.event.OwnStatusChange;
|
import envoy.client.event.OwnStatusChange;
|
||||||
import envoy.client.helper.ShutdownHelper;
|
import envoy.client.helper.ShutdownHelper;
|
||||||
import envoy.client.util.*;
|
import envoy.client.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tray icon with the Envoy logo, a "Envoy" tool tip and a pop-up menu with menu items for
|
* A tray icon with the Envoy logo, an "Envoy" tool tip and a pop-up menu with menu items for
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Changing the user status</li>
|
* <li>Changing the user status</li>
|
||||||
* <li>Logging out</li>
|
* <li>Logging out</li>
|
||||||
@ -43,17 +45,16 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
*/
|
*/
|
||||||
private boolean displayMessageNotification;
|
private boolean displayMessageNotification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the tray icon's image.
|
||||||
|
*/
|
||||||
|
private final Dimension size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Envoy logo on which the current user status and unread message count will be drawn to
|
* The Envoy logo on which the current user status and unread message count will be drawn to
|
||||||
* compose the tray icon.
|
* compose the tray icon.
|
||||||
*/
|
*/
|
||||||
private final Image logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png")
|
private final Image logo;
|
||||||
.getScaledInstance(size, size, BufferedImage.SCALE_SMOOTH);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size of the tray icon, as defined by the system tray.
|
|
||||||
*/
|
|
||||||
private static final int size = (int) SystemTray.getSystemTray().getTrayIconSize().getWidth();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {@code true} if the status tray icon is supported on this platform
|
* @return {@code true} if the status tray icon is supported on this platform
|
||||||
@ -68,6 +69,10 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
* @since Envoy Client v0.2-beta
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public StatusTrayIcon(Stage stage) {
|
public StatusTrayIcon(Stage stage) {
|
||||||
|
size = SystemTray.getSystemTray().getTrayIconSize();
|
||||||
|
logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width,
|
||||||
|
size.height, SCALE_SMOOTH);
|
||||||
|
|
||||||
final var popup = new PopupMenu();
|
final var popup = new PopupMenu();
|
||||||
|
|
||||||
// Adding the exit menu item
|
// Adding the exit menu item
|
||||||
@ -102,6 +107,9 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
.addListener((ov, wasFocused, isFocused) -> displayMessageNotification =
|
.addListener((ov, wasFocused, isFocused) -> displayMessageNotification =
|
||||||
!displayMessageNotification && wasFocused ? false : !isFocused);
|
!displayMessageNotification && wasFocused ? false : !isFocused);
|
||||||
|
|
||||||
|
// Listen to changes in the total unread message amount
|
||||||
|
Chat.getTotalUnreadAmount().addListener((ov, oldValue, newValue) -> updateImage());
|
||||||
|
|
||||||
// Show the window if the user clicks on the icon
|
// Show the window if the user clicks on the icon
|
||||||
trayIcon.addActionListener(evt -> Platform.runLater(() -> {
|
trayIcon.addActionListener(evt -> Platform.runLater(() -> {
|
||||||
stage.setIconified(false);
|
stage.setIconified(false);
|
||||||
@ -150,6 +158,17 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
: "New message received", message.getText(), MessageType.INFO);
|
: "New message received", message.getText(), MessageType.INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the tray icon's image by first releasing the resources held by the current image and
|
||||||
|
* then setting a new one generated by the {@link StatusTrayIcon#createImage()} method.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.3-beta
|
||||||
|
*/
|
||||||
|
private void updateImage() {
|
||||||
|
trayIcon.getImage().flush();
|
||||||
|
trayIcon.setImage(createImage());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composes an icon that displays the current user status and the amount of unread messages, if
|
* Composes an icon that displays the current user status and the amount of unread messages, if
|
||||||
* any are present.
|
* any are present.
|
||||||
@ -159,7 +178,7 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
private BufferedImage createImage() {
|
private BufferedImage createImage() {
|
||||||
|
|
||||||
// Create a new image with the dimensions of the logo
|
// Create a new image with the dimensions of the logo
|
||||||
var img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
var img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
|
||||||
// Obtain the draw graphics of the image and copy the logo
|
// Obtain the draw graphics of the image and copy the logo
|
||||||
var g = img.createGraphics();
|
var g = img.createGraphics();
|
||||||
@ -179,7 +198,16 @@ public final class StatusTrayIcon implements EventListener {
|
|||||||
case OFFLINE:
|
case OFFLINE:
|
||||||
g.setColor(Color.GRAY);
|
g.setColor(Color.GRAY);
|
||||||
}
|
}
|
||||||
g.fillOval(size / 2, size / 2, size / 2, size / 2);
|
g.fillOval(size.width / 2, size.height / 2, size.width / 2, size.height / 2);
|
||||||
|
|
||||||
|
// Draw total amount of unread messages, if any are present
|
||||||
|
if (Chat.getTotalUnreadAmount().get() > 0) {
|
||||||
|
g.setColor(Color.RED);
|
||||||
|
g.fillOval(size.width / 2, 0, size.width / 2, size.height / 2);
|
||||||
|
g.setColor(Color.BLACK);
|
||||||
|
g.drawString(String.valueOf(Chat.getTotalUnreadAmount().get()), size.width / 2,
|
||||||
|
size.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Finish drawing
|
// Finish drawing
|
||||||
g.dispose();
|
g.dispose();
|
||||||
|
Reference in New Issue
Block a user