Log server activity, scrollable chat tabs, and move info panel to a chat tab
Native TS3 logs client joins/leaves/moves, group changes and channel edits to the server tab; minor local-only notices (mic mute, tray) no longer spam the log. Chat tabs scroll by mouse wheel like server tabs. The info panel can now be moved into a persistent, generically-labelled chat tab that tracks tree selection and collapses the panel's space entirely; a tree rebuild no longer briefly fires a null selection that reset this. Added "Find Client in Channel Tree" to the client context menu, and tree selection now survives model rebuilds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,4 +33,11 @@ public interface ConnectionListener {
|
||||
|
||||
/** Someone poked the local client. */
|
||||
void onPoke(String fromName, String message);
|
||||
|
||||
/**
|
||||
* Something happened on the server that native TS3 records in the server
|
||||
* log: a client connecting/disconnecting/moving, a channel being edited,
|
||||
* a group being (un)assigned, and so on.
|
||||
*/
|
||||
void onServerLog(String message);
|
||||
}
|
||||
|
||||
@@ -791,14 +791,23 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
c.serverGroupIds = parseIntList(e.getClientServerGroups());
|
||||
c.channelGroupId = e.getClientChannelGroupId();
|
||||
c.self = (e.getClientId() == selfClientId);
|
||||
if (e.getClientId() != selfClientId) announceClientEntered(e);
|
||||
if (e.getClientId() != selfClientId) {
|
||||
announceClientEntered(e);
|
||||
logClientEntered(e);
|
||||
}
|
||||
ui.onModelChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientLeave(ClientLeaveEvent e) {
|
||||
if (e.getClientId() == selfClientId) announceOwnRemoval(safeInt(e, "reasonid"), e);
|
||||
else announceClientLeft(e);
|
||||
if (e.getClientId() == selfClientId) {
|
||||
announceOwnRemoval(safeInt(e, "reasonid"), e);
|
||||
} else {
|
||||
ClientEntry leaving = model.getClient(e.getClientId());
|
||||
String name = leaving != null ? leaving.nickname : "Client " + e.getClientId();
|
||||
announceClientLeft(e);
|
||||
logClientLeft(e, name);
|
||||
}
|
||||
model.removeClient(e.getClientId());
|
||||
if (playback != null) playback.removeClient(e.getClientId());
|
||||
ui.onModelChanged();
|
||||
@@ -810,12 +819,84 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (c != null) {
|
||||
int from = c.channelId;
|
||||
c.channelId = e.getTargetChannelId();
|
||||
if (e.getClientId() == selfClientId) announceOwnMove(safeInt(e, "reasonid"), e);
|
||||
else announceClientMoved(safeInt(e, "reasonid"), e.getClientId(), from, e.getTargetChannelId());
|
||||
if (e.getClientId() == selfClientId) {
|
||||
announceOwnMove(safeInt(e, "reasonid"), e);
|
||||
} else {
|
||||
announceClientMoved(safeInt(e, "reasonid"), e.getClientId(), from, e.getTargetChannelId());
|
||||
logClientMoved(e, c.nickname, from, e.getTargetChannelId());
|
||||
}
|
||||
ui.onModelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** A client became visible to us, logged the way native TS3's server tab does. */
|
||||
private void logClientEntered(ClientJoinEvent e) {
|
||||
String name = e.getClientNickname();
|
||||
switch (safeInt(e, "reasonid")) {
|
||||
case REASON_MOVED:
|
||||
log(name + " appears, coming from channel \"" + channelName(e.getClientFromId()) + "\"");
|
||||
break;
|
||||
case REASON_CHANNEL_KICK:
|
||||
log(name + " appears, was kicked from channel \"" + channelName(e.getClientFromId())
|
||||
+ "\" by " + invokerName(e));
|
||||
break;
|
||||
case REASON_SWITCHED:
|
||||
log(name + " switched to channel \"" + channelName(e.getClientTargetId())
|
||||
+ "\", coming from channel \"" + channelName(e.getClientFromId()) + "\"");
|
||||
break;
|
||||
default:
|
||||
log(name + " connected to channel \"" + channelName(e.getClientTargetId()) + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/** A client stopped being visible to us. */
|
||||
private void logClientLeft(ClientLeaveEvent e, String name) {
|
||||
String reasonMsg = orEmpty(e.get("reasonmsg"));
|
||||
String suffix = reasonMsg.isEmpty() ? "" : " (" + reasonMsg + ")";
|
||||
switch (safeInt(e, "reasonid")) {
|
||||
case REASON_TIMEOUT:
|
||||
log(name + " dropped (ping timeout)");
|
||||
break;
|
||||
case REASON_SERVER_KICK:
|
||||
log(name + " was kicked from the server by " + invokerName(e) + suffix);
|
||||
break;
|
||||
case REASON_BAN:
|
||||
log(name + " was banned from the server by " + invokerName(e) + suffix);
|
||||
break;
|
||||
case REASON_CHANNEL_KICK:
|
||||
log(name + " left: was kicked to channel \"" + channelName(e.getClientTargetId())
|
||||
+ "\" by " + invokerName(e) + suffix);
|
||||
break;
|
||||
case REASON_MOVED:
|
||||
log(name + " left, heading to channel \"" + channelName(e.getClientTargetId()) + "\"");
|
||||
break;
|
||||
case REASON_SWITCHED:
|
||||
log(name + " left, switched to channel \"" + channelName(e.getClientTargetId()) + "\"");
|
||||
break;
|
||||
default:
|
||||
log(name + " disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
/** A client we can see moved between two channels we can see. */
|
||||
private void logClientMoved(ClientMovedEvent e, String name, int fromChannel, int toChannel) {
|
||||
String reasonMsg = orEmpty(e.get("reasonmsg"));
|
||||
String suffix = reasonMsg.isEmpty() ? "" : " (" + reasonMsg + ")";
|
||||
switch (safeInt(e, "reasonid")) {
|
||||
case REASON_MOVED:
|
||||
log(name + " was moved from channel \"" + channelName(fromChannel) + "\" to \""
|
||||
+ channelName(toChannel) + "\" by " + invokerName(e));
|
||||
break;
|
||||
case REASON_CHANNEL_KICK:
|
||||
log(name + " was kicked from channel \"" + channelName(fromChannel) + "\" to \""
|
||||
+ channelName(toChannel) + "\" by " + invokerName(e) + suffix);
|
||||
break;
|
||||
default:
|
||||
log(name + " switched from channel \"" + channelName(fromChannel) + "\" to \""
|
||||
+ channelName(toChannel) + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- who went where: TeamSpeak's reason ids ----
|
||||
|
||||
/** {@code reasonid} of a client view/move notification. */
|
||||
@@ -950,6 +1031,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
ClientEntry c = model.getClient(e.getClientId());
|
||||
if (c == null) return;
|
||||
boolean renamed = has(e, "client_nickname") && !e.get("client_nickname").equals(c.nickname);
|
||||
String oldName = c.nickname;
|
||||
if (renamed) c.nickname = e.get("client_nickname");
|
||||
if (has(e, "client_input_muted")) c.inputMuted = e.getBoolean("client_input_muted");
|
||||
if (has(e, "client_output_muted")) c.outputMuted = e.getBoolean("client_output_muted");
|
||||
@@ -966,12 +1048,12 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (has(e, "client_talk_power")) c.talkPower = e.getInt("client_talk_power");
|
||||
if (has(e, "client_is_channel_commander"))
|
||||
c.channelCommander = e.getBoolean("client_is_channel_commander");
|
||||
announceClientUpdate(e, c, renamed);
|
||||
announceClientUpdate(e, c, renamed, oldName);
|
||||
ui.onModelChanged();
|
||||
}
|
||||
|
||||
/** Renames, talk-power changes and recording flags all arrive as a client update. */
|
||||
private void announceClientUpdate(ClientUpdatedEvent e, ClientEntry c, boolean renamed) {
|
||||
private void announceClientUpdate(ClientUpdatedEvent e, ClientEntry c, boolean renamed, String oldName) {
|
||||
if (!connected) return;
|
||||
boolean self = c.id == selfClientId;
|
||||
Map<String, String> vars = clientVars(c.id, null);
|
||||
@@ -979,6 +1061,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (renamed) {
|
||||
sound(safeInt(e, "invokerid") == selfClientId
|
||||
? SoundEvent.CLIENT_RENAMED_BY_YOU : SoundEvent.CLIENT_RENAMED_BY_OTHER, vars);
|
||||
log(oldName + " is now known as " + c.nickname);
|
||||
}
|
||||
if (safeInt(e, "client_talk_request") > 0 && !self) {
|
||||
sound(SoundEvent.CLIENT_REQUESTED_TALK_POWER, vars);
|
||||
@@ -1012,6 +1095,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (connected) {
|
||||
sound(byInvoker(e, SoundEvent.CHANNEL_CREATED_BY_YOU, SoundEvent.CHANNEL_CREATED_BY_OTHER,
|
||||
SoundEvent.CHANNEL_CREATED_BY_OTHER), channelVars(cid, e.get("invokername")));
|
||||
log("Channel \"" + name + "\" was created by " + invokerName(e));
|
||||
}
|
||||
ui.onModelChanged();
|
||||
}
|
||||
@@ -1022,6 +1106,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (connected) {
|
||||
sound(byInvoker(e, SoundEvent.CHANNEL_DELETED_BY_YOU, SoundEvent.CHANNEL_DELETED_BY_OTHER,
|
||||
SoundEvent.CHANNEL_DELETED_BY_SERVER), channelVars(cid, e.get("invokername")));
|
||||
log("Channel \"" + channelName(cid) + "\" was deleted by " + invokerName(e));
|
||||
}
|
||||
model.removeChannel(cid);
|
||||
ui.onModelChanged();
|
||||
@@ -1042,6 +1127,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
: byInvoker(e, SoundEvent.CHANNEL_EDITED_OTHER_BY_YOU,
|
||||
SoundEvent.CHANNEL_EDITED_OTHER_BY_OTHER, SoundEvent.CHANNEL_EDITED_OTHER_BY_SERVER),
|
||||
channelVars(ch.id, e.get("invokername")));
|
||||
log("Channel \"" + ch.name + "\" was edited by " + invokerName(e));
|
||||
}
|
||||
ui.onModelChanged();
|
||||
}
|
||||
@@ -1057,6 +1143,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (connected) {
|
||||
sound(byInvoker(e, SoundEvent.CHANNEL_MOVED_BY_YOU, SoundEvent.CHANNEL_MOVED_BY_OTHER,
|
||||
SoundEvent.CHANNEL_MOVED_BY_OTHER), channelVars(ch.id, e.get("invokername")));
|
||||
log("Channel \"" + ch.name + "\" was moved by " + invokerName(e));
|
||||
}
|
||||
ui.onModelChanged();
|
||||
}
|
||||
@@ -1098,17 +1185,22 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
: byInvoker(e, SoundEvent.CLIENT_SERVERGROUP_ADDED_BY_USER,
|
||||
SoundEvent.CLIENT_SERVERGROUP_ADDED_BY_USER, SoundEvent.CLIENT_SERVERGROUP_ADDED_BY_SERVER),
|
||||
groupVars(e.getClientId(), e.getName()));
|
||||
log(clientLogName(e.getClientId()) + " was added to server group \"" + e.getName()
|
||||
+ "\" by " + invokerName(e) + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerGroupClientDeleted(ServerGroupClientDeletedEvent e) {
|
||||
boolean self = safeInt(e, "clid") == selfClientId;
|
||||
int clientId = safeInt(e, "clid");
|
||||
boolean self = clientId == selfClientId;
|
||||
sound(self
|
||||
? byInvoker(e, SoundEvent.YOU_SERVERGROUP_REMOVED_BY_USER,
|
||||
SoundEvent.YOU_SERVERGROUP_REMOVED_BY_USER, SoundEvent.YOU_SERVERGROUP_REMOVED_BY_SERVER)
|
||||
: byInvoker(e, SoundEvent.CLIENT_SERVERGROUP_REMOVED_BY_USER,
|
||||
SoundEvent.CLIENT_SERVERGROUP_REMOVED_BY_USER, SoundEvent.CLIENT_SERVERGROUP_REMOVED_BY_SERVER),
|
||||
groupVars(safeInt(e, "clid"), e.get("name")));
|
||||
groupVars(clientId, e.get("name")));
|
||||
log(clientLogName(clientId) + " was removed from server group \"" + orEmpty(e.get("name"))
|
||||
+ "\" by " + invokerName(e) + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1116,15 +1208,24 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
ClientEntry c = model.getClient(e.getClientId());
|
||||
if (c != null) c.channelGroupId = e.getChannelGroupId();
|
||||
boolean self = e.getClientId() == selfClientId;
|
||||
String groupName = model.channelGroupName(e.getChannelGroupId());
|
||||
sound(self
|
||||
? byInvoker(e, SoundEvent.YOU_CHANNELGROUP_CHANGED_BY_USER,
|
||||
SoundEvent.YOU_CHANNELGROUP_CHANGED_BY_USER, SoundEvent.YOU_CHANNELGROUP_CHANGED_BY_SERVER)
|
||||
: byInvoker(e, SoundEvent.CLIENT_CHANNELGROUP_CHANGED_BY_USER,
|
||||
SoundEvent.CLIENT_CHANNELGROUP_CHANGED_BY_USER, SoundEvent.CLIENT_CHANNELGROUP_CHANGED_BY_SERVER),
|
||||
groupVars(e.getClientId(), model.channelGroupName(e.getChannelGroupId())));
|
||||
groupVars(e.getClientId(), groupName));
|
||||
log("Channel group \"" + orEmpty(groupName) + "\" was assigned to " + clientLogName(e.getClientId())
|
||||
+ " by " + invokerName(e) + ".");
|
||||
ui.onModelChanged();
|
||||
}
|
||||
|
||||
/** A client's nickname for a log line, falling back to its id once it has left. */
|
||||
private String clientLogName(int clientId) {
|
||||
ClientEntry c = model.getClient(clientId);
|
||||
return c != null ? c.nickname : "Client " + clientId;
|
||||
}
|
||||
|
||||
/** Picks the event variant matching who caused the change: us, another client, or the server. */
|
||||
private SoundEvent byInvoker(BaseEvent e, SoundEvent byYou, SoundEvent byOther, SoundEvent byServer) {
|
||||
int invoker = safeInt(e, "invokerid");
|
||||
@@ -1583,6 +1684,21 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
sounds.fire(event, deafened, variables);
|
||||
}
|
||||
|
||||
/** Records a line in the server tab's log, the way native TS3 reports server activity. */
|
||||
private void log(String message) {
|
||||
if (connected) ui.onServerLog(message);
|
||||
}
|
||||
|
||||
private String channelName(int channelId) {
|
||||
ChannelNode ch = model.getChannel(channelId);
|
||||
return ch != null ? ch.name : "channel #" + channelId;
|
||||
}
|
||||
|
||||
private static String invokerName(BaseEvent e) {
|
||||
String name = orEmpty(e.get("invokername"));
|
||||
return name.isEmpty() ? "the server" : name;
|
||||
}
|
||||
|
||||
/** The placeholder values a pack may reference for an action involving a client. */
|
||||
private Map<String, String> clientVars(int clientId, String fallbackName) {
|
||||
ClientEntry c = model.getClient(clientId);
|
||||
|
||||
Reference in New Issue
Block a user