Files
voice-cat/clients/apple/iOS/VoiceCatiOS/Views/ChannelTreeView.swift

132 lines
4.8 KiB
Swift
Raw Permalink Normal View History

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
@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")
}
}
}
}
.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)
}
.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" : "")")
}
}