Prompt for protected macOS channels
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
This commit is contained in:
+5
-3
@@ -24,11 +24,13 @@ up instantly. Newest status at the top.
|
|||||||
server profiles now persist validated host, port, authentication mode and identity through
|
server profiles now persist validated host, port, authentication mode and identity through
|
||||||
`VoiceCat.Core`; malformed profile files do not prevent startup, writes replace atomically,
|
`VoiceCat.Core`; malformed profile files do not prevent startup, writes replace atomically,
|
||||||
and passwords are deliberately excluded from the model and JSON. Three behavior tests cover
|
and passwords are deliberately excluded from the model and JSON. Three behavior tests cover
|
||||||
profile round trips, corrupt input and account validation. Workload installation remains
|
profile round trips, corrupt input and account validation. Protected channels are labeled
|
||||||
|
and prompt for a session-only password before interrupting active voice; cancel and rejected
|
||||||
|
joins restore the selected channel and voice session. Workload installation remains
|
||||||
blocked by this Windows machine's unrelated Visual Studio
|
blocked by this Windows machine's unrelated Visual Studio
|
||||||
iOS/Android MSI repair failure, so device execution is not claimed. **Next:** run the
|
iOS/Android MSI repair failure, so device execution is not claimed. **Next:** run the
|
||||||
macOS CI/device capture-playback test and ten-minute listen gate, then add protected-channel
|
macOS CI/device capture-playback test and ten-minute listen gate, then add optional Keychain
|
||||||
prompts, optional Keychain credentials, non-default devices, private messages,
|
credentials, non-default devices, private messages,
|
||||||
moderation/settings, ScreenCaptureKit,
|
moderation/settings, ScreenCaptureKit,
|
||||||
VoiceOver verification, signing and notarization.
|
VoiceOver verification, signing and notarization.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
`VoiceCat.Mac` is the native AppKit C# port. It targets `net10.0-macos` and references the same `VoiceCat.Core` and `VoiceCat.Audio` assemblies used by the Windows client and managed CLI.
|
`VoiceCat.Mac` is the native AppKit C# port. It targets `net10.0-macos` and references the same `VoiceCat.Core` and `VoiceCat.Audio` assemblies used by the Windows client and managed CLI.
|
||||||
|
|
||||||
The current checkpoint is a functional client: AppKit launch/menu lifecycle, saved server profiles, guest and account authentication, explicit TOFU approval (including changed-key warning), channel selection, user roster, channel text, voice subscription, managed microphone-stream lifecycle, default-device Core Audio capture/playback, disconnect reporting, native accessibility labels, sandbox/network/audio entitlements, and deterministic disposal. Profiles save the host, port, authentication mode, username or guest nickname; account passwords remain session-only and are never written to the profile JSON. Capture is converted to interleaved 48 kHz int16 PCM through `AVAudioConverter`; playback uses an `AVAudioSourceNode` and the shared bounded `PcmRing`, so its render callback does not allocate, lock, or block. It does not yet replace the Swift release. Protected-channel prompts, optional Keychain credentials, non-default device selection, moderation sheets, private messages, settings, ScreenCaptureKit sharing, VoiceOver verification, signing and notarization remain.
|
The current checkpoint is a functional client: AppKit launch/menu lifecycle, saved server profiles, guest and account authentication, explicit TOFU approval (including changed-key warning), protected-channel password prompts, channel selection, user roster, channel text, voice subscription, managed microphone-stream lifecycle, default-device Core Audio capture/playback, disconnect reporting, native accessibility labels, sandbox/network/audio entitlements, and deterministic disposal. Profiles save the host, port, authentication mode, username or guest nickname; account passwords remain session-only and are never written to the profile JSON. Capture is converted to interleaved 48 kHz int16 PCM through `AVAudioConverter`; playback uses an `AVAudioSourceNode` and the shared bounded `PcmRing`, so its render callback does not allocate, lock, or block. It does not yet replace the Swift release. Optional Keychain credentials, non-default device selection, moderation sheets, private messages, settings, ScreenCaptureKit sharing, VoiceOver verification, signing and notarization remain.
|
||||||
|
|
||||||
Build on Apple Silicon macOS 15.6+ with Xcode 26 and the .NET 10 macOS workload:
|
Build on Apple Silicon macOS 15.6+ with Xcode 26 and the .NET 10 macOS workload:
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ internal sealed class MainWindowController : NSWindowController
|
|||||||
private uint currentChannel = 1;
|
private uint currentChannel = 1;
|
||||||
private uint microphoneStreamId;
|
private uint microphoneStreamId;
|
||||||
private bool joinedVoice;
|
private bool joinedVoice;
|
||||||
|
private bool changingChannel;
|
||||||
private IAudioCapture? microphone;
|
private IAudioCapture? microphone;
|
||||||
private IAudioPlayback? playback;
|
private IAudioPlayback? playback;
|
||||||
|
|
||||||
@@ -61,7 +62,11 @@ internal sealed class MainWindowController : NSWindowController
|
|||||||
private void RefreshState()
|
private void RefreshState()
|
||||||
{
|
{
|
||||||
channels.RemoveAllItems();
|
channels.RemoveAllItems();
|
||||||
foreach (var channel in client.Channels.OrderBy(c => c.Order).ThenBy(c => c.Name)) { channels.AddItem(channel.Name); channels.LastItem!.RepresentedObject = new NSString(channel.Id.ToString()); }
|
foreach (var channel in client.Channels.OrderBy(c => c.Order).ThenBy(c => c.Name))
|
||||||
|
{
|
||||||
|
channels.AddItem(channel.Name + (channel.PasswordProtected ? " [password]" : ""));
|
||||||
|
channels.LastItem!.RepresentedObject = new NSString(channel.Id.ToString());
|
||||||
|
}
|
||||||
currentChannel = client.Users.FirstOrDefault(u => u.Id == selfId)?.ChannelId ?? currentChannel;
|
currentChannel = client.Users.FirstOrDefault(u => u.Id == selfId)?.ChannelId ?? currentChannel;
|
||||||
int selectedIndex = client.Channels.ToList().FindIndex(c => c.Id == currentChannel); if (selectedIndex >= 0) channels.SelectItem(selectedIndex);
|
int selectedIndex = client.Channels.ToList().FindIndex(c => c.Id == currentChannel); if (selectedIndex >= 0) channels.SelectItem(selectedIndex);
|
||||||
users.Value = string.Join("\n", client.Users.Where(u => u.ChannelId == currentChannel).OrderBy(u => u.Nickname).Select(u => (u.Id == selfId ? "You — " : "") + u.Nickname));
|
users.Value = string.Join("\n", client.Users.Where(u => u.ChannelId == currentChannel).OrderBy(u => u.Nickname).Select(u => (u.Id == selfId ? "You — " : "") + u.Nickname));
|
||||||
@@ -69,16 +74,49 @@ internal sealed class MainWindowController : NSWindowController
|
|||||||
}
|
}
|
||||||
private async void ChangeChannel(object? sender, EventArgs args)
|
private async void ChangeChannel(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
if (!uint.TryParse(channels.SelectedItem?.RepresentedObject?.ToString(), out uint id) || id == currentChannel) return;
|
if (changingChannel || !uint.TryParse(channels.SelectedItem?.RepresentedObject?.ToString(), out uint id) || id == currentChannel) return;
|
||||||
|
var channel = client.Channels.FirstOrDefault(item => item.Id == id);
|
||||||
|
if (channel is null) { RefreshState(); return; }
|
||||||
|
string? password = channel.PasswordProtected ? PromptForChannelPassword(channel.Name) : "";
|
||||||
|
if (password is null) { RefreshState(); return; }
|
||||||
|
bool resumeVoice = joinedVoice;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool resumeVoice = joinedVoice;
|
changingChannel = true; channels.Enabled = false;
|
||||||
if (resumeVoice) await LeaveVoice();
|
if (resumeVoice) await LeaveVoice();
|
||||||
var result = (await client.RequestAsync(new() { JoinChannel = new() { ChannelId = id } })).JoinChannelResult;
|
var result = (await client.RequestAsync(new() { JoinChannel = new() { ChannelId = id, Password = password } })).JoinChannelResult;
|
||||||
if (!result.Ok) status.StringValue = result.Error;
|
if (result.Ok) currentChannel = id;
|
||||||
if (resumeVoice) await JoinVoice();
|
if (resumeVoice) await JoinVoice();
|
||||||
|
RefreshState();
|
||||||
|
status.StringValue = result.Ok ? $"Joined {channel.Name}" : result.Error;
|
||||||
}
|
}
|
||||||
catch (Exception exception) { status.StringValue = exception.Message; }
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
string message = exception.Message;
|
||||||
|
if (resumeVoice && !joinedVoice)
|
||||||
|
{
|
||||||
|
try { await JoinVoice(); }
|
||||||
|
catch (Exception restoreException) { message += " Voice could not be restored: " + restoreException.Message; }
|
||||||
|
}
|
||||||
|
RefreshState(); status.StringValue = message;
|
||||||
|
}
|
||||||
|
finally { changingChannel = false; channels.Enabled = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? PromptForChannelPassword(string channelName)
|
||||||
|
{
|
||||||
|
var field = new NSSecureTextField(new CGRect(0, 0, 320, 26)) { PlaceholderString = "Channel password" };
|
||||||
|
((INSAccessibility)field).AccessibilityLabel = $"Password for {channelName}";
|
||||||
|
var alert = new NSAlert
|
||||||
|
{
|
||||||
|
MessageText = $"Join {channelName}",
|
||||||
|
InformativeText = "Enter the channel password.",
|
||||||
|
AlertStyle = NSAlertStyle.Informational,
|
||||||
|
AccessoryView = field
|
||||||
|
};
|
||||||
|
alert.AddButton("Join"); alert.AddButton("Cancel");
|
||||||
|
if (alert.RunModal() != 1000) { field.StringValue = ""; return null; }
|
||||||
|
string value = field.StringValue; field.StringValue = ""; return value;
|
||||||
}
|
}
|
||||||
private void Send(object? sender, EventArgs args)
|
private void Send(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -853,16 +853,17 @@ build produced.
|
|||||||
**Checkpoint (2026-09-16):** `clients/apple/dotnet/VoiceCat.Mac` is a separate .NET 10
|
**Checkpoint (2026-09-16):** `clients/apple/dotnet/VoiceCat.Mac` is a separate .NET 10
|
||||||
AppKit application that consumes `VoiceCat.Core` directly. It implements guest and account
|
AppKit application that consumes `VoiceCat.Core` directly. It implements guest and account
|
||||||
connection, validated saved server profiles whose JSON never contains passwords,
|
connection, validated saved server profiles whose JSON never contains passwords,
|
||||||
interactive TOFU approval, channel browsing, roster/chat, voice subscription, native
|
interactive TOFU approval, protected-channel password prompts, channel browsing, roster/chat,
|
||||||
accessibility labels and default-device microphone/playback. Core Audio capture is resampled
|
voice subscription, native accessibility labels and default-device microphone/playback.
|
||||||
|
Core Audio capture is resampled
|
||||||
and converted through `AVAudioConverter` before entering the managed audio engine. Stereo
|
and converted through `AVAudioConverter` before entering the managed audio engine. Stereo
|
||||||
playback uses `AVAudioSourceNode` and the shared bounded PCM ring; its render callback does
|
playback uses `AVAudioSourceNode` and the shared bounded PCM ring; its render callback does
|
||||||
not allocate, lock or block. Voice stream lifetime follows subscription, disconnect and
|
not allocate, lock or block. Voice stream lifetime follows subscription, disconnect and
|
||||||
channel changes. Current Apple API calls compile warning-free against Microsoft's
|
channel changes. Current Apple API calls compile warning-free against Microsoft's
|
||||||
26.4.10259 macOS reference assembly, and a macOS CI job builds the native codec/DSP shim and
|
26.4.10259 macOS reference assembly, and a macOS CI job builds the native codec/DSP shim and
|
||||||
Apple solution. A real macOS device run and listen test remain required. The existing Swift
|
Apple solution. A real macOS device run and listen test remain required. The existing Swift
|
||||||
app remains the release client until protected-channel prompts, optional Keychain credentials,
|
app remains the release client until optional Keychain credentials, selectable devices, the
|
||||||
selectable devices, the rest of the moderation/settings surface, ScreenCaptureKit, VoiceOver
|
rest of the moderation/settings surface, ScreenCaptureKit, VoiceOver
|
||||||
validation, signing and notarization are complete.
|
validation, signing and notarization are complete.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user