import AppKit import VoiceCatCore // UserPickerSheet — a modal sheet for picking one user from the list of all connected server // users. Used by the "Messages → New Private Message…" menu item so the user can start a PM // with anyone on the server, not just the current channel. Mirrors the Windows client's // `UserPickerDialog` (clients/windows/VoiceCat.App/Forms/UserPickerDialog.cs), adapted to the // Mac sheet pattern used by the rest of the macOS client (InputSheet, MoveUserSheet, etc.). final class UserPickerSheet: NSViewController, NSTableViewDataSource, NSTableViewDelegate { /// Called with the selected user's ID, or `nil` if the user cancelled. var onComplete: ((UInt32?) -> Void)? private let users: [User] private let tableView = NSTableView() private var okButton: NSButton? private var selectedRow: Int = -1 init(users: [User]) { self.users = users super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func loadView() { view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 320)) } override func viewDidLoad() { super.viewDidLoad() buildUI() } // MARK: - UI private func buildUI() { let titleLabel = NSTextField(labelWithString: "Select a user:") titleLabel.font = .boldSystemFont(ofSize: 13) titleLabel.setAccessibilityLabel("Select a user") let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("user")) tableView.addTableColumn(col) tableView.headerView = nil tableView.dataSource = self tableView.delegate = self tableView.doubleAction = #selector(okClicked) tableView.target = self tableView.setAccessibilityLabel("User list") let scroll = NSScrollView() scroll.documentView = tableView scroll.hasVerticalScroller = true scroll.borderType = .bezelBorder scroll.translatesAutoresizingMaskIntoConstraints = false let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancelClicked)) cancelButton.bezelStyle = .rounded let okButton = NSButton(title: "OK", target: self, action: #selector(okClicked)) okButton.bezelStyle = .rounded okButton.keyEquivalent = "\r" okButton.isEnabled = false self.okButton = okButton let buttonRow = NSStackView(views: [NSView(), cancelButton, okButton]) buttonRow.orientation = .horizontal buttonRow.spacing = 8 let stack = NSStackView(views: [titleLabel, scroll, buttonRow]) stack.orientation = .vertical stack.spacing = 8 stack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.topAnchor), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } override func viewDidAppear() { super.viewDidAppear() if users.count == 1 { tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) } } // MARK: - Actions @objc private func okClicked() { guard selectedRow >= 0, selectedRow < users.count else { return } let userId = users[selectedRow].id dismiss(nil) onComplete?(userId) } @objc private func cancelClicked() { dismiss(nil) onComplete?(nil) } // MARK: - NSTableViewDataSource / Delegate func numberOfRows(in tableView: NSTableView) -> Int { users.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let id = NSUserInterfaceItemIdentifier("userCell") let cell = tableView.makeView(withIdentifier: id, owner: nil) as? NSTableCellView ?? makeCellView(identifier: id) cell.textField?.stringValue = users[row].nickname return cell } func tableViewSelectionDidChange(_ notification: Notification) { selectedRow = tableView.selectedRow okButton?.isEnabled = selectedRow >= 0 } private func makeCellView(identifier: NSUserInterfaceItemIdentifier) -> NSTableCellView { let cell = NSTableCellView() cell.identifier = identifier let tf = NSTextField(labelWithString: "") tf.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(tf) cell.textField = tf NSLayoutConstraint.activate([ tf.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4), tf.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4), tf.centerYAnchor.constraint(equalTo: cell.centerYAnchor), ]) return cell } }