1// FindByPeerIDView.swift2// SophaxChat3//4// Lets the user enter a 16-character peerID and resolve it to a contact5// via the Kademlia DHT network (requires Tor to be running).67import SwiftUI8import SophaxChatCore910struct FindByPeerIDView: View {11@EnvironmentObject var appState: AppState12@Environment(\.dismiss) private var dismiss1314@State private var peerIDInput =""15@State private var isSearching = false16@State private var foundPeer: KnownPeer?= nil17@State private var errorText: String?= nil1819// Navigation path so we can push into ChatView after "Start Chat"20@State private var navPath = NavigationPath()2122 var body: some View {23 NavigationStack(path: $navPath){24 Form {25// MARK: Input26 Section {27 HStack {28 TextField("16-character peer ID", text: $peerIDInput)29.autocorrectionDisabled()30.textInputAutocapitalization(.never)31.font(.system(.body, design:.monospaced))32.onChange(of: peerIDInput){ _, v in33// Clamp to 16 hex chars34 peerIDInput = String(v.lowercased()35.filter { $0.isHexDigit }36.prefix(16))37 foundPeer = nil38 errorText = nil39}40 if !peerIDInput.isEmpty {41 Button {42 peerIDInput =""43 foundPeer = nil44 errorText = nil45} label:{46 Image(systemName:"xmark.circle.fill")47.foregroundStyle(.secondary)48}49.buttonStyle(.plain)50}51}52} header:{53 Text("Peer ID")54} footer:{55 Text("Enter the 16-character ID shown on another user's Contact Card (under their username).")56}5758// MARK: Search button59 Section {60 Button {61 Task { await search()}62} label:{63 HStack {64 Spacer()65 if isSearching {66 ProgressView()67.padding(.trailing, 6)68 Text("Searching DHT…")69} else {70 Label("Find via DHT", systemImage:"network")71}72 Spacer()73}74}75.disabled(peerIDInput.count != 16 || isSearching)76}7778// MARK: Error79 if let err = errorText {80 Section {81 Label(err, systemImage:"exclamationmark.triangle")82.foregroundStyle(.red)83}84}8586// MARK: Result87 if let peer = foundPeer {88 Section("Found"){89 PeerResultRow(peer: peer)9091 Button {92 dismiss()93// Post notification so ChatListView can navigate to ChatView94 NotificationCenter.default.post(95 name:.sophaxOpenChat,96 object: peer97)98} label:{99 Label("Start Chat", systemImage:"bubble.left.and.bubble.right")100.frame(maxWidth:.infinity)101}102.buttonStyle(.borderedProminent)103}104}105}106.navigationTitle("Find by Peer ID")107.navigationBarTitleDisplayMode(.inline)108.toolbar {109 ToolbarItem(placement:.cancellationAction){110 Button("Cancel"){ dismiss()}111}112}113.navigationDestination(for: KnownPeer.self){ peer in114 ChatView(peer: peer)115}116}117.presentationDetents([.medium,.large])118}119120// MARK: - Search121122 private func search() async {123 isSearching = true124 foundPeer = nil125 errorText = nil126 do {127 foundPeer = try await appState.lookupPeerViaDHT(peerID: peerIDInput)128} catch {129 errorText = lookupErrorMessage(error)130}131 isSearching = false132}133134 private func lookupErrorMessage(_ error: Error)-> String {135 if TorManager.shared.state !=.ready {136 return "Tor is not connected. Enable Tor in Settings and wait for it to bootstrap."137}138 if appState.chatManager?.dhtEngine == nil {139 return "DHT is not running. Make sure Tor is enabled and restart the app."140}141 return "Peer not found. They may be offline or not yet reachable over the DHT network."142}143}144145// MARK: - Peer result row146147private struct PeerResultRow: View {148@EnvironmentObject var appState: AppState149 let peer: KnownPeer150151 var body: some View {152 HStack(spacing: 12){153 ZStack {154 Circle()155.fill(Color.accentColor.opacity(0.15))156.frame(width: 44, height: 44)157 if let data = appState.peerAvatars[peer.id],158 let uiImg = UIImage(data: data){159 Image(uiImage: uiImg)160.resizable().scaledToFill()161.frame(width: 44, height: 44)162.clipShape(Circle())163} else {164 Text(String(peer.username.prefix(1)).uppercased())165.font(.headline)166.foregroundStyle(Color.accentColor)167}168}169 VStack(alignment:.leading, spacing: 2){170 Text(appState.displayName(for: peer))171.font(.headline)172 Text(peer.id)173.font(.system(.caption, design:.monospaced))174.foregroundStyle(.secondary)175 if let addr = peer.tcpAddress {176 Text(addr)177.font(.caption2)178.foregroundStyle(.tertiary)179.lineLimit(1)180}181}182 Spacer()183 Image(systemName:"checkmark.circle.fill")184.foregroundStyle(.green)185}186.padding(.vertical, 4)187}188}