Files
bitchat/bitchat/Views/MeshPeerList.swift
T
764f016d17 [codex] Refactor app runtime and ownership architecture (#1104)
* Refactor app runtime and view model architecture

* Move app ownership into stores and coordinators

* Fix smoke test environment injection

* Stabilize fragmentation package tests

* Fix coordinator build warnings

* Clean up chat view model warnings

* Fix Nostr relay startup coalescing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2026-05-30 14:13:26 +02:00

152 lines
7.4 KiB
Swift

import SwiftUI
import BitFoundation
struct MeshPeerList: View {
@EnvironmentObject private var peerListModel: PeerListModel
let textColor: Color
let secondaryTextColor: Color
let onTapPeer: (PeerID) -> Void
let onToggleFavorite: (PeerID) -> Void
let onShowFingerprint: (PeerID) -> Void
@Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = []
private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to a blocked peer indicator")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
}
var body: some View {
let currentIDs = peerListModel.meshRows.map(\.id)
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
let peers: [MeshPeerRow] = displayIDs.compactMap { id in
peerListModel.meshRows.first(where: { $0.id == id })
}
if peerListModel.meshRows.isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text(Strings.noneNearby)
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
.padding(.top, 12)
}
} else {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<peers.count, id: \.self) { idx in
let peer = peers[idx]
let isMe = peer.isMe
HStack(spacing: 4) {
let assigned = peerListModel.colorForMeshPeer(id: peer.peerID, isDark: colorScheme == .dark)
let baseColor = isMe ? Color.orange : assigned
if isMe {
Image(systemName: "person.fill")
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isConnected {
// Mesh-connected peer: radio icon
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isReachable {
// Mesh-reachable (relayed): point.3 icon
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isMutualFavorite {
// Mutual favorite reachable via Nostr: globe icon (purple)
Image(systemName: "globe")
.font(.bitchatSystem(size: 10))
.foregroundColor(.purple)
} else {
// Fallback icon for others (dimmed)
Image(systemName: "person")
.font(.bitchatSystem(size: 10))
.foregroundColor(secondaryTextColor)
}
let (base, suffix) = peer.displayName.splitSuffix()
HStack(spacing: 0) {
Text(base)
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(baseColor)
if !suffix.isEmpty {
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
Text(suffix)
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(suffixColor)
}
}
if peer.isBlocked {
Image(systemName: "nosign")
.font(.bitchatSystem(size: 10))
.foregroundColor(.red)
.help(Strings.blockedTooltip)
}
if !isMe {
if peer.isConnected {
if let icon = peer.encryptionStatus.icon {
Image(systemName: icon)
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
}
} else {
// Offline: prefer showing verified badge from persisted fingerprints
if peer.showsVerifiedBadgeWhenOffline {
Image(systemName: "checkmark.seal.fill")
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if let icon = peer.encryptionStatus.icon {
// Fallback to whatever status says (likely lock if we had a past session)
Image(systemName: icon)
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
}
}
}
Spacer()
// Unread message indicator for this peer
if peer.hasUnread {
Image(systemName: "envelope.fill")
.font(.bitchatSystem(size: 10))
.foregroundColor(.orange)
.help(Strings.newMessagesTooltip)
}
if !isMe {
Button(action: { onToggleFavorite(peer.peerID) }) {
Image(systemName: peer.isFavorite ? "star.fill" : "star")
.font(.bitchatSystem(size: 12))
.foregroundColor(peer.isFavorite ? .yellow : secondaryTextColor)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
.padding(.vertical, 4)
.padding(.top, idx == 0 ? 10 : 0)
.contentShape(Rectangle())
.onTapGesture { if !isMe { onTapPeer(peer.peerID) } }
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID) } }
}
}
// Seed and update order outside result builder
.onAppear {
orderedIDs = currentIDs
}
.onChange(of: currentIDs) { ids in
var newOrder = orderedIDs
newOrder.removeAll { !ids.contains($0) }
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
if newOrder != orderedIDs { orderedIDs = newOrder }
}
}
}
}