* Remove link previews: link URLs inline\n\n- Delete LinkPreviewView and all references\n- Strip preview usage from ContentView\n- Make detected URLs tappable via .link attribute\n- Add MessageTextHelpers for tokens/length checks\n- Wire helpers into iOS/macOS targets

* Peer list UX: match message colors, use map pin icon, stable ordering\n\n- Color list names and icons with same peer color as messages\n- Default icon mappin; teleported uses face.dashed (iOS)\n- Geohash: move teleported peers to bottom\n- Maintain stable order: append new peers, remove missing\n- Mesh list: replace state icons with colored mappin/person

* Fix peer list build errors: remove ambiguous Set type, avoid guard in ViewBuilder, remove duplicate bindings, and simplify person lookup

* Peer list: remove onChange expressions to avoid type-check issues; rely on ObservedObject updates

* Peer lists: simplify view structure to satisfy SwiftUI type checker (replace Group with VStack, flatten body)

* Peer lists: move ordering mutations to onAppear/onChange, keep body pure; fix result builder errors

* Fix: remove trailing modifiers outside if/else to avoid ViewBuilder type issues

* Color parity: fix seed cache to include color scheme; normalize Nostr seeds; switch to mappin.circle icon

* Peer colors: ensure exact match with message colors\n\n- Mesh list uses same seed logic as messages (getNoiseKeyForShortID fallback)\n- Color cache keyed by theme; normalize nostr seeds\n- Icons: use mappin.and.ellipse (default) and face.dashed for teleported\n- Teleport tag: accept "teleported" in addition to "teleport" and presence events

* Fix geohash color mismatch: populate nostrKeyMapping before building messages in resubscribe; also honor t,teleport tag for teleported state

* Message actions: username tap opens sheet via clickable AttributedString; message tap inserts @mention; rename to 'direct message'

* Cashu UX: suppress 'Show more' when message contains a Cashu token (chips handle rendering)

* Cashu detection: broaden regex (allow '.') + shorter min length; avoid long-text fallback when Cashu present so chips render and no full blob

* Geohash levels: rename region->province, country->region; update labels, precision, UI, manager mapping, tests; add Codable backward-compat for renamed cases

* Fix braces in LocationChannel.swift: close enum before extension to satisfy file-scope declarations

* Geohash links: make #geohash tappable and open that channel (bitchat://geohash/<gh>); handle in ContentView.onOpenURL with validation and selection

* Underline tappable #geohash mentions for clarity

* Geochat: sanitize timeline on channel switch to remove any blank-content entries (prevents blank rows)

* Xcode project: sync sources after UI changes (remove LinkPreview, add helpers, update build refs)

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-24 02:05:02 +02:00
committed by GitHub
co-authored by jack
parent 2758afa126
commit 0260798712
11 changed files with 336 additions and 668 deletions
+36 -38
View File
@@ -9,60 +9,48 @@ struct MeshPeerList: View {
let onShowFingerprint: (String) -> Void
@Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = []
var body: some View {
Group {
if viewModel.allPeers.isEmpty {
if viewModel.allPeers.isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text("nobody around...")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
.padding(.top, 12)
} else {
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
}
}
} else {
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)
}
// Stable visual order without mutating state here
let currentIDs = mapped.map { $0.peer.id }
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
let peers: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = displayIDs.compactMap { id in
mapped.first(where: { $0.peer.id == id })
}
VStack(alignment: .leading, spacing: 0) {
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) {
let assigned = viewModel.colorForMeshPeer(id: peer.id, isDark: colorScheme == .dark)
let baseColor = isMe ? Color.orange : assigned
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)
Image(systemName: "person.fill").font(.system(size: 10)).foregroundColor(baseColor)
} 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)
}
}
Image(systemName: "mappin.and.ellipse").font(.system(size: 10)).foregroundColor(baseColor)
}
let displayName = isMe ? viewModel.nickname : peer.nickname
let (base, suffix) = splitSuffix(from: displayName)
let assigned = viewModel.colorForMeshPeer(id: peer.id, isDark: colorScheme == .dark)
let baseColor = isMe ? Color.orange : assigned
HStack(spacing: 0) {
Text(base)
.font(.system(size: 14, design: .monospaced))
@@ -75,7 +63,6 @@ struct MeshPeerList: View {
}
}
// Blocked indicator
if !isMe, viewModel.isPeerBlocked(peer.id) {
Image(systemName: "nosign")
.font(.system(size: 10))
@@ -86,7 +73,7 @@ struct MeshPeerList: View {
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))
.foregroundColor(baseColor)
}
Spacer()
@@ -108,6 +95,17 @@ struct MeshPeerList: View {
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.id) } }
}
}
// Seed and update order outside result builder
.onAppear {
let currentIDs = mapped.map { $0.peer.id }
orderedIDs = currentIDs
}
.onChange(of: mapped.map { $0.peer.id }) { 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 }
}
}
}
}