M5: Windows client moderation UI; add C ABI getters for account list, user mute/deafen, channel topic
This commit is contained in:
@@ -19,10 +19,13 @@ public partial class MainForm : Form
|
||||
private List<ChannelInfo> _channels = [];
|
||||
private readonly Dictionary<uint, UserInfo> _users = [];
|
||||
private readonly HashSet<uint> _talkingUsers = [];
|
||||
private PermissionsInfo _ownPermissions = new(false, false, false, false, false, false);
|
||||
|
||||
// Voice state
|
||||
private uint _micStreamId; // 0 = not started
|
||||
private Keys _pttKey = Keys.F8;
|
||||
private bool _serverMuted;
|
||||
private bool _serverDeafened;
|
||||
|
||||
public MainForm(VoiceCatClient client, uint selfUserId, string nickname)
|
||||
{
|
||||
@@ -36,19 +39,18 @@ public partial class MainForm : Form
|
||||
_client.EventReceived += OnEvent;
|
||||
_client.LevelChanged += OnLevelChanged;
|
||||
_pumpTimer.Tick += (_, _) => _client.PumpEvents();
|
||||
|
||||
// M5: load own permissions and build permission-aware menus.
|
||||
_ownPermissions = SafeGetPermissions();
|
||||
BuildMenus();
|
||||
BuildChannelContextMenu();
|
||||
BuildUserContextMenu();
|
||||
|
||||
_pumpTimer.Start();
|
||||
|
||||
// Channel tree
|
||||
tvChannels.DoubleClick += TvChannels_DoubleClick;
|
||||
tvChannels.KeyDown += TvChannels_KeyDown;
|
||||
|
||||
// User list — double-click or Enter for per-user tuning, right-click for context menu
|
||||
var ctxUsers = new ContextMenuStrip();
|
||||
var miTune = new ToolStripMenuItem("&Adjust volume and noise settings...");
|
||||
miTune.Click += (_, _) => OpenUserTuning();
|
||||
ctxUsers.Opening += (_, _) => miTune.Enabled = lstUsers.SelectedItem is UserListItem;
|
||||
ctxUsers.Items.Add(miTune);
|
||||
lstUsers.ContextMenuStrip = ctxUsers;
|
||||
lstUsers.DoubleClick += (_, _) => OpenUserTuning();
|
||||
lstUsers.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) OpenUserTuning(); };
|
||||
|
||||
@@ -89,7 +91,11 @@ public partial class MainForm : Form
|
||||
foreach (var u in users)
|
||||
{
|
||||
_users[u.Id] = u;
|
||||
if (u.Id == _selfUserId) _currentChannelId = u.ChannelId;
|
||||
if (u.Id == _selfUserId)
|
||||
{
|
||||
_currentChannelId = u.ChannelId;
|
||||
UpdateSelfServerMuteState(u.ServerMuted, u.ServerDeafened);
|
||||
}
|
||||
}
|
||||
RefreshChannelTree();
|
||||
RefreshUserList();
|
||||
@@ -106,6 +112,105 @@ public partial class MainForm : Form
|
||||
LoadInputDevices();
|
||||
}
|
||||
|
||||
// ── Menu / context menu builders (M5) ─────────────────────────────────────
|
||||
|
||||
private PermissionsInfo SafeGetPermissions()
|
||||
{
|
||||
try { return _client.GetPermissions(); }
|
||||
catch { return new PermissionsInfo(false, false, false, false, false, false); }
|
||||
}
|
||||
|
||||
private void BuildMenus()
|
||||
{
|
||||
if (!_ownPermissions.CanAdminAccounts) return;
|
||||
|
||||
var adminMenu = new ToolStripMenuItem("&Admin");
|
||||
var miAccounts = new ToolStripMenuItem("&Server accounts...");
|
||||
miAccounts.Click += (_, _) =>
|
||||
{
|
||||
using var dlg = new AccountsDialog(_client);
|
||||
dlg.ShowDialog(this);
|
||||
};
|
||||
adminMenu.DropDownItems.Add(miAccounts);
|
||||
menuStrip.Items.Add(adminMenu);
|
||||
}
|
||||
|
||||
private void BuildChannelContextMenu()
|
||||
{
|
||||
var ctx = new ContextMenuStrip();
|
||||
ctx.Opening += (_, _) =>
|
||||
{
|
||||
ctx.Items.Clear();
|
||||
bool hasSelection = tvChannels.SelectedNode?.Tag is uint;
|
||||
bool canCreate = _ownPermissions.CanCreateTempChannel || _ownPermissions.IsAdmin;
|
||||
bool isAdmin = _ownPermissions.IsAdmin;
|
||||
|
||||
if (hasSelection)
|
||||
{
|
||||
ctx.Items.Add("&Join", null, (_, _) => ChannelTreeJoinSelected());
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
}
|
||||
|
||||
if (canCreate)
|
||||
{
|
||||
ctx.Items.Add("&Create channel...", null, (_, _) => CreateChannel());
|
||||
}
|
||||
|
||||
if (hasSelection && isAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Edit channel...", null, (_, _) => EditSelectedChannel());
|
||||
ctx.Items.Add("&Delete channel...", null, (_, _) => DeleteSelectedChannel());
|
||||
}
|
||||
};
|
||||
tvChannels.ContextMenuStrip = ctx;
|
||||
}
|
||||
|
||||
private void BuildUserContextMenu()
|
||||
{
|
||||
var ctx = new ContextMenuStrip();
|
||||
ctx.Opening += (_, _) =>
|
||||
{
|
||||
ctx.Items.Clear();
|
||||
if (lstUsers.SelectedItem is not UserListItem item) return;
|
||||
if (!_users.TryGetValue(item.UserId, out var user)) return;
|
||||
bool isSelf = user.Id == _selfUserId;
|
||||
|
||||
var miTune = new ToolStripMenuItem("&Adjust volume and noise settings...");
|
||||
miTune.Click += (_, _) => OpenUserTuning();
|
||||
ctx.Items.Add(miTune);
|
||||
|
||||
if (!isSelf)
|
||||
{
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
|
||||
if (_ownPermissions.CanMoveUsers || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Move to channel...", null, (_, _) => MoveSelectedUser());
|
||||
}
|
||||
if (_ownPermissions.CanKick || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Kick...", null, (_, _) => KickSelectedUser());
|
||||
}
|
||||
if (_ownPermissions.CanBan || _ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add("&Ban...", null, (_, _) => BanSelectedUser());
|
||||
}
|
||||
|
||||
ctx.Items.Add(new ToolStripSeparator());
|
||||
|
||||
if (_ownPermissions.IsAdmin)
|
||||
{
|
||||
ctx.Items.Add(user.ServerMuted ? "Server &unmute" : "Server &mute",
|
||||
null, (_, _) => ToggleServerMuteSelected());
|
||||
ctx.Items.Add(user.ServerDeafened ? "Server un&deafen" : "Server &deafen",
|
||||
null, (_, _) => ToggleServerDeafenSelected());
|
||||
ctx.Items.Add("&Set permissions...", null, (_, _) => SetPermissionsSelected());
|
||||
}
|
||||
}
|
||||
};
|
||||
lstUsers.ContextMenuStrip = ctx;
|
||||
}
|
||||
|
||||
// ── Event dispatch ────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEvent(VoiceCatEvent ev)
|
||||
@@ -127,6 +232,12 @@ public partial class MainForm : Form
|
||||
case VcEventType.JoinResult:
|
||||
HandleJoinResult(ev);
|
||||
break;
|
||||
case VcEventType.GenericResult:
|
||||
HandleGenericResult(ev);
|
||||
break;
|
||||
case VcEventType.AccountList:
|
||||
AddActivity("Account list updated");
|
||||
break;
|
||||
case VcEventType.TextMessage:
|
||||
HandleTextMessage(ev);
|
||||
break;
|
||||
@@ -166,7 +277,8 @@ public partial class MainForm : Form
|
||||
|
||||
private void HandleUserJoined(VoiceCatEvent ev)
|
||||
{
|
||||
var user = new UserInfo(ev.UserId, ev.Text ?? $"User#{ev.UserId}", false, ev.ChannelId);
|
||||
var user = new UserInfo(ev.UserId, ev.Text ?? $"User#{ev.UserId}", false, ev.ChannelId,
|
||||
false, false, false, false);
|
||||
_users[ev.UserId] = user;
|
||||
RefreshUserList();
|
||||
RebuildScopeCombo();
|
||||
@@ -192,7 +304,11 @@ public partial class MainForm : Form
|
||||
foreach (var u in users)
|
||||
{
|
||||
_users[u.Id] = u;
|
||||
if (u.Id == _selfUserId) _currentChannelId = u.ChannelId;
|
||||
if (u.Id == _selfUserId)
|
||||
{
|
||||
_currentChannelId = u.ChannelId;
|
||||
UpdateSelfServerMuteState(u.ServerMuted, u.ServerDeafened);
|
||||
}
|
||||
}
|
||||
RefreshChannelTree();
|
||||
RefreshUserList();
|
||||
@@ -221,6 +337,13 @@ public partial class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGenericResult(VoiceCatEvent ev)
|
||||
{
|
||||
string prefix = ev.Result == VcResult.Ok ? "Success" : "Failed";
|
||||
string detail = !string.IsNullOrEmpty(ev.Text) ? $": {ev.Text}" : "";
|
||||
AddActivity($"{prefix}{detail} ({ev.Result})");
|
||||
}
|
||||
|
||||
private void HandleTextMessage(VoiceCatEvent ev)
|
||||
{
|
||||
string time = ev.TimestampUnixMs > 0
|
||||
@@ -341,6 +464,8 @@ public partial class MainForm : Form
|
||||
string label = user.Nickname;
|
||||
if (user.Id == _selfUserId) label += " (you)";
|
||||
if (_talkingUsers.Contains(user.Id)) label += " (talking)";
|
||||
if (user.SelfMicMuted || user.ServerMuted) label += " (muted)";
|
||||
if (user.SelfDeafened || user.ServerDeafened) label += " (deafened)";
|
||||
lstUsers.Items.Add(new UserListItem(user.Id, label));
|
||||
}
|
||||
lstUsers.EndUpdate();
|
||||
@@ -376,15 +501,19 @@ public partial class MainForm : Form
|
||||
|
||||
private void UpdateStatusLabel()
|
||||
{
|
||||
string suffix = "";
|
||||
if (_serverMuted) suffix += " [server muted]";
|
||||
if (_serverDeafened) suffix += " [server deafened]";
|
||||
|
||||
if (_currentChannelId == 0)
|
||||
{
|
||||
lblStatus.Text = $"Connected as {_nickname} — not in a channel.";
|
||||
lblStatus.Text = $"Connected as {_nickname}{suffix} — not in a channel.";
|
||||
return;
|
||||
}
|
||||
string chanName = _channels.FirstOrDefault(c => c.Id == _currentChannelId)?.Name
|
||||
?? $"Channel #{_currentChannelId}";
|
||||
int count = _users.Values.Count(u => u.ChannelId == _currentChannelId);
|
||||
lblStatus.Text = $"Connected as {_nickname} — {chanName} ({count} user{(count == 1 ? "" : "s")})";
|
||||
lblStatus.Text = $"Connected as {_nickname}{suffix} — {chanName} ({count} user{(count == 1 ? "" : "s")})";
|
||||
}
|
||||
|
||||
// ── Device management ─────────────────────────────────────────────────────
|
||||
@@ -591,6 +720,130 @@ public partial class MainForm : Form
|
||||
_client.JoinChannel(channelId, password);
|
||||
}
|
||||
|
||||
private void ChannelTreeJoinSelected()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is uint channelId)
|
||||
JoinChannelRequest(channelId);
|
||||
}
|
||||
|
||||
// ── M5: Moderation helpers ────────────────────────────────────────────────
|
||||
|
||||
private void UpdateSelfServerMuteState(bool muted, bool deafened)
|
||||
{
|
||||
bool wasMuted = _serverMuted;
|
||||
bool wasDeafened = _serverDeafened;
|
||||
_serverMuted = muted;
|
||||
_serverDeafened = deafened;
|
||||
|
||||
if (muted && !wasMuted) AddActivity("You have been server-muted");
|
||||
if (deafened && !wasDeafened) AddActivity("You have been server-deafened");
|
||||
if (!muted && wasMuted) AddActivity("Server mute cleared");
|
||||
if (!deafened && wasDeafened) AddActivity("Server deafen cleared");
|
||||
|
||||
UpdateStatusLabel();
|
||||
}
|
||||
|
||||
private void CreateChannel()
|
||||
{
|
||||
using var dlg = new ChannelEditDialog(_channels);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.CreateChannel(dlg.Result);
|
||||
}
|
||||
|
||||
private void EditSelectedChannel()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is not uint channelId) return;
|
||||
var channel = _channels.FirstOrDefault(c => c.Id == channelId);
|
||||
if (channel is null) return;
|
||||
|
||||
var editInfo = new ChannelEditInfo(
|
||||
channel.Id,
|
||||
channel.ParentId,
|
||||
channel.Name,
|
||||
channel.Topic,
|
||||
channel.PasswordProtected,
|
||||
null,
|
||||
channel.MaxUsers,
|
||||
0,
|
||||
new AudioConfigInfo(0, false, 48000, 0, 20, 0, true, 0, false, 10));
|
||||
|
||||
using var dlg = new ChannelEditDialog(_channels, editInfo);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.EditChannel(dlg.Result);
|
||||
}
|
||||
|
||||
private void DeleteSelectedChannel()
|
||||
{
|
||||
if (tvChannels.SelectedNode?.Tag is not uint channelId) return;
|
||||
var channel = _channels.FirstOrDefault(c => c.Id == channelId);
|
||||
if (channel is null) return;
|
||||
|
||||
var confirm = MessageBox.Show(this, $"Delete channel \"{channel.Name}\"?",
|
||||
"VoiceCat", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (confirm != DialogResult.Yes) return;
|
||||
|
||||
_client.DeleteChannel(channelId);
|
||||
}
|
||||
|
||||
private UserInfo? SelectedUser()
|
||||
{
|
||||
if (lstUsers.SelectedItem is not UserListItem item) return null;
|
||||
_users.TryGetValue(item.UserId, out var user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void MoveSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new MoveUserDialog(_channels, user.ChannelId);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
_client.MoveUser(user.Id, dlg.SelectedChannelId);
|
||||
}
|
||||
|
||||
private void KickSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new InputDialog("Kick user", "&Reason:", "Kicked by admin");
|
||||
string? reason = dlg.ShowDialog(this) == DialogResult.OK ? dlg.TextValue : null;
|
||||
_client.KickUser(user.Id, reason);
|
||||
}
|
||||
|
||||
private void BanSelectedUser()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
using var dlg = new BanUserDialog(user.Nickname);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||||
_client.BanUser(user.Id, dlg.Reason, dlg.ExpiresUnixMs);
|
||||
}
|
||||
|
||||
private void ToggleServerMuteSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
_client.SetServerMute(user.Id, !user.ServerMuted, user.ServerDeafened);
|
||||
}
|
||||
|
||||
private void ToggleServerDeafenSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
_client.SetServerMute(user.Id, user.ServerMuted, !user.ServerDeafened);
|
||||
}
|
||||
|
||||
private void SetPermissionsSelected()
|
||||
{
|
||||
var user = SelectedUser();
|
||||
if (user is null) return;
|
||||
// The C ABI does not expose a user's current permissions, so start unchecked.
|
||||
var current = new PermissionsInfo(false, false, false, false, false, false);
|
||||
using var dlg = new PermissionsDialog(user.Nickname, current);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
_client.SetPermission(user.Id, dlg.Result);
|
||||
}
|
||||
|
||||
// ── Per-user tuning ───────────────────────────────────────────────────────
|
||||
|
||||
private void OpenUserTuning()
|
||||
|
||||
Reference in New Issue
Block a user