Files
voice-cat/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs
Talon 65736df464 fix(windows): polish hotkeys, user list, titlebar, and PM window
- Suppress the system ding on global hotkeys/PTT and the user-list Enter
  key by setting SuppressKeyPress (Handled alone leaves WM_CHAR to beep).
- Preserve the user-list keyboard selection across talking/mute refreshes
  instead of resetting it on every Items.Clear().
- Include the connected server name in the main window titlebar.
- Close the private-message window on Escape.
- Show the PM window without an owner so focus is no longer trapped to it
  and the main window can be worked in while a PM is open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:30:48 +02:00

285 lines
11 KiB
C#

using VoiceCat.App.Models;
using VoiceCat.Interop;
namespace VoiceCat.App.Forms;
/// <summary>
/// Saved-server list + connect flow. On success, ConnectedClient/SelfUserId/Nickname are set
/// and DialogResult == OK; the caller (Program.cs) takes ownership of ConnectedClient (does
/// NOT dispose it here — MainForm owns its lifetime from that point on).
/// </summary>
public partial class ConnectDialog : Form
{
private readonly List<SavedServer> _servers;
private readonly System.Windows.Forms.Timer _pumpTimer = new() { Interval = 30 };
private VoiceCatClient? _client;
private bool _identityDialogShown;
public VoiceCatClient? ConnectedClient { get; private set; }
public uint SelfUserId { get; private set; }
public string Nickname { get; private set; } = "";
public string ServerName { get; private set; } = "";
public ConnectDialog()
{
InitializeComponent();
_servers = ServerListStore.Load();
RefreshServerList();
_pumpTimer.Tick += (_, _) =>
{
try { _client?.PumpEvents(); }
catch (Exception ex) { Console.Error.WriteLine($"[ConnectDialog] EXCEPTION in PumpEvents/event handler: {ex}"); }
};
btnAddNew.Click += BtnAddNew_Click;
btnEdit.Click += BtnEdit_Click;
btnRemove.Click += BtnRemove_Click;
btnConnect.Click += BtnConnect_Click;
lstServers.DoubleClick += BtnConnect_Click;
}
private void RefreshServerList()
{
object? previouslySelected = lstServers.SelectedItem;
lstServers.Items.Clear();
foreach (var s in _servers) lstServers.Items.Add(s);
if (previouslySelected is not null && _servers.Contains(previouslySelected))
lstServers.SelectedItem = previouslySelected;
else if (lstServers.Items.Count > 0)
lstServers.SelectedIndex = 0;
UpdateButtonsEnabled();
}
private void UpdateButtonsEnabled()
{
bool hasSelection = lstServers.SelectedItem is not null;
btnConnect.Enabled = hasSelection;
btnEdit.Enabled = hasSelection;
btnRemove.Enabled = hasSelection;
}
private void BtnAddNew_Click(object? sender, EventArgs e)
{
using var dlg = new AddServerDialog();
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
_servers.Add(dlg.Result);
ServerListStore.Save(_servers);
RefreshServerList();
lstServers.SelectedItem = dlg.Result;
}
private void BtnEdit_Click(object? sender, EventArgs e)
{
if (lstServers.SelectedItem is not SavedServer existing) return;
using var dlg = new AddServerDialog(existing);
if (dlg.ShowDialog(this) != DialogResult.OK) return;
ServerListStore.Save(_servers);
RefreshServerList();
}
private void BtnRemove_Click(object? sender, EventArgs e)
{
if (lstServers.SelectedItem is not SavedServer existing) return;
var confirm = MessageBox.Show(this, $"Remove '{existing}' from the saved-server list?",
"VoiceCat", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (confirm != DialogResult.Yes) return;
_servers.Remove(existing);
ServerListStore.Save(_servers);
RefreshServerList();
}
private void BtnConnect_Click(object? sender, EventArgs e)
{
Console.WriteLine("[ConnectDialog] BtnConnect_Click fired");
if (lstServers.SelectedItem is not SavedServer server)
{
Console.WriteLine("[ConnectDialog] no SavedServer selected — ignoring click");
return;
}
Console.WriteLine($"[ConnectDialog] selected server: Host={server.Host} Port={server.Port} AuthMode={server.AuthMode}");
try
{
StartConnect(server);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ConnectDialog] EXCEPTION in StartConnect: {ex}");
lblStatus.Text = $"Internal error: {ex.Message}";
CleanupFailedAttempt();
}
}
private void StartConnect(SavedServer server)
{
SetBusy(true);
lblStatus.Text = "Connecting...";
ServerName = string.IsNullOrWhiteSpace(server.DisplayName)
? $"{server.Host}:{server.Port}"
: server.DisplayName;
string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!;
Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}");
Directory.CreateDirectory(tofuDir);
Console.WriteLine("[ConnectDialog] creating VoiceCatClient...");
_client = new VoiceCatClient("VoiceCat-Windows", VoiceCatClient.VersionString,
VcLogLevel.Info, ServerListStore.TofuStorePath);
Console.WriteLine("[ConnectDialog] VoiceCatClient created OK");
_client.EventReceived += OnEvent;
_identityDialogShown = false;
_pumpTimer.Start();
Console.WriteLine($"[ConnectDialog] pump timer started, Enabled={_pumpTimer.Enabled}, Interval={_pumpTimer.Interval}");
Console.WriteLine($"[ConnectDialog] calling Connect({server.Host}, {server.Port})...");
var connectResult = _client.Connect(server.Host, server.Port);
Console.WriteLine($"[ConnectDialog] Connect() returned {connectResult}");
if (connectResult != VcResult.Ok)
{
lblStatus.Text = $"Connect failed: {connectResult}";
CleanupFailedAttempt();
return;
}
if (server.AuthMode == AuthMode.Guest)
{
Nickname = string.IsNullOrWhiteSpace(server.LastNickname) ? Environment.UserName : server.LastNickname;
Console.WriteLine($"[ConnectDialog] calling AuthenticateGuest({Nickname})...");
var authResult = _client.AuthenticateGuest(Nickname);
Console.WriteLine($"[ConnectDialog] AuthenticateGuest() returned {authResult}");
}
else
{
string password;
if (server.ProtectedPasswordBase64 is not null)
{
password = PasswordProtector.Unprotect(server.ProtectedPasswordBase64);
}
else
{
using var pwDlg = new PasswordPromptDialog($"Password for {server.SavedUsername}@{server.Host}:");
if (pwDlg.ShowDialog(this) != DialogResult.OK)
{
lblStatus.Text = "Cancelled.";
CleanupFailedAttempt();
return;
}
password = pwDlg.Password;
}
Nickname = server.SavedUsername ?? "";
Console.WriteLine($"[ConnectDialog] calling AuthenticateUser({Nickname})...");
var authResult = _client.AuthenticateUser(server.SavedUsername ?? "", password);
Console.WriteLine($"[ConnectDialog] AuthenticateUser() returned {authResult}");
}
}
private void OnEvent(VoiceCatEvent ev)
{
Console.WriteLine($"[ConnectDialog] event: {ev}");
switch (ev.Type)
{
case VcEventType.ConnectionState:
lblStatus.Text = ev.ConnectionState switch
{
VcConnectionState.Connecting => "Connecting...",
VcConnectionState.TlsHandshake => "TLS handshake...",
VcConnectionState.VerifyingIdentity => "Verifying server identity...",
VcConnectionState.Authenticating => "Authenticating...",
VcConnectionState.Connected => "Connected.",
_ => lblStatus.Text,
};
break;
case VcEventType.ServerIdentity:
HandleServerIdentity((VcTofuStatus)ev.U32a, ev.Text ?? "");
break;
case VcEventType.AuthResult:
if (ev.Result == VcResult.Ok)
{
SelfUserId = ev.UserId;
ConnectedClient = _client;
_pumpTimer.Stop();
_client!.EventReceived -= OnEvent;
DialogResult = DialogResult.OK;
Close();
}
else
{
lblStatus.Text = $"Authentication failed: {ev.Text}";
CleanupFailedAttempt();
}
break;
case VcEventType.Disconnected:
if (ConnectedClient is null)
{
lblStatus.Text = string.IsNullOrEmpty(ev.Text) ? "Disconnected." : $"Disconnected: {ev.Text}";
CleanupFailedAttempt();
}
break;
}
}
private void HandleServerIdentity(VcTofuStatus status, string certFingerprintHex)
{
Console.WriteLine($"[ConnectDialog] HandleServerIdentity status={status} fp={certFingerprintHex} alreadyShown={_identityDialogShown}");
if (_identityDialogShown) return; // one decision per connect attempt
if (status == VcTofuStatus.Matched)
{
// Silent success path — no dialog. See ServerIdentityDialog's doc comment.
Console.WriteLine("[ConnectDialog] status=Matched -> auto-confirming, no dialog");
_client!.ConfirmServerIdentity(true);
return;
}
_identityDialogShown = true;
Console.WriteLine("[ConnectDialog] showing ServerIdentityDialog...");
using var dlg = new ServerIdentityDialog(status, certFingerprintHex, _client!.GetServerIdentityDisplay());
var dlgResult = dlg.ShowDialog(this);
Console.WriteLine($"[ConnectDialog] ServerIdentityDialog closed with {dlgResult}");
bool accept = dlgResult == DialogResult.OK;
var confirmResult = _client.ConfirmServerIdentity(accept);
Console.WriteLine($"[ConnectDialog] ConfirmServerIdentity({accept}) returned {confirmResult}");
if (!accept) lblStatus.Text = "Server identity rejected.";
}
private void CleanupFailedAttempt()
{
_pumpTimer.Stop();
if (_client is not null)
{
_client.EventReceived -= OnEvent;
_client.Dispose();
_client = null;
}
SetBusy(false);
}
private void SetBusy(bool busy)
{
lstServers.Enabled = !busy;
btnConnect.Enabled = !busy && lstServers.SelectedItem is not null;
btnAddNew.Enabled = !busy;
btnEdit.Enabled = !busy && lstServers.SelectedItem is not null;
btnRemove.Enabled = !busy && lstServers.SelectedItem is not null;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (DialogResult != DialogResult.OK)
{
_pumpTimer.Stop();
if (_client is not null)
{
_client.EventReceived -= OnEvent;
_client.Dispose();
_client = null;
}
}
base.OnFormClosing(e);
}
}