92 lines
3.5 KiB
Swift
92 lines
3.5 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
|
||
|
|
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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" : "")")
|
||
|
|
}
|
||
|
|
}
|