import SwiftUI struct NodeListView: View { @Environment(APIClient.self) private var api @Environment(MeshDataStore.self) private var store @State private var alertText: String? @AppStorage("nodes.onlyOnline") private var onlyOnline: Bool = true private static let onlineThreshold: TimeInterval = 900 private var visibleNodes: [Node] { guard onlyOnline else { return store.nodes } return store.nodes.filter { Self.isOnline($0, threshold: Self.onlineThreshold) } } var body: some View { Group { if store.loading && store.nodes.isEmpty { ProgressView("Loading nodes…") } else if store.nodes.isEmpty { ContentUnavailableView("No nodes", systemImage: "antenna.radiowaves.left.and.right.slash") } else if visibleNodes.isEmpty { ContentUnavailableView { Label("No online nodes", systemImage: "antenna.radiowaves.left.and.right.slash") } description: { Text("Turn off the Only online filter to see every node.") } actions: { Button("Show all nodes") { onlyOnline = false } } } else { List(visibleNodes) { node in if node.hasPKC == true, let id = node.nodeId { NavigationLink { ChatView(target: .dm(nodeId: id, name: node.displayName)) } label: { NodeRow(node: node, onlineThreshold: Self.onlineThreshold) } } else { Button { alertText = "Node \(node.displayName) has not exchanged encryption keys, DM is not possible." } label: { NodeRow(node: node, onlineThreshold: Self.onlineThreshold) } .buttonStyle(.plain) } } } } .navigationTitle("Nodes") .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { Toggle(isOn: $onlyOnline) { Label("Only online", systemImage: "wifi") } Button { Task { await store.refresh(api: api) } } label: { Label("Refresh", systemImage: "arrow.clockwise") } .disabled(store.loading) } label: { Image(systemName: onlyOnline ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") } } } .refreshable { await store.refresh(api: api) } .task { if store.nodes.isEmpty { await store.refresh(api: api) } } .alert("Cannot open DM", isPresented: .init( get: { alertText != nil }, set: { if !$0 { alertText = nil } } )) { Button("OK") { alertText = nil } } message: { Text(alertText ?? "") } } static func isOnline(_ node: Node, threshold: TimeInterval) -> Bool { guard let raw = node.lastHeard, raw > 0 else { return false } let secs = raw > 9_999_999_999 ? raw / 1000.0 : raw return Date().timeIntervalSince1970 - secs < threshold } } private struct NodeRow: View { let node: Node let onlineThreshold: TimeInterval var body: some View { HStack(spacing: 10) { VStack { Text("\(node.hopsAway ?? 99)") .font(.headline.monospacedDigit()) Text("hops").font(.caption2).foregroundStyle(.secondary) } .frame(width: 44) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Circle() .fill(isOnline ? Color.green : Color.gray) .frame(width: 9, height: 9) Text(node.displayName).font(.body) if node.hasPKC == true { Image(systemName: "key.fill") .font(.caption2) .foregroundStyle(.yellow) } } HStack(spacing: 6) { if let id = node.nodeId { Text(id).font(.caption.monospaced()).foregroundStyle(.secondary) } Spacer() Text(lastHeardText) .font(.caption) .foregroundStyle(.secondary) } } } .padding(.vertical, 2) } private var lastHeardSeconds: TimeInterval? { guard let raw = node.lastHeard, raw > 0 else { return nil } let secs = raw > 9_999_999_999 ? raw / 1000.0 : raw return Date().timeIntervalSince1970 - secs } private var isOnline: Bool { guard let s = lastHeardSeconds else { return false } return s < onlineThreshold } private var lastHeardText: String { guard let s = lastHeardSeconds else { return "?" } if s < 60 { return "now" } if s < 3600 { return "\(Int(s / 60))m" } if s < 86400 { return "\(Int(s / 3600))h" } return "\(Int(s / 86400))d" } }