2026-06-19 02:10:25 +02:00
|
|
|
import SwiftUI
|
|
|
|
|
import VoiceCatCore
|
|
|
|
|
|
|
|
|
|
// ChannelNode wraps Channel for OutlineGroup; childrenOrNil must be nil (not empty [])
|
|
|
|
|
// for leaf channels so OutlineGroup doesn't render expand buttons.
|
|
|
|
|
struct ChannelNode: Identifiable {
|
|
|
|
|
let channel: Channel
|
|
|
|
|
let children: [ChannelNode]?
|
|
|
|
|
var id: UInt32 { channel.id }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ChannelTreeView: View {
|
|
|
|
|
@Bindable var session: SessionState
|
|
|
|
|
@State private var showCreateChannel = false
|
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
|
|
|
@State private var editChannel: Channel?
|
2026-06-19 02:10:25 +02:00
|
|
|
@State private var channelPassword = ""
|
|
|
|
|
@State private var passwordChannelId: UInt32?
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
List(channelTree, children: \.children) { node in
|
|
|
|
|
ChannelRowView(node: node, session: session)
|
|
|
|
|
.onTapGesture {
|
|
|
|
|
if node.channel.passwordProtected {
|
|
|
|
|
passwordChannelId = node.channel.id
|
|
|
|
|
} else {
|
|
|
|
|
session.joinChannel(node.channel.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.swipeActions(edge: .trailing) {
|
|
|
|
|
if session.permissions.isAdmin {
|
|
|
|
|
Button(role: .destructive) {
|
|
|
|
|
session.deleteChannel(node.channel.id)
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Delete", systemImage: "trash")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
.swipeActions(edge: .leading) {
|
|
|
|
|
if session.permissions.isAdmin {
|
|
|
|
|
Button {
|
|
|
|
|
editChannel = node.channel
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Edit", systemImage: "pencil")
|
|
|
|
|
}
|
|
|
|
|
.tint(.blue)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-19 02:10:25 +02:00
|
|
|
}
|
|
|
|
|
.listStyle(.sidebar)
|
|
|
|
|
.toolbar {
|
|
|
|
|
if session.permissions.canCreateTempChannel || session.permissions.isAdmin {
|
|
|
|
|
ToolbarItem(placement: .primaryAction) {
|
|
|
|
|
Button {
|
|
|
|
|
showCreateChannel = true
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "plus")
|
|
|
|
|
}
|
|
|
|
|
.accessibilityLabel("Create channel")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if session.currentChannelId != 0 {
|
|
|
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
|
|
|
Button("Leave", systemImage: "arrow.left.circle") {
|
|
|
|
|
session.leaveChannel()
|
|
|
|
|
}
|
|
|
|
|
.accessibilityLabel("Leave current channel")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented: $showCreateChannel) {
|
|
|
|
|
ChannelEditView(channelId: nil, session: session)
|
|
|
|
|
}
|
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
|
|
|
.sheet(item: $editChannel) { ch in
|
|
|
|
|
ChannelEditView(channelId: ch.id, session: session)
|
|
|
|
|
}
|
2026-06-19 02:10:25 +02:00
|
|
|
.alert("Channel Password", isPresented: Binding(
|
|
|
|
|
get: { passwordChannelId != nil },
|
|
|
|
|
set: { if !$0 { passwordChannelId = nil; channelPassword = "" } }
|
|
|
|
|
)) {
|
|
|
|
|
SecureField("Password", text: $channelPassword)
|
|
|
|
|
.accessibilityLabel("Channel password")
|
|
|
|
|
Button("Join") {
|
|
|
|
|
if let cid = passwordChannelId {
|
|
|
|
|
session.joinChannel(cid, password: channelPassword)
|
|
|
|
|
}
|
|
|
|
|
passwordChannelId = nil
|
|
|
|
|
channelPassword = ""
|
|
|
|
|
}
|
|
|
|
|
Button("Cancel", role: .cancel) {
|
|
|
|
|
passwordChannelId = nil
|
|
|
|
|
channelPassword = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var channelTree: [ChannelNode] {
|
|
|
|
|
buildTree(parentId: 0, channels: session.channels)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func buildTree(parentId: UInt32, channels: [Channel]) -> [ChannelNode] {
|
|
|
|
|
channels
|
|
|
|
|
.filter { $0.parentId == parentId }
|
|
|
|
|
.map { ch in
|
|
|
|
|
let kids = buildTree(parentId: ch.id, channels: channels)
|
|
|
|
|
return ChannelNode(channel: ch, children: kids.isEmpty ? nil : kids)
|
|
|
|
|
}
|
|
|
|
|
.sorted { $0.channel.name < $1.channel.name }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ChannelRowView: View {
|
|
|
|
|
let node: ChannelNode
|
|
|
|
|
let session: SessionState
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
let ch = node.channel
|
|
|
|
|
let isCurrent = session.currentChannelId == ch.id
|
|
|
|
|
let usersHere = session.users.filter { $0.channelId == ch.id }
|
|
|
|
|
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Image(systemName: ch.passwordProtected ? "lock.fill" : "number")
|
|
|
|
|
.foregroundStyle(isCurrent ? .blue : .secondary)
|
|
|
|
|
.imageScale(.small)
|
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
|
|
|
Text(ch.name)
|
|
|
|
|
.fontWeight(isCurrent ? .semibold : .regular)
|
|
|
|
|
if !ch.topic.isEmpty {
|
|
|
|
|
Text(ch.topic)
|
|
|
|
|
.font(.caption2)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Spacer()
|
|
|
|
|
if !usersHere.isEmpty {
|
|
|
|
|
Text("\(usersHere.count)")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.accessibilityLabel("\(usersHere.count) users")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.accessibilityElement(children: .combine)
|
|
|
|
|
.accessibilityLabel("\(ch.name)\(isCurrent ? ", current" : "")\(ch.passwordProtected ? ", password protected" : "")\(!usersHere.isEmpty ? ", \(usersHere.count) users" : "")")
|
|
|
|
|
}
|
|
|
|
|
}
|