Fix Windows channel tree ordering
This commit is contained in:
@@ -150,8 +150,8 @@ public sealed class ChannelEditDialog : Form
|
|||||||
{
|
{
|
||||||
Location = new Point(inputX, y - 2),
|
Location = new Point(inputX, y - 2),
|
||||||
Size = new Size(120, 23),
|
Size = new Size(120, 23),
|
||||||
Minimum = 0,
|
Minimum = int.MinValue,
|
||||||
Maximum = uint.MaxValue,
|
Maximum = int.MaxValue,
|
||||||
Value = existing?.SortOrder ?? 0,
|
Value = existing?.SortOrder ?? 0,
|
||||||
TabIndex = 5,
|
TabIndex = 5,
|
||||||
};
|
};
|
||||||
@@ -385,7 +385,7 @@ public sealed class ChannelEditDialog : Form
|
|||||||
PasswordProtected: _chkPassword.Checked,
|
PasswordProtected: _chkPassword.Checked,
|
||||||
Password: _chkPassword.Checked ? _txtPassword.Text : null,
|
Password: _chkPassword.Checked ? _txtPassword.Text : null,
|
||||||
MaxUsers: (uint)_numMaxUsers.Value,
|
MaxUsers: (uint)_numMaxUsers.Value,
|
||||||
SortOrder: (uint)_numSortOrder.Value,
|
SortOrder: (int)_numSortOrder.Value,
|
||||||
Audio: audio);
|
Audio: audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -575,25 +575,21 @@ public partial class MainForm : Form
|
|||||||
tvChannels.BeginUpdate();
|
tvChannels.BeginUpdate();
|
||||||
tvChannels.Nodes.Clear();
|
tvChannels.Nodes.Clear();
|
||||||
|
|
||||||
var byParent = _channels
|
void AddChildren(TreeNodeCollection nodes, IReadOnlyList<ChannelHierarchyItem> children)
|
||||||
.GroupBy(c => c.ParentId)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
|
|
||||||
void AddChildren(TreeNodeCollection nodes, uint parentId)
|
|
||||||
{
|
{
|
||||||
if (!byParent.TryGetValue(parentId, out var kids)) return;
|
foreach (ChannelHierarchyItem item in children)
|
||||||
foreach (var ch in kids.OrderBy(c => c.Name))
|
|
||||||
{
|
{
|
||||||
|
ChannelInfo ch = item.Channel;
|
||||||
int count = _users.Values.Count(u => u.ChannelId == ch.Id);
|
int count = _users.Values.Count(u => u.ChannelId == ch.Id);
|
||||||
var label = $"{ch.Name} ({count})";
|
var label = $"{ch.Name} ({count})";
|
||||||
if (ch.PasswordProtected) label += " [password]";
|
if (ch.PasswordProtected) label += " [password]";
|
||||||
if (ch.Id == _currentChannelId) label += " ►";
|
if (ch.Id == _currentChannelId) label += " ►";
|
||||||
var node = new TreeNode(label) { Tag = ch.Id };
|
var node = new TreeNode(label) { Tag = ch.Id };
|
||||||
nodes.Add(node);
|
nodes.Add(node);
|
||||||
AddChildren(node.Nodes, ch.Id);
|
AddChildren(node.Nodes, item.Children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AddChildren(tvChannels.Nodes, 0);
|
AddChildren(tvChannels.Nodes, ChannelHierarchy.Build(_channels));
|
||||||
tvChannels.ExpandAll();
|
tvChannels.ExpandAll();
|
||||||
SeekAndSelect(tvChannels.Nodes, toSelect);
|
SeekAndSelect(tvChannels.Nodes, toSelect);
|
||||||
tvChannels.EndUpdate();
|
tvChannels.EndUpdate();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed partial class VoiceCatClient
|
|||||||
}
|
}
|
||||||
private static Voicecat.V1.Channel Channel(ChannelEditInfo info) => new()
|
private static Voicecat.V1.Channel Channel(ChannelEditInfo info) => new()
|
||||||
{
|
{
|
||||||
Id = info.Id, ParentId = info.ParentId, Name = info.Name, Topic = info.Topic, MaxUsers = info.MaxUsers, Order = unchecked((int)info.SortOrder),
|
Id = info.Id, ParentId = info.ParentId, Name = info.Name, Topic = info.Topic, MaxUsers = info.MaxUsers, Order = info.SortOrder,
|
||||||
Audio = new() { Codec = info.Audio.Codec, Mode = info.Audio.Stereo ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = info.Audio.SampleRate,
|
Audio = new() { Codec = info.Audio.Codec, Mode = info.Audio.Stereo ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = info.Audio.SampleRate,
|
||||||
BitrateBps = info.Audio.BitrateBps, FrameMs = info.Audio.FrameMs, Application = (OpusApplication)info.Audio.Application, Complexity = info.Audio.Complexity,
|
BitrateBps = info.Audio.BitrateBps, FrameMs = info.Audio.FrameMs, Application = (OpusApplication)info.Audio.Application, Complexity = info.Audio.Complexity,
|
||||||
Fec = info.Audio.Fec, ExpectedPacketLoss = info.Audio.ExpectedPacketLoss, Dtx = info.Audio.Dtx, Dred = info.Audio.Dred }
|
Fec = info.Audio.Fec, ExpectedPacketLoss = info.Audio.ExpectedPacketLoss, Dtx = info.Audio.Dtx, Dred = info.Audio.Dred }
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace VoiceCat.Windows;
|
||||||
|
|
||||||
|
/// <summary>A channel and its ordered children as displayed by the Windows client.</summary>
|
||||||
|
public sealed record ChannelHierarchyItem(
|
||||||
|
ChannelInfo Channel,
|
||||||
|
IReadOnlyList<ChannelHierarchyItem> Children);
|
||||||
|
|
||||||
|
public static class ChannelHierarchy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the server-defined channel hierarchy. SortOrder orders siblings; LINQ's stable
|
||||||
|
/// ordering preserves the server's sequence when siblings have the same value.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ChannelHierarchyItem> Build(IReadOnlyList<ChannelInfo> channels)
|
||||||
|
{
|
||||||
|
var byParent = channels
|
||||||
|
.GroupBy(channel => channel.ParentId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.OrderBy(channel => channel.SortOrder).ToArray());
|
||||||
|
|
||||||
|
IReadOnlyList<ChannelHierarchyItem> AddChildren(uint parentId)
|
||||||
|
{
|
||||||
|
if (!byParent.TryGetValue(parentId, out ChannelInfo[]? children))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return children
|
||||||
|
.Select(channel => new ChannelHierarchyItem(channel, AddChildren(channel.Id)))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddChildren(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ public sealed record ChannelInfo(
|
|||||||
string Topic,
|
string Topic,
|
||||||
bool PasswordProtected,
|
bool PasswordProtected,
|
||||||
uint MaxUsers,
|
uint MaxUsers,
|
||||||
uint SortOrder,
|
int SortOrder,
|
||||||
AudioConfigInfo Audio);
|
AudioConfigInfo Audio);
|
||||||
|
|
||||||
public sealed record ChannelEditInfo(
|
public sealed record ChannelEditInfo(
|
||||||
@@ -19,7 +19,7 @@ public sealed record ChannelEditInfo(
|
|||||||
bool PasswordProtected,
|
bool PasswordProtected,
|
||||||
string? Password,
|
string? Password,
|
||||||
uint MaxUsers,
|
uint MaxUsers,
|
||||||
uint SortOrder,
|
int SortOrder,
|
||||||
AudioConfigInfo Audio);
|
AudioConfigInfo Audio);
|
||||||
|
|
||||||
public sealed record UserInfo(
|
public sealed record UserInfo(
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ public sealed partial class VoiceCatClient : IDisposable
|
|||||||
}
|
}
|
||||||
public VcResult LeaveVoice() { StopDevices(); local.Clear(); return Request(new() { UnsubscribeVoice = new() }); }
|
public VcResult LeaveVoice() { StopDevices(); local.Clear(); return Request(new() { UnsubscribeVoice = new() }); }
|
||||||
|
|
||||||
public List<ChannelInfo> ListChannels() => core.Channels.Select(c => new ChannelInfo(c.Id, c.ParentId, c.Name, c.Topic, c.PasswordProtected, c.MaxUsers, unchecked((uint)c.Order), Audio(c.Audio))).ToList();
|
public List<ChannelInfo> ListChannels() => core.Channels.Select(c => new ChannelInfo(c.Id, c.ParentId, c.Name, c.Topic, c.PasswordProtected, c.MaxUsers, c.Order, Audio(c.Audio))).ToList();
|
||||||
public List<UserInfo> ListUsers() => core.Users.Select(u => new UserInfo(u.Id, u.Nickname, u.IsGuest, u.ChannelId, u.SelfMicMuted, u.SelfDeafened, u.ServerMuted, u.ServerDeafened, u.VoiceSubscribed)).ToList();
|
public List<UserInfo> ListUsers() => core.Users.Select(u => new UserInfo(u.Id, u.Nickname, u.IsGuest, u.ChannelId, u.SelfMicMuted, u.SelfDeafened, u.ServerMuted, u.ServerDeafened, u.VoiceSubscribed)).ToList();
|
||||||
public List<StreamSummary> ListUserStreams(uint id) => id == core.Authentication?.Self.Id
|
public List<StreamSummary> ListUserStreams(uint id) => id == core.Authentication?.Self.Id
|
||||||
? local.Values.Select(s => new StreamSummary(s.Alias, (VcStreamKind)s.Info.Kind, s.Info.Label)).ToList()
|
? local.Values.Select(s => new StreamSummary(s.Alias, (VcStreamKind)s.Info.Kind, s.Info.Label)).ToList()
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ namespace VoiceCat.Tests;
|
|||||||
|
|
||||||
public class WindowsManagedClientTests
|
public class WindowsManagedClientTests
|
||||||
{
|
{
|
||||||
|
private static readonly AudioConfigInfo DefaultAudio = new(0, false, 48_000, 64_000, 20, 0, true, 10, false, 10, false);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChannelHierarchyUsesServerSortOrderAndKeepsIdsWithDisplayedChannels()
|
||||||
|
{
|
||||||
|
ChannelInfo[] channels =
|
||||||
|
[
|
||||||
|
new(10, 0, "Zulu", "", false, 0, -10, DefaultAudio),
|
||||||
|
new(20, 0, "Alpha", "", false, 0, 20, DefaultAudio),
|
||||||
|
new(11, 10, "Zulu child", "", false, 0, 10, DefaultAudio),
|
||||||
|
new(12, 10, "Alpha child", "", false, 0, 0, DefaultAudio),
|
||||||
|
new(30, 0, "Beta", "", false, 0, 20, DefaultAudio),
|
||||||
|
];
|
||||||
|
|
||||||
|
IReadOnlyList<ChannelHierarchyItem> roots = ChannelHierarchy.Build(channels);
|
||||||
|
|
||||||
|
Assert.Equal([(10U, "Zulu"), (20U, "Alpha"), (30U, "Beta")],
|
||||||
|
roots.Select(item => (item.Channel.Id, item.Channel.Name)));
|
||||||
|
Assert.Equal([(12U, "Alpha child"), (11U, "Zulu child")],
|
||||||
|
roots[0].Children.Select(item => (item.Channel.Id, item.Channel.Name)));
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task Until(Client client, Func<bool> predicate)
|
private static async Task Until(Client client, Func<bool> predicate)
|
||||||
{
|
{
|
||||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
|||||||
Reference in New Issue
Block a user