diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java index 10e0106..aa4f96d 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java @@ -359,13 +359,11 @@ final class ConnectionEventHandler implements TS3Listener { @Override public void onChannelCreate(ChannelCreateEvent e) { int cid = e.getChannelId(); - String name = e.get("channel_name"); int pid = safeInt(e, "cpid"); if (pid == 0) pid = safeInt(e, "pid"); int order = safeInt(e, "channel_order"); - ChannelNode node = conn.getModel().putChannel(cid, name, pid, order); - long icon = TeamspeakConnection.safeLong(e, "channel_icon_id"); - if (icon != 0) node.iconId = icon; + ChannelNode node = conn.getModel().putChannel(cid, e.get("channel_name"), pid, order); + applyChannelProperties(node, e); conn.getModel().relinkChannel(cid, pid, order); if (conn.isConnected()) { conn.sound(byInvoker(e, SoundEvent.CHANNEL_CREATED_BY_YOU, SoundEvent.CHANNEL_CREATED_BY_OTHER, @@ -390,22 +388,35 @@ final class ConnectionEventHandler implements TS3Listener { @Override public void onChannelEdit(ChannelEditedEvent e) { ChannelNode ch = conn.getModel().getChannel(safeInt(e, "cid")); - if (ch != null) { - if (has(e, "channel_name")) ch.name = e.get("channel_name"); - if (has(e, "channel_order")) ch.order = e.getInt("channel_order"); - if (has(e, "channel_icon_id")) ch.iconId = TeamspeakConnection.safeLong(e, "channel_icon_id"); - if (has(e, "channel_topic")) ch.topic = e.get("channel_topic"); - if (conn.isConnected()) { - boolean current = conn.inOwnChannel(ch.id); - conn.sound(current - ? byInvoker(e, SoundEvent.CHANNEL_EDITED_CURRENT_BY_YOU, - SoundEvent.CHANNEL_EDITED_CURRENT_BY_OTHER, SoundEvent.CHANNEL_EDITED_CURRENT_BY_OTHER) - : byInvoker(e, SoundEvent.CHANNEL_EDITED_OTHER_BY_YOU, - SoundEvent.CHANNEL_EDITED_OTHER_BY_OTHER, SoundEvent.CHANNEL_EDITED_OTHER_BY_SERVER), - conn.channelVars(ch.id, e.get("invokername"))); - conn.log("Channel \"" + conn.channelLink(ch.id) + "\" was edited by " + invokerName(e)); - } - conn.ui.onModelChanged(); + if (ch == null) return; + applyChannelProperties(ch, e); + // Sorting among the same siblings arrives here too, and TS3 announces it as an edit. + if (has(e, "channel_order")) conn.getModel().relinkChannel(ch.id, ch.parentId, e.getInt("channel_order")); + if (conn.isConnected()) { + boolean current = conn.inOwnChannel(ch.id); + conn.sound(current + ? byInvoker(e, SoundEvent.CHANNEL_EDITED_CURRENT_BY_YOU, + SoundEvent.CHANNEL_EDITED_CURRENT_BY_OTHER, SoundEvent.CHANNEL_EDITED_CURRENT_BY_OTHER) + : byInvoker(e, SoundEvent.CHANNEL_EDITED_OTHER_BY_YOU, + SoundEvent.CHANNEL_EDITED_OTHER_BY_OTHER, SoundEvent.CHANNEL_EDITED_OTHER_BY_SERVER), + conn.channelVars(ch.id, e.get("invokername"))); + conn.log("Channel \"" + conn.channelLink(ch.id) + "\" was edited by " + invokerName(e)); + } + conn.ui.onModelChanged(); + } + + /** The properties a channel event carries that the model keeps; each only when present. */ + private static void applyChannelProperties(ChannelNode ch, BaseEvent e) { + if (has(e, "channel_name")) ch.name = e.get("channel_name"); + // An emptied topic arrives as a bare key, which has() would skip. + if (e.getMap().containsKey("channel_topic")) ch.topic = TeamspeakConnection.orEmpty(e.get("channel_topic")); + if (has(e, "channel_icon_id")) ch.iconId = TeamspeakConnection.safeLong(e, "channel_icon_id"); + if (has(e, "channel_flag_password")) ch.hasPassword = e.getBoolean("channel_flag_password"); + if (has(e, "channel_flag_permanent")) ch.permanent = e.getBoolean("channel_flag_permanent"); + if (has(e, "channel_flag_maxclients_unlimited") && e.getBoolean("channel_flag_maxclients_unlimited")) { + ch.maxClients = -1; + } else if (has(e, "channel_maxclients")) { + ch.maxClients = safeInt(e, "channel_maxclients"); } } @@ -425,17 +436,18 @@ final class ConnectionEventHandler implements TS3Listener { @Override public void onChannelMoved(ChannelMovedEvent e) { ChannelNode ch = conn.getModel().getChannel(safeInt(e, "cid")); - if (ch != null) { - int parent = has(e, "cpid") ? e.getInt("cpid") : ch.parentId; - int order = has(e, "order") ? e.getInt("order") : ch.order; - conn.getModel().relinkChannel(ch.id, parent, order); - if (conn.isConnected()) { - conn.sound(byInvoker(e, SoundEvent.CHANNEL_MOVED_BY_YOU, SoundEvent.CHANNEL_MOVED_BY_OTHER, - SoundEvent.CHANNEL_MOVED_BY_OTHER), conn.channelVars(ch.id, e.get("invokername"))); - conn.log("Channel \"" + conn.channelLink(ch.id) + "\" was moved by " + invokerName(e)); - } - conn.ui.onModelChanged(); + if (ch == null) return; + int parent = has(e, "cpid") ? e.getInt("cpid") : ch.parentId; + int order = has(e, "order") ? e.getInt("order") : ch.order; + conn.getModel().relinkChannel(ch.id, parent, order); + if (conn.isConnected()) { + conn.sound(byInvoker(e, SoundEvent.CHANNEL_MOVED_BY_YOU, SoundEvent.CHANNEL_MOVED_BY_OTHER, + SoundEvent.CHANNEL_MOVED_BY_OTHER), conn.channelVars(ch.id, e.get("invokername"))); + String newParent = parent == 0 ? conn.getModel().getServerName() : conn.channelLink(parent); + conn.log("Channel \"" + conn.channelLink(ch.id) + "\" was moved by " + invokerName(e) + + ", new parent channel is \"" + newParent + "\""); } + conn.ui.onModelChanged(); } @Override @@ -537,12 +549,9 @@ final class ConnectionEventHandler implements TS3Listener { public void onChannelList(ChannelListEvent e) { // Incremental channel arriving during connect. int cid = e.getChannelId(); - String name = e.get("channel_name"); - int pid = safeInt(e, "cpid"); - int order = safeInt(e, "channel_order"); - ChannelNode node = conn.getModel().putChannel(cid, name, pid, order); - long icon = TeamspeakConnection.safeLong(e, "channel_icon_id"); - if (icon != 0) node.iconId = icon; + ChannelNode node = conn.getModel().putChannel(cid, e.get("channel_name"), + safeInt(e, "cpid"), safeInt(e, "channel_order")); + applyChannelProperties(node, e); } @Override diff --git a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java index a3054a5..dfd191b 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java @@ -708,12 +708,23 @@ public final class TeamspeakConnection implements TS3Listener { * siblings, or 0 to place it first */ public void moveChannel(int channelId, int newParentId, int orderPredecessorId) { + ChannelNode channel = model.getChannel(channelId); + // The server refuses channelmove within the same parent ("already member of + // channel"); sorting among the same siblings is an edit of channel_order. + boolean reorder = channel != null && channel.parentId == newParentId; new Thread(() -> { try { - SingleCommand cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT); - cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); - cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId))); - cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId))); + SingleCommand cmd; + if (reorder) { + cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId))); + } else { + cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId))); + cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId))); + } client.executeCommand(cmd).complete(); } catch (Exception e) { error("Could not move channel: " + rootMessage(e)); diff --git a/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java b/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java index ca8c4ed..83aa69a 100644 --- a/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java +++ b/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java @@ -14,17 +14,27 @@ import java.util.Map; *
An action is heard when the pack maps it, the user left it enabled and either * the speakers are on or the action is marked important — the muted case * is what the important flag exists for. + * + *
A sound that is already starting is not started again: one server action often
+ * arrives as a burst of events (a channel deleted with its subchannels, every tab on
+ * the same server hearing the same move), and stacking identical copies of a sound
+ * only makes it louder and garbled.
*/
public final class SoundNotifier {
/** Contact class in a pack's {@code ${clientType}} placeholder: friend, neutral or blocked. */
private static final String DEFAULT_CLIENT_TYPE = "neutral";
+ /** How close together two plays of the same sound count as one. */
+ private static final long COALESCE_NANOS = 250_000_000L;
+
private final Settings settings;
private volatile SoundPlayer player;
private volatile SoundPack pack;
private volatile List