Files
bitchat/bitchat/Views/MeshPeerList.swift
T
496972dcc9 Mesh robustness optimizations (#463)
* Mesh robustness: link-aware fragmentation, relay jitter, connection budget, adaptive scanning; fix BLE queue deadlock

- Link-aware fragmentation using per-link write/notify limits
- Directed fragments for one-to-one; exclude from relays
- Add relay jitter with cancel-on-duplicate to reduce floods
- Cap central links and rate-limit connect attempts with queue
- Foreground duty-cycled scanning when connected; continuous when isolated
- Fix deadlock by avoiding sync on BLE queue when already on it
- Silence Keychain var->let warnings

* Mesh: add relay jitter; dynamic RSSI threshold; candidate scoring/backoff; fix misplaced lines in BLEService extension

* macOS UI: restore mesh peer list when geohash feature is iOS-only by moving mesh list outside iOS switch (ContentView)

* ContentView: add macOS mesh peer list under #else to fix missing People section on macOS

* Refactor People section: extract MeshPeerList and GeohashPeopleList into separate views to reduce SwiftUI type-checking complexity; integrate in ContentView

* BLE: prevent re-fragmentation loops; UI: darker mesh blue + first-row spacing in peer list

* Mesh flood/battery: probabilistic relays, adaptive TTL caps, fragment pacing + scan pause, assembly cap; enable relayed DMs with direct-first then flood fallback.

* Refactor BLEService broadcast: split pad policy, encrypted/direct-first handler, and link-aware sender; simplify broadcastPacket to delegate by type.

* Extract relay decision into RelayController with RelayDecision; simplify handleReceivedPacket to use it.

* Move RelayController and RelayDecision into their own file under Services; keep BLEService leaner.

* UI: adjust mesh blue to a darker, less purple tone; apply consistently for count and #mesh badge.

* Project: include RelayController.swift in target and sync project file changes.

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2025-08-20 16:27:49 +02:00

95 lines
4.7 KiB
Swift

import SwiftUI
struct MeshPeerList: View {
@ObservedObject var viewModel: ChatViewModel
let textColor: Color
let secondaryTextColor: Color
let onTapPeer: (String) -> Void
let onToggleFavorite: (String) -> Void
let onShowFingerprint: (String) -> Void
var body: some View {
Group {
if viewModel.allPeers.isEmpty {
Text("nobody around...")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
.padding(.top, 12)
} else {
let peerNicknames = viewModel.meshService.getPeerNicknames()
let myPeerID = viewModel.meshService.myPeerID
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
let isMe = peer.id == myPeerID
let hasUnread = viewModel.hasUnreadMessages(for: peer.id)
let enc = viewModel.getEncryptionStatus(for: peer.id)
return (peer, isMe, hasUnread, enc)
}
let peers = mapped.sorted { lhs, rhs in
let lFav = lhs.peer.favoriteStatus?.isFavorite ?? false
let rFav = rhs.peer.favoriteStatus?.isFavorite ?? false
if lFav != rFav { return lFav }
let lhsName = lhs.isMe ? viewModel.nickname : lhs.peer.nickname
let rhsName = rhs.isMe ? viewModel.nickname : rhs.peer.nickname
return lhsName < rhsName
}
ForEach(0..<peers.count, id: \.self) { idx in
let item = peers[idx]
let peer = item.peer
let isMe = item.isMe
let hasUnread = item.hasUnread
HStack(spacing: 4) {
if isMe {
Image(systemName: "person.fill").font(.system(size: 10)).foregroundColor(textColor)
} else if hasUnread {
Image(systemName: "envelope.fill").font(.system(size: 12)).foregroundColor(.orange)
} else {
switch peer.connectionState {
case .bluetoothConnected:
Image(systemName: "dot.radiowaves.left.and.right").font(.system(size: 10)).foregroundColor(textColor)
case .nostrAvailable:
Image(systemName: "globe").font(.system(size: 10)).foregroundColor(.purple)
case .offline:
if peer.favoriteStatus?.isFavorite ?? false {
Image(systemName: "moon.fill").font(.system(size: 10)).foregroundColor(.gray)
} else {
Image(systemName: "person").font(.system(size: 10)).foregroundColor(.gray)
}
}
}
let displayName = isMe ? viewModel.nickname : peer.nickname
Text(displayName)
.font(.system(size: 14, design: .monospaced))
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) || peerNicknames[peer.id] != nil ? textColor : secondaryTextColor)
if let icon = item.enc.icon, !isMe {
Image(systemName: icon)
.font(.system(size: 10))
.foregroundColor(item.enc == .noiseVerified || item.enc == .noiseSecured ? textColor : (item.enc == .noiseHandshaking ? .orange : .red))
}
Spacer()
if !isMe {
Button(action: { onToggleFavorite(peer.id) }) {
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
.font(.system(size: 12))
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
.padding(.vertical, 4)
.padding(.top, idx == 0 ? 6 : 0)
.contentShape(Rectangle())
.onTapGesture { if !isMe { onTapPeer(peer.id) } }
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.id) } }
}
}
}
}
}