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

@@ -0,0 +1,47 @@
package com.ts3client.notify.desktop;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DBusWireTest {
@Test
void methodCallReadsBackAsWritten() {
Map<String, Object> hints = new LinkedHashMap<>();
hints.put("desktop-entry", "ts3j-client");
hints.put("suppress-sound", true);
byte[] message = DBusWire.methodCall(7, "org.freedesktop.Notifications", "/org/freedesktop/Notifications",
"org.freedesktop.Notifications", "Notify", "susssasa{sv}i",
"TS3J", 3L, "", "Bob", "Hi ž", List.of("default", "Open"), hints, -1);
assertEquals(message.length, DBusWire.messageLength(ByteBuffer.wrap(message, 0, DBusWire.FIXED_HEADER)));
DBusWire.Message m = DBusWire.parse(message);
assertEquals(DBusWire.METHOD_CALL, m.type());
assertEquals(7, m.serial());
assertEquals("Notify", m.string(DBusWire.FIELD_MEMBER));
assertEquals("/org/freedesktop/Notifications", m.string(DBusWire.FIELD_PATH));
assertEquals("TS3J", m.body().get(0));
assertEquals(3L, m.body().get(1));
assertEquals("Hi ž", m.body().get(4));
assertEquals(List.of("default", "Open"), m.body().get(5));
List<?> entries = (List<?>) m.body().get(6);
assertArrayEquals(new Object[]{"desktop-entry", "ts3j-client"}, (Object[]) entries.get(0));
assertArrayEquals(new Object[]{"suppress-sound", true}, (Object[]) entries.get(1));
assertEquals(-1, m.body().get(7));
}
@Test
void completeTypesAreDelimited() {
assertEquals(1, DBusWire.typeEnd("su", 0));
assertEquals(5, DBusWire.typeEnd("a{sv}i", 0));
assertEquals(5, DBusWire.typeEnd("a(yv)s", 0));
assertEquals(3, DBusWire.typeEnd("aasu", 1));
}
}