Keep each server's session state in core

What a server tab knows beyond its views - the connection lifecycle,
title and status, the identity used, the microphone/speaker/away and
commander flags, chat sending and the channel lookups - moves from the
Swing ServerTab and ServerTabSelfState into core's ServerSession, so
another frontend gets the same behaviour without copying it.

ServerSession also takes over what the Swing event bridge decided on its
own: clearing the model on disconnect, naming the tab after the server,
showing senders under their contact names, and the chat's system lines.
The bridge now only updates views. SelfState moves along, while its
icons stay with the tray.

A server switch no longer needs to queue behind the UI thread: the
disconnect is reported synchronously, so the session is free again by
the time it reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 08:09:23 +00:00
parent 24f4b3813d
commit 8f385b9343
10 changed files with 657 additions and 580 deletions

View File

@@ -12,6 +12,7 @@ import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
import com.ts3client.session.SelfState;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer;
@@ -201,7 +202,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public void onCommanderToggle(boolean commander) {
if (selected != null) selected.setCommander(commander);
if (selected != null) selected.session().setCommander(commander);
}
@Override
@@ -211,7 +212,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public void onSetAvatar() {
if (selected != null && selected.isConnected()) selected.showAvatarDialog();
if (selected != null && selected.session().isConnected()) selected.showAvatarDialog();
}
@Override
@@ -226,7 +227,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public boolean canViewBanList() {
return selected != null && selected.canViewBanList();
return selected != null && selected.session().canViewBanList();
}
@Override
@@ -283,14 +284,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public void onMicMuteToggle(boolean muted) {
if (selected == null) return;
selected.setMicMuted(muted);
selected.session().setMicMuted(muted);
updateToolbar();
}
@Override
public void onDeafenToggle(boolean deafened) {
if (selected == null) return;
selected.setDeafened(deafened);
selected.session().setDeafened(deafened);
updateToolbar();
}
@@ -350,7 +351,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
/** The tab a new connection should use: the current one if it is free, else a new one. */
private ServerTab tabForNewConnection() {
if (selected != null && !selected.isBusy()) return selected;
if (selected != null && !selected.session().isBusy()) return selected;
ServerTab tab = newTab();
selectTab(tab);
return tab;
@@ -369,11 +370,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public void closeTab(ServerTab tab) {
if (tab == null) return;
tab.disconnect();
tab.session().disconnect();
if (tabs.size() == 1) return; // always keep one view around
tabs.remove(tab);
tab.dispose();
tab.session().dispose();
tabPane.removeTab(tab);
if (micTab == tab) micTab = null;
if (selected == tab) {
@@ -394,17 +395,17 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
updateTray();
// Closing and reopening the capture line can block briefly; keep it off the EDT.
new Thread(() -> {
if (previous != null) previous.setMicrophoneActive(false);
if (tab != null) tab.setMicrophoneActive(true);
if (previous != null) previous.session().setMicrophoneActive(false);
if (tab != null) tab.session().setMicrophoneActive(true);
}, "mic-handover").start();
}
/** Gives the microphone to some connected tab when nobody holds it. */
private void assignMicrophoneIfFree() {
if (micTab != null && tabs.contains(micTab) && micTab.isConnected()) return;
if (micTab != null && tabs.contains(micTab) && micTab.session().isConnected()) return;
micTab = null;
for (ServerTab t : tabs) {
if (t.isConnected()) {
if (t.session().isConnected()) {
setMicTab(t);
return;
}
@@ -458,11 +459,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
List<ServerTab> hotkeyTargets(boolean activeServerOnly) {
List<ServerTab> out = new ArrayList<>();
if (activeServerOnly) {
if (selected != null && selected.isConnected()) out.add(selected);
if (selected != null && selected.session().isConnected()) out.add(selected);
return out;
}
for (ServerTab tab : tabs) {
if (tab.isConnected()) out.add(tab);
if (tab.session().isConnected()) out.add(tab);
}
return out;
}
@@ -479,21 +480,21 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
void setPushToTalk(boolean talking) {
pttPressed = talking;
ServerTab tab = micTab;
if (tab == null || !tab.isConnected() || tab.connection().getMicrophone() == null) return;
if (tab == null || !tab.session().isConnected() || tab.connection().getMicrophone() == null) return;
tab.connection().getMicrophone().setPushToTalk(talking);
}
/** Tells every connected microphone about a key press, as TS3 tells all its connections. */
private void keyPressed() {
for (ServerTab tab : tabs) {
if (tab.isConnected() && tab.connection().getMicrophone() != null) {
if (tab.session().isConnected() && tab.connection().getMicrophone() != null) {
tab.connection().getMicrophone().keyPressed();
}
}
}
void moveMicrophoneToSelectedTab() {
if (selected != null && selected.isConnected()) setMicTab(selected);
if (selected != null && selected.session().isConnected()) setMicTab(selected);
updateToolbar();
}
@@ -545,12 +546,12 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}
settings.nickname = nickname.trim();
settings.save();
for (ServerTab tab : tabs) tab.setNickname(settings.nickname);
for (ServerTab tab : tabs) tab.session().setNickname(settings.nickname);
}
void browseCurrentChannel() {
if (selected == null || !selected.isConnected()) return;
ChannelNode channel = selected.currentChannel();
if (selected == null || !selected.session().isConnected()) return;
ChannelNode channel = selected.session().currentChannel();
if (channel != null) selected.browseFiles(channel);
}
@@ -588,8 +589,8 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}
}
case CHANNEL -> {
if (selected != null && selected.isConnected()) {
out.addAll(selected.channelPaths());
if (selected != null && selected.session().isConnected()) {
out.addAll(selected.session().channelPaths());
}
}
default -> {
@@ -642,7 +643,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
// The first connection to come up takes the microphone; later ones are muted
// until the user activates them.
if (micTab == null) setMicTab(tab);
tab.switchTo(address, port, nickname, password, identityId, channel, channelPassword);
tab.session().switchTo(address, port, nickname, password, identityId, channel, channelPassword);
}
/** Connects on the current tab, leaving whatever server it is on. */
@@ -652,16 +653,16 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}
private void addCurrentServerBookmark() {
if (selected == null || !selected.isConnected()) {
if (selected == null || !selected.session().isConnected()) {
JOptionPane.showMessageDialog(this, "Connect to a server first.",
"Add bookmark", JOptionPane.INFORMATION_MESSAGE);
return;
}
String addr = selected.address();
int port = selected.port();
String channelPath = selected.currentChannelPath();
String addr = selected.session().address();
int port = selected.session().port();
String channelPath = selected.session().currentChannelPath();
JTextField labelField = new JTextField(selected.title());
JTextField labelField = new JTextField(selected.session().title());
JCheckBox joinChannel = new JCheckBox("Join \"" + channelPath + "\" on connect", !channelPath.isEmpty());
JPanel form = new JPanel(new GridLayout(0, 1, 0, UIScale.scale(2)));
form.add(new JLabel("Bookmark label:"));
@@ -673,7 +674,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}
Bookmark bookmark = new Bookmark(labelField.getText().trim(), addr, port,
settings.nickname, settings.serverPassword);
bookmark.identityId = selected.identityId();
bookmark.identityId = selected.session().identityId();
if (joinChannel.isSelected()) bookmark.channel = channelPath;
bookmarks.add(bookmark);
bookmarks.save();
@@ -683,7 +684,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
/** The away button's drop-down: the global actions, the presets and their editor. */
private JPopupMenu buildAwayMenu() {
JPopupMenu menu = new JPopupMenu();
boolean anyConnected = tabs.stream().anyMatch(ServerTab::isConnected);
boolean anyConnected = tabs.stream().anyMatch(t -> t.session().isConnected());
JCheckBoxMenuItem globally = new JCheckBoxMenuItem("Set Globally Away", Icons.of("AWAY"), isGloballyAway());
globally.setEnabled(anyConnected);
@@ -716,22 +717,22 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private void toggleAway(boolean away) {
if (selected == null) return;
selected.setAway(away, "");
selected.session().setAway(away, "");
updateToolbar();
}
/** Sets the away message on the selected server only. */
private void setAwayStatus() {
if (selected == null) return;
String message = askAwayMessage(selected.awayMessage());
String message = askAwayMessage(selected.session().awayMessage());
if (message == null) return;
selected.setAway(true, message);
selected.session().setAway(true, message);
updateToolbar();
}
private void setAwayEverywhere(boolean away, String message) {
for (ServerTab tab : tabs) {
if (tab.isConnected()) tab.setAway(away, message);
if (tab.session().isConnected()) tab.session().setAway(away, message);
}
updateToolbar();
}
@@ -740,18 +741,18 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private boolean isGloballyAway() {
boolean any = false;
for (ServerTab tab : tabs) {
if (!tab.isConnected()) continue;
if (!tab.session().isConnected()) continue;
any = true;
if (!tab.isAway()) return false;
if (!tab.session().isAway()) return false;
}
return any;
}
/** The message to preload the prompt with: the selected tab's, else any set one. */
private String currentAwayMessage() {
if (selected != null && !selected.awayMessage().isEmpty()) return selected.awayMessage();
if (selected != null && !selected.session().awayMessage().isEmpty()) return selected.session().awayMessage();
for (ServerTab tab : tabs) {
if (tab.isConnected() && !tab.awayMessage().isEmpty()) return tab.awayMessage();
if (tab.session().isConnected() && !tab.session().awayMessage().isEmpty()) return tab.session().awayMessage();
}
return "";
}
@@ -763,7 +764,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
}
private void doDisconnect() {
if (selected != null) selected.disconnect();
if (selected != null) selected.session().disconnect();
}
/**
@@ -782,7 +783,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
// Already shutting down (hook itself is running); nothing to remove.
}
for (ServerTab tab : new ArrayList<>(tabs)) {
tab.shutdown();
tab.session().shutdown();
}
soundPlayer.shutdown();
hotkeys.dispose();
@@ -860,7 +861,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
if (n != null && !n.trim().isEmpty()) {
settings.nickname = n.trim();
settings.save();
for (ServerTab tab : tabs) tab.setNickname(settings.nickname);
for (ServerTab tab : tabs) tab.session().setNickname(settings.nickname);
}
}
@@ -877,14 +878,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
// ---- toolbar / status ----
private void updateToolbar() {
boolean connected = selected != null && selected.isConnected();
boolean anyConnected = tabs.stream().anyMatch(ServerTab::isConnected);
boolean connected = selected != null && selected.session().isConnected();
boolean anyConnected = tabs.stream().anyMatch(t -> t.session().isConnected());
boolean micMuted = connected && selected.isMicMuted();
boolean deaf = connected && selected.isDeafened();
boolean micMuted = connected && selected.session().isMicMuted();
boolean deaf = connected && selected.session().isDeafened();
boolean active = selected != null && selected == micTab;
boolean away = connected && selected.isAway();
boolean commander = connected && selected.isCommander();
boolean away = connected && selected.session().isAway();
boolean commander = connected && selected.session().isCommander();
toolbar.refresh(connected, anyConnected, micMuted, deaf, active, away);
menuBar.refresh(connected, away, commander);
updateTray();
@@ -895,8 +896,8 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
* visible one when nobody is capturing.
*/
private ServerTab trayTab() {
if (micTab != null && micTab.isConnected()) return micTab;
return selected != null && selected.isConnected() ? selected : null;
if (micTab != null && micTab.session().isConnected()) return micTab;
return selected != null && selected.session().isConnected() ? selected : null;
}
private void updateTray() {
@@ -905,17 +906,17 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
if (tab == null) {
tray.update(SelfState.DISCONNECTED, null);
} else {
tray.update(tab.selfState(), tab.title());
tray.update(tab.session().selfState(), tab.session().title());
}
}
private void updateStatusLabel() {
statusBar.setStatus(selected == null ? "Not connected" : selected.status());
statusBar.setStatus(selected == null ? "Not connected" : selected.session().status());
}
private void updateConnectionStatus() {
ServerTab tab = selected;
if (tab == null || !tab.isConnected()) return;
if (tab == null || !tab.session().isConnected()) return;
int users = tab.connection().getModel().clientCount();
StringBuilder s = new StringBuilder("Connected to ")
.append(tab.connection().getModel().getServerName())