Notify of pokes and private messages on the desktop

While none of the client's windows has the focus, a poke or a private
message shows a desktop notification, which the desktop's notification
centre keeps. A private conversation keeps one notification, updated as
messages come; clicking it brings the window forward with that chat open,
and they all go once a window of the client is in front again.

On Linux they go to the freedesktop.org notification service, spoken to
over the session bus by a small D-Bus client of our own on Java's Unix
sockets, so no library or native code is involved; Plasma also takes a
reply typed into the notification. Elsewhere they are AWT tray messages,
which Windows and macOS put in their notification centres. The options
can turn them off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 15:20:51 +00:00
parent a6efc0b8da
commit 02ff63341e
14 changed files with 1019 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import com.ts3client.session.ServerSession;
import com.ts3client.session.Sessions;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer;
import com.ts3client.ui.tray.TrayBackend;
import javax.swing.AbstractAction;
import javax.swing.JCheckBox;
@@ -35,6 +36,7 @@ import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
@@ -81,6 +83,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
/** Mirrors the active server's own client state next to the clock; set once the window is up. */
private volatile TrayController tray;
private final DesktopNotifier notifier;
private MainToolbar toolbar;
private StatusBar statusBar;
@@ -100,6 +103,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
this.sounds = new SoundNotifier(settings);
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
this.notifier = new DesktopNotifier(this, settings);
// Any of the client's windows coming to the front counts, a dialog included.
KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("activeWindow", e -> {
if (e.getNewValue() != null) notifier.windowActivated();
});
this.hotkeys = new HotkeyService(new HotkeyActions(this), this::hotkeyArgumentChoices);
hotkeys.addKeyPressListener(() -> SwingUtilities.invokeLater(sessions::keyPressed));
contacts.addListener(() -> SwingUtilities.invokeLater(this::contactsChanged));
@@ -437,9 +445,21 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
void tabDisconnected(ServerTab tab) {
if (!tabs.contains(tab)) return;
sessions.disconnected(tab.session());
notifier.forget(tab);
tabUpdated(tab);
}
/** Tells the user of a poke or a private message while the client is in the background. */
DesktopNotifier notifier() {
return notifier;
}
/** The tray icon when AWT docked it, whose messages are the notifications on Windows and macOS. */
TrayBackend awtTray() {
TrayController current = tray;
return current == null ? null : current.backend();
}
// ---- hotkeys ----
/**
@@ -537,7 +557,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
IconTheme.get().reload(settings);
}
/** Brings the window back from the tray, the taskbar or behind other windows. */
void bringToFront() {
TrayController current = tray;
if (current != null) {
// The tray can ask the window manager in a way it honours for a window without focus.
current.show();
return;
}
setVisible(true);
setExtendedState(getExtendedState() & ~JFrame.ICONIFIED);
toFront();
@@ -743,6 +770,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
soundPlayer.shutdown();
hotkeys.dispose();
if (tray != null) tray.dispose();
notifier.shutdown();
}
/** Leaves every server and ends the process. */