PeerID 23/n: ChatViewModel + its dependences (#801)

This commit is contained in:
Islam
2025-10-15 00:20:19 +02:00
committed by GitHub
parent 3479c7d5df
commit 47d75ab9d8
8 changed files with 223 additions and 261 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
// Get peer ID from userInfo
if let peerID = userInfo["peerID"] as? String {
DispatchQueue.main.async {
self.chatViewModel?.startPrivateChat(with: peerID)
self.chatViewModel?.startPrivateChat(with: PeerID(str: peerID))
}
}
}
+5 -5
View File
@@ -148,9 +148,9 @@ final class CommandProcessor {
if chatViewModel?.selectedPrivateChatPeer != nil {
// In private chat
if let peerNickname = meshService?.peerNickname(peerID: PeerID(str: targetPeerID)) {
if let peerNickname = meshService?.peerNickname(peerID: targetPeerID) {
let personalMessage = "* \(emoji) \(myNickname) \(action) you\(suffix) *"
meshService?.sendPrivateMessage(personalMessage, to: PeerID(str: targetPeerID),
meshService?.sendPrivateMessage(personalMessage, to: targetPeerID,
recipientNickname: peerNickname,
messageID: UUID().uuidString)
// Also add a local system message so the sender sees a natural-language confirmation
@@ -214,7 +214,7 @@ final class CommandProcessor {
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) {
let fingerprint = meshService?.getFingerprint(for: peerID) {
if identityManager.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is already blocked")
}
@@ -258,7 +258,7 @@ final class CommandProcessor {
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) {
let fingerprint = meshService?.getFingerprint(for: peerID) {
if !identityManager.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is not blocked")
}
@@ -285,7 +285,7 @@ final class CommandProcessor {
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let noisePublicKey = Data(hexString: peerID) else {
let noisePublicKey = Data(hexString: peerID.id) else {
return .error(message: "can't find peer: \(nickname)")
}
+3 -3
View File
@@ -235,10 +235,10 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
}
/// Get peer ID for nickname
func getPeerID(for nickname: String) -> String? {
func getPeerID(for nickname: String) -> PeerID? {
for peer in peers {
if peer.displayName == nickname || peer.nickname == nickname {
return peer.peerID.id
return peer.peerID
}
}
return nil
@@ -347,7 +347,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Compatibility Methods (for easy migration)
var allPeers: [BitchatPeer] { peers }
var connectedPeers: [PeerID] { Array(connectedPeerIDs) }
var connectedPeers: Set<PeerID> { connectedPeerIDs }
var favoritePeers: Set<String> {
Set(favorites.compactMap { getFingerprint(for: $0.peerID) })
}
File diff suppressed because it is too large Load Diff
+31 -29
View File
@@ -172,7 +172,7 @@ struct ContentView: View {
set: { _ in viewModel.showingFingerprintFor = nil }
)) {
if let peerID = viewModel.showingFingerprintFor {
FingerprintView(viewModel: viewModel, peerID: peerID)
FingerprintView(viewModel: viewModel, peerID: peerID.id)
}
}
.confirmationDialog(
@@ -191,11 +191,11 @@ struct ContentView: View {
Button("content.actions.direct_message") {
if let peerID = selectedMessageSenderID {
if peerID.hasPrefix("nostr:") {
if let full = viewModel.fullNostrHex(forSenderPeerID: peerID) {
if let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)) {
viewModel.startGeohashDM(withPubkeyHex: full)
}
} else {
viewModel.startPrivateChat(with: peerID)
viewModel.startPrivateChat(with: PeerID(str: peerID))
}
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar = true
@@ -218,7 +218,7 @@ struct ContentView: View {
Button("content.actions.block", role: .destructive) {
// Prefer direct geohash block when we have a Nostr sender ID
if let peerID = selectedMessageSenderID, peerID.hasPrefix("nostr:"),
let full = viewModel.fullNostrHex(forSenderPeerID: peerID),
let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)),
let sender = selectedMessageSender {
viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender)
} else if let sender = selectedMessageSender {
@@ -255,8 +255,8 @@ struct ContentView: View {
LazyVStack(alignment: .leading, spacing: 0) {
// Extract messages based on context (private or public chat)
let messages: [BitchatMessage] = {
if let privatePeer = privatePeer {
let msgs = viewModel.getPrivateChatMessages(for: privatePeer)
if let privatePeer {
let msgs = viewModel.getPrivateChatMessages(for: PeerID(str: privatePeer))
return msgs
} else {
return viewModel.messages
@@ -370,22 +370,22 @@ struct ContentView: View {
.onOpenURL { url in
guard url.scheme == "bitchat", url.host == "user" else { return }
let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let peerID = id.removingPercentEncoding ?? id
selectedMessageSenderID = peerID
let peerID = PeerID(str: id.removingPercentEncoding ?? id)
selectedMessageSenderID = peerID.id
// Derive a stable display name from the peerID instead of peeking at the last message,
// which may be a transformed system action (sender == "system").
if peerID.hasPrefix("nostr") {
if peerID.isGeoDM || peerID.isGeoChat {
// For geohash senders, resolve display name via mapping (works for "nostr:" and "nostr_" keys)
selectedMessageSender = viewModel.geohashDisplayName(for: peerID)
} else {
// Mesh sender: use current mesh nickname if available; otherwise fall back to last non-system message
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: peerID)) {
if let name = viewModel.meshService.peerNickname(peerID: peerID) {
selectedMessageSender = name
} else {
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
}
}
if viewModel.isSelfSender(peerID: selectedMessageSenderID, displayName: selectedMessageSender) {
if viewModel.isSelfSender(peerID: peerID, displayName: selectedMessageSender) {
selectedMessageSender = nil
selectedMessageSenderID = nil
} else {
@@ -426,7 +426,7 @@ struct ContentView: View {
// Force scroll to bottom when opening a chat view
let targetID: String? = {
if let peer = privatePeer,
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
let last = viewModel.getPrivateChatMessages(for: PeerID(str: peer)).suffix(300).last?.id {
return "dm:\(peer)|\(last)"
}
let contextKey: String = {
@@ -446,7 +446,7 @@ struct ContentView: View {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
let targetID2: String? = {
if let peer = privatePeer,
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
let last = viewModel.getPrivateChatMessages(for: PeerID(str: peer)).suffix(300).last?.id {
return "dm:\(peer)|\(last)"
}
let contextKey: String = {
@@ -465,7 +465,7 @@ struct ContentView: View {
// When switching to a different private chat, jump to bottom
let targetID: String? = {
if let peer = privatePeer,
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
let last = viewModel.getPrivateChatMessages(for: PeerID(str: peer)).suffix(300).last?.id {
return "dm:\(peer)|\(last)"
}
let contextKey: String = {
@@ -587,7 +587,7 @@ struct ContentView: View {
}
.onAppear {
// Also check when view appears
if let peerID = privatePeer {
if let peerID = PeerID(str: privatePeer) {
// Try multiple times to ensure read receipts are sent
viewModel.markPrivateMessagesAsRead(from: peerID)
@@ -935,14 +935,14 @@ struct ContentView: View {
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onTapPeer: { peerID in
viewModel.startPrivateChat(with: peerID)
viewModel.startPrivateChat(with: PeerID(str: peerID))
showSidebar = true
},
onToggleFavorite: { peerID in
viewModel.toggleFavorite(peerID: peerID)
viewModel.toggleFavorite(peerID: PeerID(str: peerID))
},
onShowFingerprint: { peerID in
viewModel.showFingerprint(for: peerID)
viewModel.showFingerprint(for: PeerID(str: peerID))
}
)
}
@@ -981,18 +981,20 @@ struct ContentView: View {
HStack(spacing: 8) {
privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID)
let peerID = PeerID(str: headerContext.headerPeerID)
let isFavorite = viewModel.isFavorite(peerID: peerID)
if !privatePeerID.hasPrefix("nostr_") {
Button(action: {
viewModel.toggleFavorite(peerID: headerContext.headerPeerID)
viewModel.toggleFavorite(peerID: peerID)
}) {
Image(systemName: viewModel.isFavorite(peerID: headerContext.headerPeerID) ? "star.fill" : "star")
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.bitchatSystem(size: 14))
.foregroundColor(viewModel.isFavorite(peerID: headerContext.headerPeerID) ? Color.yellow : textColor)
.foregroundColor(isFavorite ? Color.yellow : textColor)
}
.buttonStyle(.plain)
.accessibilityLabel(
viewModel.isFavorite(peerID: headerContext.headerPeerID)
isFavorite
? String(localized: "content.accessibility.remove_favorite", comment: "Accessibility label to remove a favorite")
: String(localized: "content.accessibility.add_favorite", comment: "Accessibility label to add a favorite")
)
@@ -1047,7 +1049,7 @@ struct ContentView: View {
private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: String) -> some View {
Button(action: {
viewModel.showFingerprint(for: context.headerPeerID)
viewModel.showFingerprint(for: PeerID(str: context.headerPeerID))
}) {
HStack(spacing: 6) {
if let connectionState = context.peer?.connectionState {
@@ -1080,7 +1082,7 @@ struct ContentView: View {
.font(.bitchatSystem(size: 14))
.foregroundColor(.purple)
.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(context.headerPeerID) {
} else if viewModel.meshService.isPeerConnected(PeerID(str: context.headerPeerID)) || viewModel.connectedPeers.contains(PeerID(str: context.headerPeerID)) {
Image(systemName: "dot.radiowaves.left.and.right")
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
@@ -1094,11 +1096,11 @@ struct ContentView: View {
if !privatePeerID.hasPrefix("nostr_") {
let statusPeerID: String = {
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
return short
return short.id
}
return context.headerPeerID
}()
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID))
if let icon = encryptionStatus.icon {
Image(systemName: icon)
.font(.bitchatSystem(size: 14))
@@ -1133,16 +1135,16 @@ struct ContentView: View {
private func makePrivateHeaderContext(for privatePeerID: String) -> PrivateHeaderContext {
let headerPeerID: String = {
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
return short
return short.id
}
return privatePeerID
}()
let peer = viewModel.getPeer(byID: headerPeerID)
let peer = viewModel.getPeer(byID: PeerID(str: headerPeerID))
let displayName: String = {
if privatePeerID.hasPrefix("nostr_"), case .location(let ch) = locationManager.selectedChannel {
let disp = viewModel.geohashDisplayName(for: privatePeerID)
let disp = viewModel.geohashDisplayName(for: PeerID(str: privatePeerID))
return "#\(ch.geohash)/@\(disp)"
}
if let name = peer?.displayName { return name }
+5 -4
View File
@@ -66,12 +66,12 @@ struct FingerprintView: View {
VStack(alignment: .leading, spacing: 16) {
// Prefer short mesh ID for session/encryption status
let statusPeerID: String = {
if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short }
if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short.id }
return peerID
}()
// Resolve a friendly name
let peerNickname: String = {
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
if let p = viewModel.getPeer(byID: PeerID(str: statusPeerID)) { return p.displayName }
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: statusPeerID)) { return name }
if peerID.count == 64, let data = Data(hexString: peerID) {
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
@@ -84,7 +84,7 @@ struct FingerprintView: View {
return Strings.unknownPeer()
}()
// Accurate encryption state based on short ID session
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID))
HStack {
if let icon = encryptionStatus.icon {
@@ -115,7 +115,7 @@ struct FingerprintView: View {
.font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
if let fingerprint = viewModel.getFingerprint(for: statusPeerID) {
if let fingerprint = viewModel.getFingerprint(for: PeerID(str: statusPeerID)) {
Text(formatFingerprint(fingerprint))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(textColor)
@@ -176,6 +176,7 @@ struct FingerprintView: View {
// Verification status
if encryptionStatus == .noiseSecured || encryptionStatus == .noiseVerified {
let isVerified = encryptionStatus == .noiseVerified
let peerID = PeerID(str: peerID)
VStack(spacing: 12) {
Text(isVerified ? Strings.verifiedBadge : Strings.notVerifiedBadge)
+5 -5
View File
@@ -21,8 +21,8 @@ struct MeshPeerList: View {
let myPeerID = viewModel.meshService.myPeerID
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
let isMe = peer.peerID == myPeerID
let hasUnread = viewModel.hasUnreadMessages(for: peer.peerID.id)
let enc = viewModel.getEncryptionStatus(for: peer.peerID.id)
let hasUnread = viewModel.hasUnreadMessages(for: peer.peerID)
let enc = viewModel.getEncryptionStatus(for: peer.peerID)
return (peer, isMe, hasUnread, enc)
}
// Stable visual order without mutating state here
@@ -47,7 +47,7 @@ struct MeshPeerList: View {
let peer = item.peer
let isMe = item.isMe
HStack(spacing: 4) {
let assigned = viewModel.colorForMeshPeer(id: peer.peerID.id, isDark: colorScheme == .dark)
let assigned = viewModel.colorForMeshPeer(id: peer.peerID, isDark: colorScheme == .dark)
let baseColor = isMe ? Color.orange : assigned
if isMe {
Image(systemName: "person.fill")
@@ -89,7 +89,7 @@ struct MeshPeerList: View {
}
}
if !isMe, viewModel.isPeerBlocked(peer.peerID.id) {
if !isMe, viewModel.isPeerBlocked(peer.peerID) {
Image(systemName: "nosign")
.font(.bitchatSystem(size: 10))
.foregroundColor(.red)
@@ -105,7 +105,7 @@ struct MeshPeerList: View {
}
} else {
// Offline: prefer showing verified badge from persisted fingerprints
if let fp = viewModel.getFingerprint(for: peer.peerID.id),
if let fp = viewModel.getFingerprint(for: peer.peerID),
viewModel.verifiedFingerprints.contains(fp) {
Image(systemName: "checkmark.seal.fill")
.font(.bitchatSystem(size: 10))
+1 -1
View File
@@ -373,7 +373,7 @@ struct VerificationSheetView: View {
}
// Optional: Remove verification for selected peer (if verified)
if let pid = viewModel.selectedPrivateChatPeer?.id,
if let pid = viewModel.selectedPrivateChatPeer,
let fp = viewModel.getFingerprint(for: pid),
viewModel.verifiedFingerprints.contains(fp) {
Button(action: { viewModel.unverifyFingerprint(for: pid) }) {