Move clients to another channel or our own from the Android client sheet

"Move to channel…" closes the sheet and marks the client, and the next
channel tapped is where they go; a bar above the tree names them and cancels.
"Move to own channel" shows while they are elsewhere.

Both show only with some i_client_move_power. The server's needed
permissions never carry it, even for an admin, so core asks for it with
permget on connect and whenever our own groups change; a refusal (guests may
not ask) counts as none. Whether it beats the client's needed move power only
the server knows, and it refuses when not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 22:16:21 +00:00
parent cd02e4b2d8
commit b1ad403fdf
7 changed files with 90 additions and 2 deletions

View File

@@ -506,6 +506,7 @@ final class ConnectionEventHandler implements TS3Listener {
ClientEntry c = conn.getModel().getClient(e.getClientId());
if (c != null) c.addServerGroup(e.getServerGroupId());
boolean self = e.getClientId() == conn.getSelfClientId();
if (self) conn.requestMovePower();
conn.sound(self
? byInvoker(e, SoundEvent.YOU_SERVERGROUP_ADDED_BY_USER, SoundEvent.YOU_SERVERGROUP_ADDED_BY_USER,
SoundEvent.YOU_SERVERGROUP_ADDED_BY_SERVER)
@@ -523,6 +524,7 @@ final class ConnectionEventHandler implements TS3Listener {
ClientEntry c = conn.getModel().getClient(clientId);
if (c != null) c.removeServerGroup(e.getServerGroupId());
boolean self = clientId == conn.getSelfClientId();
if (self) conn.requestMovePower();
conn.sound(self
? byInvoker(e, SoundEvent.YOU_SERVERGROUP_REMOVED_BY_USER,
SoundEvent.YOU_SERVERGROUP_REMOVED_BY_USER, SoundEvent.YOU_SERVERGROUP_REMOVED_BY_SERVER)
@@ -539,6 +541,7 @@ final class ConnectionEventHandler implements TS3Listener {
ClientEntry c = conn.getModel().getClient(e.getClientId());
if (c != null) c.channelGroupId = e.getChannelGroupId();
boolean self = e.getClientId() == conn.getSelfClientId();
if (self) conn.requestMovePower();
String groupName = conn.getModel().channelGroupName(e.getChannelGroupId());
conn.sound(self
? byInvoker(e, SoundEvent.YOU_CHANNELGROUP_CHANGED_BY_USER,

View File

@@ -32,6 +32,8 @@ public final class ServerModel {
private int nextPermissionId = FIRST_PERMISSION_ID;
/** Permission id -> the local client's resolved value, from {@code notifyclientneededpermissions}. */
private final Map<Integer, Integer> selfPermissionValues = new LinkedHashMap<>();
/** The local client's {@code i_client_move_power}, which the needed permissions leave out; 0 until known. */
private int selfMovePower;
private String serverName = "TeamSpeak Server";
/** The {@code virtualserver_*} properties from {@code initserver}, kept up to date by server edits. */
private final Map<String, String> serverProperties = new HashMap<>();
@@ -47,6 +49,7 @@ public final class ServerModel {
permissionIds.clear();
nextPermissionId = FIRST_PERMISSION_ID;
selfPermissionValues.clear();
selfMovePower = 0;
serverProperties.clear();
defaultChannelGroupId = 0;
}
@@ -154,6 +157,14 @@ public final class ServerModel {
return value == null ? 0 : value;
}
public synchronized void setSelfMovePower(int power) {
selfMovePower = power;
}
public synchronized int selfMovePower() {
return selfMovePower;
}
// ---- what the local client may do ----
/**

View File

@@ -352,6 +352,7 @@ public final class TeamspeakConnection implements TS3Listener {
loadChatLogs();
ui.onConnected();
requestPermissionNames();
requestMovePower();
ui.onStatus("Retrieving channels…");
syncAll();
@@ -728,6 +729,23 @@ public final class TeamspeakConnection implements TS3Listener {
attempt(sock -> sock.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete());
}
/**
* Asks for our own {@code i_client_move_power}, which {@code notifyclientneededpermissions}
* never carries. Asking needs {@code b_client_permissionoverview_own}; refused, we take it
* as none, which is what the groups that lack the one usually have of the other.
*/
void requestMovePower() {
submit(sock -> {
int power = 0;
for (SingleCommand answer : sock.executeCommand(new SingleCommand("permget", ProtocolRole.CLIENT,
new CommandSingleParameter("permsid", "i_client_move_power"))).get()) {
String value = answer.toMap().get("permvalue");
if (value != null) power = Integer.parseInt(value);
}
model.setSelfMovePower(power);
}, e -> model.setSelfMovePower(0));
}
/** Assigns or removes a server group for a client (by database id, as {@code servergroupaddclient} needs). */
public void setClientServerGroup(int clientDatabaseId, int groupId, boolean assign) {
act("Could not " + (assign ? "assign" : "remove") + " server group: ", sock -> {

View File

@@ -383,6 +383,14 @@ public final class ServerSession {
return !model.hasPermissionNames() || model.selfPermissionValue("b_client_ban_list") > 0;
}
/**
* Whether we have any {@code i_client_move_power}; whether it beats a client's needed
* move power only the server knows, so it is left to refuse.
*/
public boolean canMoveClients() {
return conn.isConnected() && conn.getModel().selfMovePower() != 0;
}
// ---- connection events ----
private void setStatus(String text) {