Keep track of total unread messages and display them in the status tray

This commit is contained in:
Kai S. K. Engelbart 2020-10-18 16:45:36 +02:00
parent 44f4d8f1e0
commit 2e17caea4d
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
2 changed files with 96 additions and 39 deletions

View File

@ -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())
else { if (m.getStatus() == MessageStatus.READ)
m.setStatus(MessageStatus.READ); break;
writeProxy.writeMessageStatusChange(new MessageStatusChange(m)); else {
} m.setStatus(MessageStatus.READ);
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();
}
} }

View File

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