PeerID 27/n: ContentView and MeshPeerList (#835)

This commit is contained in:
Islam
2025-10-19 14:42:50 +02:00
committed by GitHub
parent d4967ae9c3
commit 13b19fb8eb
2 changed files with 46 additions and 47 deletions
+40 -41
View File
@@ -80,7 +80,7 @@ struct ContentView: View {
// Timer-based refresh removed; use LocationChannelManager live updates instead // Timer-based refresh removed; use LocationChannelManager live updates instead
// Window sizes for rendering (infinite scroll up) // Window sizes for rendering (infinite scroll up)
@State private var windowCountPublic: Int = 300 @State private var windowCountPublic: Int = 300
@State private var windowCountPrivate: [String: Int] = [:] @State private var windowCountPrivate: [PeerID: Int] = [:]
// MARK: - Computed Properties // MARK: - Computed Properties
@@ -124,7 +124,7 @@ struct ContentView: View {
private struct PrivateHeaderContext { private struct PrivateHeaderContext {
let headerPeerID: String let headerPeerID: PeerID
let peer: BitchatPeer? let peer: BitchatPeer?
let displayName: String let displayName: String
let isNostrAvailable: Bool let isNostrAvailable: Bool
@@ -328,9 +328,9 @@ struct ContentView: View {
// MARK: - Message List View // MARK: - Message List View
private func messagesView(privatePeer: String?, isAtBottom: Binding<Bool>) -> some View { private func messagesView(privatePeer: PeerID?, isAtBottom: Binding<Bool>) -> some View {
let messages: [BitchatMessage] = { let messages: [BitchatMessage] = {
if let peerID = PeerID(str: privatePeer) { if let peerID = privatePeer {
return viewModel.getPrivateChatMessages(for: peerID) return viewModel.getPrivateChatMessages(for: peerID)
} }
return viewModel.messages return viewModel.messages
@@ -470,7 +470,7 @@ struct ContentView: View {
} }
.onChange(of: viewModel.privateChats) { _ in .onChange(of: viewModel.privateChats) { _ in
if let peerID = privatePeer, if let peerID = privatePeer,
let messages = viewModel.privateChats[PeerID(str: peerID)], let messages = viewModel.privateChats[peerID],
!messages.isEmpty { !messages.isEmpty {
// If the newest private message is from me, always scroll // If the newest private message is from me, always scroll
let lastMsg = messages.last! let lastMsg = messages.last!
@@ -525,7 +525,7 @@ struct ContentView: View {
} }
.onAppear { .onAppear {
// Also check when view appears // Also check when view appears
if let peerID = PeerID(str: privatePeer) { if let peerID = privatePeer {
// Try multiple times to ensure read receipts are sent // Try multiple times to ensure read receipts are sent
viewModel.markPrivateMessagesAsRead(from: peerID) viewModel.markPrivateMessagesAsRead(from: peerID)
@@ -826,10 +826,10 @@ struct ContentView: View {
} }
private func scrollToBottom(on proxy: ScrollViewProxy, private func scrollToBottom(on proxy: ScrollViewProxy,
privatePeer: String?, privatePeer: PeerID?,
isAtBottom: Binding<Bool>) { isAtBottom: Binding<Bool>) {
let targetID: String? = { let targetID: String? = {
if let peer = PeerID(str: privatePeer), if let peer = privatePeer,
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id { let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
return "dm:\(peer)|\(last)" return "dm:\(peer)|\(last)"
} }
@@ -855,7 +855,7 @@ struct ContentView: View {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
let secondTarget: String? = { let secondTarget: String? = {
if let peer = PeerID(str: privatePeer), if let peer = privatePeer,
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id { let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
return "dm:\(peer)|\(last)" return "dm:\(peer)|\(last)"
} }
@@ -992,14 +992,14 @@ struct ContentView: View {
textColor: textColor, textColor: textColor,
secondaryTextColor: secondaryTextColor, secondaryTextColor: secondaryTextColor,
onTapPeer: { peerID in onTapPeer: { peerID in
viewModel.startPrivateChat(with: PeerID(str: peerID)) viewModel.startPrivateChat(with: peerID)
showSidebar = true showSidebar = true
}, },
onToggleFavorite: { peerID in onToggleFavorite: { peerID in
viewModel.toggleFavorite(peerID: PeerID(str: peerID)) viewModel.toggleFavorite(peerID: peerID)
}, },
onShowFingerprint: { peerID in onShowFingerprint: { peerID in
viewModel.showFingerprint(for: PeerID(str: peerID)) viewModel.showFingerprint(for: peerID)
} }
) )
} }
@@ -1014,7 +1014,7 @@ struct ContentView: View {
private var privateChatSheetView: some View { private var privateChatSheetView: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if let privatePeerID = viewModel.selectedPrivateChatPeer?.id { if let privatePeerID = viewModel.selectedPrivateChatPeer {
let headerContext = makePrivateHeaderContext(for: privatePeerID) let headerContext = makePrivateHeaderContext(for: privatePeerID)
HStack(spacing: 12) { HStack(spacing: 12) {
@@ -1038,12 +1038,11 @@ struct ContentView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID) privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID)
let peerID = PeerID(str: headerContext.headerPeerID) let isFavorite = viewModel.isFavorite(peerID: headerContext.headerPeerID)
let isFavorite = viewModel.isFavorite(peerID: peerID)
if !privatePeerID.hasPrefix("nostr_") { if !privatePeerID.isGeoDM {
Button(action: { Button(action: {
viewModel.toggleFavorite(peerID: peerID) viewModel.toggleFavorite(peerID: headerContext.headerPeerID)
}) { }) {
Image(systemName: isFavorite ? "star.fill" : "star") Image(systemName: isFavorite ? "star.fill" : "star")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
@@ -1082,7 +1081,7 @@ struct ContentView: View {
.background(backgroundColor) .background(backgroundColor)
} }
messagesView(privatePeer: viewModel.selectedPrivateChatPeer?.id, isAtBottom: $isAtBottomPrivate) messagesView(privatePeer: viewModel.selectedPrivateChatPeer, isAtBottom: $isAtBottomPrivate)
.background(backgroundColor) .background(backgroundColor)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
Divider() Divider()
@@ -1104,9 +1103,9 @@ struct ContentView: View {
) )
} }
private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: String) -> some View { private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: PeerID) -> some View {
Button(action: { Button(action: {
viewModel.showFingerprint(for: PeerID(str: context.headerPeerID)) viewModel.showFingerprint(for: context.headerPeerID)
}) { }) {
HStack(spacing: 6) { HStack(spacing: 6) {
if let connectionState = context.peer?.connectionState { if let connectionState = context.peer?.connectionState {
@@ -1129,7 +1128,7 @@ struct ContentView: View {
case .offline: case .offline:
EmptyView() EmptyView()
} }
} else if viewModel.meshService.isPeerReachable(PeerID(str: context.headerPeerID)) { } else if viewModel.meshService.isPeerReachable(context.headerPeerID) {
Image(systemName: "point.3.filled.connected.trianglepath.dotted") Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(textColor) .foregroundColor(textColor)
@@ -1139,7 +1138,7 @@ struct ContentView: View {
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(.purple) .foregroundColor(.purple)
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator")) .accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
} else if viewModel.meshService.isPeerConnected(PeerID(str: context.headerPeerID)) || viewModel.connectedPeers.contains(PeerID(str: context.headerPeerID)) { } else if viewModel.meshService.isPeerConnected(context.headerPeerID) || viewModel.connectedPeers.contains(context.headerPeerID) {
Image(systemName: "dot.radiowaves.left.and.right") Image(systemName: "dot.radiowaves.left.and.right")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
.foregroundColor(textColor) .foregroundColor(textColor)
@@ -1150,14 +1149,14 @@ struct ContentView: View {
.font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced)) .font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(textColor) .foregroundColor(textColor)
if !privatePeerID.hasPrefix("nostr_") { if !privatePeerID.isGeoDM {
let statusPeerID: String = { let statusPeerID: PeerID = {
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) { if privatePeerID.id.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID.id) {
return short.id return short
} }
return context.headerPeerID return context.headerPeerID
}() }()
let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID)) let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
if let icon = encryptionStatus.icon { if let icon = encryptionStatus.icon {
Image(systemName: icon) Image(systemName: icon)
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
@@ -1189,33 +1188,33 @@ struct ContentView: View {
.frame(height: headerHeight) .frame(height: headerHeight)
} }
private func makePrivateHeaderContext(for privatePeerID: String) -> PrivateHeaderContext { private func makePrivateHeaderContext(for privatePeerID: PeerID) -> PrivateHeaderContext {
let headerPeerID: String = { let headerPeerID: PeerID = {
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) { if privatePeerID.id.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID.id) {
return short.id return short
} }
return privatePeerID return privatePeerID
}() }()
let peer = viewModel.getPeer(byID: PeerID(str: headerPeerID)) let peer = viewModel.getPeer(byID: headerPeerID)
let displayName: String = { let displayName: String = {
if privatePeerID.hasPrefix("nostr_"), case .location(let ch) = locationManager.selectedChannel { if privatePeerID.isGeoDM, case .location(let ch) = locationManager.selectedChannel {
let disp = viewModel.geohashDisplayName(for: PeerID(str: privatePeerID)) let disp = viewModel.geohashDisplayName(for: privatePeerID)
return "#\(ch.geohash)/@\(disp)" return "#\(ch.geohash)/@\(disp)"
} }
if let name = peer?.displayName { return name } if let name = peer?.displayName { return name }
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: headerPeerID)) { return name } if let name = viewModel.meshService.peerNickname(peerID: headerPeerID) { return name }
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data()), if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID.id) ?? Data()),
!fav.peerNickname.isEmpty { return fav.peerNickname } !fav.peerNickname.isEmpty { return fav.peerNickname }
if headerPeerID.count == 16 { if headerPeerID.id.count == 16 {
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(PeerID(str: headerPeerID)) let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
if let id = candidates.first, if let id = candidates.first,
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) { let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) {
if let pet = social.localPetname, !pet.isEmpty { return pet } if let pet = social.localPetname, !pet.isEmpty { return pet }
if !social.claimedNickname.isEmpty { return social.claimedNickname } if !social.claimedNickname.isEmpty { return social.claimedNickname }
} }
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) { } else if let keyData = headerPeerID.noiseKey {
let fp = keyData.sha256Fingerprint() let fp = keyData.sha256Fingerprint()
if let social = viewModel.identityManager.getSocialIdentity(for: fp) { if let social = viewModel.identityManager.getSocialIdentity(for: fp) {
if let pet = social.localPetname, !pet.isEmpty { return pet } if let pet = social.localPetname, !pet.isEmpty { return pet }
@@ -1227,7 +1226,7 @@ struct ContentView: View {
let isNostrAvailable: Bool = { let isNostrAvailable: Bool = {
guard let connectionState = peer?.connectionState else { guard let connectionState = peer?.connectionState else {
if let noiseKey = Data(hexString: headerPeerID), if let noiseKey = Data(hexString: headerPeerID.id),
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
favoriteStatus.isMutual { favoriteStatus.isMutual {
return true return true
@@ -1746,7 +1745,7 @@ private extension ContentView {
private func expandWindow(ifNeededFor message: BitchatMessage, private func expandWindow(ifNeededFor message: BitchatMessage,
allMessages: [BitchatMessage], allMessages: [BitchatMessage],
privatePeer: String?, privatePeer: PeerID?,
proxy: ScrollViewProxy) { proxy: ScrollViewProxy) {
let step = TransportConfig.uiWindowStepCount let step = TransportConfig.uiWindowStepCount
let contextKey: String = { let contextKey: String = {
+6 -6
View File
@@ -4,9 +4,9 @@ struct MeshPeerList: View {
@ObservedObject var viewModel: ChatViewModel @ObservedObject var viewModel: ChatViewModel
let textColor: Color let textColor: Color
let secondaryTextColor: Color let secondaryTextColor: Color
let onTapPeer: (String) -> Void let onTapPeer: (PeerID) -> Void
let onToggleFavorite: (String) -> Void let onToggleFavorite: (PeerID) -> Void
let onShowFingerprint: (String) -> Void let onShowFingerprint: (PeerID) -> Void
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = [] @State private var orderedIDs: [String] = []
@@ -130,7 +130,7 @@ struct MeshPeerList: View {
} }
if !isMe { if !isMe {
Button(action: { onToggleFavorite(peer.peerID.id) }) { Button(action: { onToggleFavorite(peer.peerID) }) {
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star") Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
.font(.bitchatSystem(size: 12)) .font(.bitchatSystem(size: 12))
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor) .foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
@@ -142,8 +142,8 @@ struct MeshPeerList: View {
.padding(.vertical, 4) .padding(.vertical, 4)
.padding(.top, idx == 0 ? 10 : 0) .padding(.top, idx == 0 ? 10 : 0)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { if !isMe { onTapPeer(peer.peerID.id) } } .onTapGesture { if !isMe { onTapPeer(peer.peerID) } }
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID.id) } } .onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID) } }
} }
} }
// Seed and update order outside result builder // Seed and update order outside result builder