Add the contact list (friends, blocked, neutral)

Mirrors the TeamSpeak 3 client's Contacts: clients can be filed as friend,
blocked or neutral from their context menu or by dragging them into the
Contacts window, given a custom and phonetic nickname, shown under either
name or both, and per contact muted automatically, ignored in server/channel
or private chat and pokes, have their away message hidden and whispers
allowed or denied. Friends are green and blocked clients red in the tree
(optional), new contacts start from per-type defaults ("Set Defaults"), and
the global whisper policy lives in Options -> Contacts. "Change Volume..."
adds a per-client dB gain that a contact entry remembers.

Contacts are stored in ~/.ts3jclient/contacts.txt using the exact record
format the official client keeps in the Contacts table of its settings.db
(field names and codes recovered from the binary), so entries move between
the two verbatim. A small read-only pure-Java SQLite reader imports the
official client's list on first run and on demand, without a driver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 07:50:58 +00:00
parent 22e0304f25
commit f409f500fd
33 changed files with 2474 additions and 23 deletions

View File

@@ -8,6 +8,7 @@ import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer;
@@ -46,6 +47,8 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private final Bookmarks bookmarks = Bookmarks.load();
private final AwayMessages awayMessages = AwayMessages.load();
private final IdentityStore identities;
/** Friends and blocked clients, shared by every connection. */
private final ContactStore contacts = ContactStore.load();
private final AudioBackend audio = new DesktopAudioBackend();
/** Sound pack playback, shared by every connection. */
private final SoundNotifier sounds;
@@ -86,6 +89,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
this.hotkeys = new HotkeyService(new HotkeyActions(this), this::hotkeyArgumentChoices);
contacts.addListener(() -> SwingUtilities.invokeLater(this::contactsChanged));
setIconImage(Icons.app().getImage());
// We tear the connections down ourselves on close, so don't let Swing kill the JVM.
@@ -193,6 +197,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
showIdentities();
}
@Override
public void onShowContacts() {
showContacts(null);
}
@Override
public boolean canViewBanList() {
return selected != null && selected.canViewBanList();
@@ -310,7 +319,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
// ---- tab management ----
private ServerTab newTab() {
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds);
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds, contacts);
tabs.add(tab);
tabPane.addTab(tab);
refreshTabs();
@@ -765,6 +774,32 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
new IdentitiesDialog(this, identities, settings, bookmarks, null).setVisible(true);
}
Settings settings() {
return settings;
}
/** The one contacts window, kept so a second request brings it forward instead of doubling it. */
private ContactsDialog contactsDialog;
/**
* Opens the contact list, or brings it forward.
*
* @param uniqueId the contact to select, or {@code null} to keep the current selection
*/
void showContacts(String uniqueId) {
if (contactsDialog == null || !contactsDialog.isDisplayable()) {
contactsDialog = new ContactsDialog(this, contacts, settings);
}
if (uniqueId != null) contactsDialog.select(uniqueId);
contactsDialog.setVisible(true);
contactsDialog.toFront();
}
/** The contact list changed somewhere: every open server follows. */
private void contactsChanged() {
for (ServerTab tab : tabs) tab.contactsChanged();
}
private void showSettings() {
// Device and voice-gating changes apply to the capturing connection and the
// playback of the visible one; the rest pick the new settings up on connect.
@@ -772,7 +807,10 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
micTab == null ? null : micTab.connection().getMicrophone(),
selected == null ? null : selected.connection().getPlayback(),
sounds, hotkeys,
this::applyOutputSettingsToAllTabs);
() -> {
applyOutputSettingsToAllTabs();
contactsChanged();
});
dlg.setVisible(true);
}