Files
voice-cat/clients/apple/iOS/VoiceCatiOS/Views/ChannelBrowserView.swift
Talon 6b7f06a282 feat(clients): expose all channel codec params + guest nickname everywhere
Channel create/edit UIs only surfaced a subset of the core's vc_audio_config,
and DRED was exposed nowhere. While adding it, found a latent ABI mismatch:
both Swift AudioConfig and the C# VcAudioConfigNative blittable struct were one
int short of the native vc_audio_config (missing the trailing `dred`), so native
read past the managed struct in vc_create_channel/vc_edit_channel.

- core marshaling: thread `dred` through Swift (Models/Marshaling/toNative) and
  C# (Structs/Models/Marshaling/VoiceCatClient) -- fixes the ABI gap + enables it
- windows: add the one missing DRED checkbox to ChannelEditDialog
- macos: ChannelEditSheet now exposes application, sample rate, packet loss,
  complexity, and DRED (was stereo/bitrate/frame/FEC/DTX only)
- ios: rebuild ChannelEditView into a full create+edit form (all params); add
  SessionState.editChannel + an admin Edit swipe action (iOS had no edit UI)
- guest nickname: add a dedicated `nickname` to SavedServer on macOS+iOS
  (backward-compatible Codable), shown in Guest mode, wired into the guest auth
  path -- guests could not set a display name on either before (only Windows)

Verified: macOS + iOS (sim, arm64) xcodebuild BUILD SUCCEEDED; core ctest 22/23
(only external_pcm aborts on a pre-existing shutdown mutex race; no C++ changed).
2026-06-21 04:03:50 +02:00

106 lines
4.1 KiB
Swift

import SwiftUI
import VoiceCatCore
/// iPhone channels tab: a drill-down browser. The root list shows only top-level channels;
/// tapping (or VoiceOver-activating) a channel pushes `ChannelDetailView`, which shows the
/// people in it and any sub-channels. Joining is an explicit action inside the detail view.
struct ChannelBrowserView: View {
@Bindable var session: SessionState
@State private var showCreateChannel = false
@State private var editChannel: Channel?
var body: some View {
NavigationStack {
List {
ForEach(rootChannels) { ch in
NavigationLink {
ChannelDetailView(channel: ch, session: session)
} label: {
ChannelRow(channel: ch, session: session)
}
.swipeActions(edge: .trailing) {
if session.permissions.isAdmin {
Button(role: .destructive) {
session.deleteChannel(ch.id)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.swipeActions(edge: .leading) {
if session.permissions.isAdmin {
Button {
editChannel = ch
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
}
}
}
.navigationTitle("Channels")
.toolbar {
if session.permissions.canCreateTempChannel || session.permissions.isAdmin {
ToolbarItem(placement: .primaryAction) {
Button {
showCreateChannel = true
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("Create channel")
}
}
}
.sheet(isPresented: $showCreateChannel) {
ChannelEditView(channelId: nil, session: session)
}
.sheet(item: $editChannel) { ch in
ChannelEditView(channelId: ch.id, session: session)
}
}
}
private var rootChannels: [Channel] {
session.channels
.filter { $0.parentId == 0 }
.sorted { $0.name < $1.name }
}
}
/// Shared channel row used by the iPhone browser and detail views. Mirrors the look of the
/// iPad `ChannelTreeView` row but keyed on a `Channel` rather than a tree `ChannelNode`.
struct ChannelRow: View {
let channel: Channel
let session: SessionState
var body: some View {
let isCurrent = session.currentChannelId == channel.id
let usersHere = session.users.filter { $0.channelId == channel.id }
HStack(spacing: 8) {
Image(systemName: channel.passwordProtected ? "lock.fill" : "number")
.foregroundStyle(isCurrent ? .blue : .secondary)
.imageScale(.small)
VStack(alignment: .leading, spacing: 1) {
Text(channel.name)
.fontWeight(isCurrent ? .semibold : .regular)
if !channel.topic.isEmpty {
Text(channel.topic)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
if !usersHere.isEmpty {
Text("\(usersHere.count)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(channel.name)\(isCurrent ? ", current" : "")\(channel.passwordProtected ? ", password protected" : "")\(!usersHere.isEmpty ? ", \(usersHere.count) users" : "")")
}
}