import SwiftUI import VoiceCatCore struct UserListView: View { @Bindable var session: SessionState @State private var selectedUser: User? @State private var showPerUserTuning = false @State private var showBanUser = false @State private var showMoveUser = false @State private var showPermissions = false var body: some View { let channelUsers = session.currentChannelId == 0 ? session.users : session.users.filter { $0.channelId == session.currentChannelId } List(channelUsers) { user in UserRowView(user: user, isSelf: user.id == session.selfUserId) .contextMenu { userContextMenu(user: user) } } .listStyle(.plain) .overlay { if channelUsers.isEmpty { ContentUnavailableView( "No Users", systemImage: "person.slash", description: Text(session.currentChannelId == 0 ? "Join a channel to see users." : "No one else here yet.") ) } } .sheet(item: $selectedUser) { user in if showPerUserTuning { PerUserTuningView(user: user, session: session) } else if showBanUser { BanUserView(user: user, session: session) } else if showMoveUser { MoveUserView(user: user, session: session) } else if showPermissions { PermissionsView(user: user, session: session) } } } @ViewBuilder private func userContextMenu(user: User) -> some View { if user.id != session.selfUserId { Button { selectedUser = user showPerUserTuning = true showBanUser = false; showMoveUser = false; showPermissions = false } label: { Label("Volume / NR", systemImage: "speaker.wave.2") } if session.permissions.canKick || session.permissions.isAdmin { Divider() Button { session.kickUser(user.id, reason: "") } label: { Label("Kick", systemImage: "person.fill.xmark") } if session.permissions.canBan || session.permissions.isAdmin { Button(role: .destructive) { selectedUser = user showBanUser = true showPerUserTuning = false; showMoveUser = false; showPermissions = false } label: { Label("Ban…", systemImage: "nosign") } } } if session.permissions.canMoveUsers || session.permissions.isAdmin { Button { selectedUser = user showMoveUser = true showPerUserTuning = false; showBanUser = false; showPermissions = false } label: { Label("Move to channel…", systemImage: "arrow.right.circle") } } if session.permissions.isAdmin { Divider() let muted = user.serverMuted Button { session.setServerMute(user.id, muted: !muted, deafened: user.serverDeafened) } label: { Label(muted ? "Unmute" : "Server Mute", systemImage: muted ? "mic" : "mic.slash") } Button { selectedUser = user showPermissions = true showPerUserTuning = false; showBanUser = false; showMoveUser = false } label: { Label("Permissions…", systemImage: "lock.shield") } } } } } private struct UserRowView: View { let user: User let isSelf: Bool var body: some View { HStack(spacing: 10) { Image(systemName: user.selfMicMuted || user.serverMuted ? "mic.slash.fill" : "mic.fill") .foregroundStyle(user.selfMicMuted || user.serverMuted ? .red : .green) .imageScale(.small) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 1) { HStack(spacing: 4) { Text(user.nickname) .fontWeight(isSelf ? .semibold : .regular) if isSelf { Text("(you)") .font(.caption2) .foregroundStyle(.secondary) } if user.isGuest { Text("guest") .font(.caption2) .foregroundStyle(.secondary) } } if user.serverMuted || user.serverDeafened { Text(user.serverDeafened ? "server deafened" : "server muted") .font(.caption2) .foregroundStyle(.orange) } } Spacer() if user.selfDeafened { Image(systemName: "headphones.slash") .imageScale(.small) .foregroundStyle(.secondary) .accessibilityHidden(true) } } .accessibilityElement(children: .combine) .accessibilityLabel("\(user.nickname)\(isSelf ? ", you" : "")\(user.isGuest ? ", guest" : "")\(user.selfMicMuted ? ", muted" : "")\(user.serverMuted ? ", server muted" : "")") } }