Keep track of total unread messages and display them in the status tray
This commit is contained in:
		@@ -3,16 +3,17 @@ package envoy.client.data;
 | 
			
		||||
import java.io.*;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import javafx.beans.property.*;
 | 
			
		||||
import javafx.collections.*;
 | 
			
		||||
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a chat between two {@link User}s
 | 
			
		||||
 * as a list of {@link Message} objects.
 | 
			
		||||
 * Represents a chat between two {@link User}s as a list of {@link Message} objects.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
@@ -25,13 +26,14 @@ public class Chat implements Serializable {
 | 
			
		||||
 | 
			
		||||
	protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
 | 
			
		||||
 | 
			
		||||
	protected int unreadAmount;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stores the last time an {@link envoy.event.IsTyping} event has been sent.
 | 
			
		||||
	 */
 | 
			
		||||
	protected transient long lastWritingEvent;
 | 
			
		||||
 | 
			
		||||
	protected int						unreadAmount;
 | 
			
		||||
	protected static IntegerProperty	totalUnreadAmount	= new SimpleIntegerProperty(0);
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 2L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
@@ -42,11 +44,14 @@ public class Chat implements Serializable {
 | 
			
		||||
	 * @param recipient the user who receives the messages
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Chat(Contact recipient) { this.recipient = recipient; }
 | 
			
		||||
	public Chat(Contact recipient) {
 | 
			
		||||
		this.recipient = recipient;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
 | 
			
		||||
		stream.defaultReadObject();
 | 
			
		||||
		messages = FXCollections.observableList((List<Message>) stream.readObject());
 | 
			
		||||
		totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void writeObject(ObjectOutputStream stream) throws IOException {
 | 
			
		||||
@@ -55,7 +60,10 @@ public class Chat implements Serializable {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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.
 | 
			
		||||
@@ -63,7 +71,9 @@ public class Chat implements Serializable {
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public int hashCode() { return Objects.hash(recipient); }
 | 
			
		||||
	public int hashCode() {
 | 
			
		||||
		return Objects.hash(recipient);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Tests equality to another object based on the recipient.
 | 
			
		||||
@@ -72,39 +82,46 @@ public class Chat implements Serializable {
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean equals(Object obj) {
 | 
			
		||||
		if (this == obj) return true;
 | 
			
		||||
		if (!(obj instanceof Chat)) return false;
 | 
			
		||||
		if (this == obj)
 | 
			
		||||
			return true;
 | 
			
		||||
		if (!(obj instanceof Chat))
 | 
			
		||||
			return false;
 | 
			
		||||
		final var other = (Chat) obj;
 | 
			
		||||
		return Objects.equals(recipient, other.recipient);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the status of all chat messages received from the recipient to
 | 
			
		||||
	 * {@code READ} starting from the bottom and stopping once a read message is
 | 
			
		||||
	 * found.
 | 
			
		||||
	 * Sets the status of all chat messages received from the recipient to {@code READ} starting
 | 
			
		||||
	 * from the bottom and stopping once a read message is found.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param writeProxy the write proxy instance used to notify the server about
 | 
			
		||||
	 *                   the message status changes
 | 
			
		||||
	 * @param writeProxy the write proxy instance used to notify the server about the message status
 | 
			
		||||
	 *                   changes
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void read(WriteProxy writeProxy) {
 | 
			
		||||
		for (int i = messages.size() - 1; i >= 0; --i) {
 | 
			
		||||
			final var m = messages.get(i);
 | 
			
		||||
			if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
 | 
			
		||||
			else {
 | 
			
		||||
				m.setStatus(MessageStatus.READ);
 | 
			
		||||
				writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
 | 
			
		||||
			}
 | 
			
		||||
			if (m.getSenderID() == recipient.getID())
 | 
			
		||||
				if (m.getStatus() == MessageStatus.READ)
 | 
			
		||||
					break;
 | 
			
		||||
				else {
 | 
			
		||||
					m.setStatus(MessageStatus.READ);
 | 
			
		||||
					writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
 | 
			
		||||
				}
 | 
			
		||||
		}
 | 
			
		||||
		totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount);
 | 
			
		||||
		unreadAmount = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if the newest message received in the chat doesn't have
 | 
			
		||||
	 *         the status {@code READ}
 | 
			
		||||
	 * @return {@code true} if the newest message received in the chat doesn't have the status
 | 
			
		||||
	 *         {@code READ}
 | 
			
		||||
	 * @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.
 | 
			
		||||
@@ -128,14 +145,25 @@ public class Chat implements Serializable {
 | 
			
		||||
	 * @return whether the message has been found and removed
 | 
			
		||||
	 * @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.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @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
 | 
			
		||||
@@ -156,8 +184,7 @@ public class Chat implements Serializable {
 | 
			
		||||
	public Contact getRecipient() { return recipient; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the last known time a {@link envoy.event.IsTyping} event has been
 | 
			
		||||
	 *         sent
 | 
			
		||||
	 * @return the last known time a {@link envoy.event.IsTyping} event has been sent
 | 
			
		||||
	 * @since Envoy Client v0.2-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public long getLastWritingEvent() { return lastWritingEvent; }
 | 
			
		||||
@@ -167,5 +194,7 @@ public class Chat implements Serializable {
 | 
			
		||||
	 *
 | 
			
		||||
	 * @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;
 | 
			
		||||
 | 
			
		||||
import static java.awt.Image.SCALE_SMOOTH;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.TrayIcon.MessageType;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
@@ -13,13 +15,13 @@ import dev.kske.eventbus.Event;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.data.User.UserStatus;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Context;
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.event.OwnStatusChange;
 | 
			
		||||
import envoy.client.helper.ShutdownHelper;
 | 
			
		||||
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>
 | 
			
		||||
 * <li>Changing the user status</li>
 | 
			
		||||
 * <li>Logging out</li>
 | 
			
		||||
@@ -43,17 +45,16 @@ public final class StatusTrayIcon implements EventListener {
 | 
			
		||||
	 */
 | 
			
		||||
	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
 | 
			
		||||
	 * compose the tray icon.
 | 
			
		||||
	 */
 | 
			
		||||
	private final Image logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png")
 | 
			
		||||
		.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();
 | 
			
		||||
	private final Image logo;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @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
 | 
			
		||||
	 */
 | 
			
		||||
	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();
 | 
			
		||||
 | 
			
		||||
		// Adding the exit menu item
 | 
			
		||||
@@ -102,6 +107,9 @@ public final class StatusTrayIcon implements EventListener {
 | 
			
		||||
			.addListener((ov, wasFocused, isFocused) -> displayMessageNotification =
 | 
			
		||||
				!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
 | 
			
		||||
		trayIcon.addActionListener(evt -> Platform.runLater(() -> {
 | 
			
		||||
			stage.setIconified(false);
 | 
			
		||||
@@ -150,6 +158,17 @@ public final class StatusTrayIcon implements EventListener {
 | 
			
		||||
					: "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
 | 
			
		||||
	 * any are present.
 | 
			
		||||
@@ -159,7 +178,7 @@ public final class StatusTrayIcon implements EventListener {
 | 
			
		||||
	private BufferedImage createImage() {
 | 
			
		||||
 | 
			
		||||
		// 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
 | 
			
		||||
		var g = img.createGraphics();
 | 
			
		||||
@@ -179,7 +198,16 @@ public final class StatusTrayIcon implements EventListener {
 | 
			
		||||
			case OFFLINE:
 | 
			
		||||
				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
 | 
			
		||||
		g.dispose();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user